A Kotlin library that leverages existing mocks to create Pact contract files, using the popular mocking libraries Mockk and Mockito.
Pact is a powerful tool for ensuring the compatibility of microservices. It allows you to define contracts between your services and test them in isolation.
However, writing these contracts can be time-consuming and repetitive. This is where pact-jvm-mock comes in. It automatically generates Pact contracts from your existing Mockk mocks or Mockito mocks, saving you time and reducing the risk of human error.
- Automatically generate Pact contracts from existing Mockk mocks or Mockito mocks
- Supports all common mock interactions, such as method calls and property accesses
- Compatible with Spring Rest Template client and fully extensible
- Easy to integrate with your existing testing workflow
To get started with pact-jvm-mock, you'll need to add the library to your project. You can do this by adding the following dependency to your build.gradle file:
Gradle
testImplementation "io.github.ludorival:pact-jvm-mockk-spring:$pactJvmMockVersion"
Or if you are using Maven:
Maven
<!-- For intercepting Mockk calls -->
<dependency>
<groupId>io.github.ludorival</groupId>
<artifactId>pact-jvm-mock-mockk</artifactId>
<version>${pactJvmMockVersion}</version>
<scope>test</scope>
</dependency>
<!-- For intercepting Mockito calls -->
<dependency>
<groupId>io.github.ludorival</groupId>
<artifactId>pact-jvm-mock-mockito</artifactId>
<version>${pactJvmMockVersion}</version>
<scope>test</scope>
</dependency>
<!-- For intercepting Spring RestTemplate calls -->
<dependency>
<groupId>io.github.ludorival</groupId>
<artifactId>pact-jvm-mock-spring</artifactId>
<version>${pactJvmMockVersion}</version>
<scope>test</scope>
</dependency>
Next, you'll need to configure the library to use your existing Mockk mocks.
For example, let's say you want to leverage existing mock of Spring RestTemplate.
Create a Kotlin object (or use an existing one) implementing PactConfiguration
. Here is a minimal example:
import io.github.ludorival.pactjvm.mock.PactConfiguration
import io.github.ludorival.pactjvm.mock.spring.SpringRestTemplateMockAdapter
object MyServicePactConfig : PactConfiguration("my-service", SpringRestTemplateMockAdapter())
Then, to start writing contract, you have to extend your test classes where you need to record the interactions with your providers. Like that
import io.github.ludorival.pactjvm.mock.mockk.PactConsumer
@PactConsumer(MyServicePactConfig::class)
class ShoppingServiceClientTest
Finally, you can use the library to generate your Pact contracts.
The library is fully compatible with your existing Mockk code. You just need to replace all your every
calls with uponReceiving
:
// Your existing Mockk code
every {
restTemplate.getForEntity(match<String> { it.contains("user-service") }, UserProfile::class.java)
} returns ResponseEntity.ok(USER_PROFILE)
// Simply becomes
uponReceiving {
restTemplate.getForEntity(match<String> { it.contains("user-service") }, UserProfile::class.java)
} returns ResponseEntity.ok(USER_PROFILE)
All your existing Mockk matchers and features continue to work exactly the same way. The library supports:
- Static responses with
returns
- Dynamic responses with
answers
- Multiple responses with
andThen
- Exception throwing with
throws
- Coroutines with
coAnswers
This migration can be easily done with a Replace All
in your IDE, replacing every
with uponReceiving
.
To use pact-jvm-mock with Mockito in Java, simply replace all your Mockito.when
calls with PactMockito.uponReceiving
:
import static io.github.ludorival.pactjvm.mock.mockito.PactMockito.uponReceiving;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
// Simple response
uponReceiving(restTemplate.getForEntity(any(String.class), eq(UserProfile.class)))
.thenReturn(ResponseEntity.ok(USER_PROFILE));
// With error response
uponReceiving(restTemplate.postForEntity(any(URI.class), any(), eq(UserProfile.class)))
.withDescription("Create user profile - validation error")
.given((builder, call) -> builder.state("Invalid user data", Collections.emptyMap()))
.thenThrow(HttpClientErrorException.BadRequest.create(
HttpStatus.BAD_REQUEST,
errorMessage,
new HttpHeaders(),
errorMessage.getBytes(StandardCharsets.UTF_8),
StandardCharsets.UTF_8
));
You can also add optional configurations like descriptions, provider states, and matching rules:
uponReceiving(restTemplate.exchange(
any(URI.class),
eq(HttpMethod.GET),
any(),
any(ParameterizedTypeReference.class)
))
.withDescription("List users")
.given((builder, call) -> builder.state("Users exist", Collections.emptyMap()))
.matchingRequest(rules -> rules.header("Authorization", new RegexMatcher("Bearer .*")))
.matchingResponse(rules -> rules.body("[*].id", TypeMatcher.INSTANCE))
.thenReturn(ResponseEntity.ok(Arrays.asList(USER_1, USER_2)));
Note: Don't forget to configure your test class with the @PactConsumer
annotation:
@PactConsumer(MyPactConfiguration.class)
public class MyTest {
public static class MyPactConfiguration extends PactConfiguration {
public MyPactConfiguration() {
super("my-consumer", new SpringRestTemplateMockAdapter());
}
}
// ... test methods
}
That's it !!
Run your tests, and you should see the generated pact files in your src/test/resources/pacts
.
By default, the description is building from the current test name.You can set a description for each interaction using either withDescription
or as part of the response chain:
uponReceiving {
restTemplate.getForEntity(match<String> { it.contains("user-service") }, UserProfile::class.java)
} withDescription { "get the user profile" } returns ResponseEntity.ok(USER_PROFILE)
The provider state refers to the state of the API or service that is being tested.
You can specify provider states using the given
method:
uponReceiving {
restTemplate.getForEntity(match<String> { it.contains("user-service") }, UserProfile::class.java)
} given {
state("The user has a preferred shopping list")
} returns ResponseEntity.ok(USER_PROFILE)
You can specify matching rules for both requests and responses using matchingRequest
and matchingResponse
:
uponReceiving {
restTemplate.exchange(
match<URI> { it.path.contains("user-service") },
HttpMethod.GET,
any(),
any<ParameterizedTypeReference<List<User>>>()
)
} matchingRequest {
header("Authorization", RegexMatcher("Bearer .*"))
} matchingResponse {
body("[*].id", TypeMatcher)
} returns ResponseEntity.ok(listOf(USER_1, USER_2))
In this example:
- The request matching rule ensures the Authorization header matches the pattern "Bearer" followed by any string
- The response matching rule specifies that each user ID in the array should match by type rather than exact value
pact-jvm-mock
offers also a way to record Http errors thanks to the throws anError()
method:
uponReceiving {
restTemplate.postForEntity(
match<URI> { it.path.contains("shopping-service") },
any(),
eq(ShoppingList::class.java)
) given {
state("The request should return a 400 Bad request")
} throws anError(ResponseEntity.badRequest().body("The title contains unexpected character"))
You can specify a custom ObjectMapper for serializing request/response bodies for specific providers. This is useful when you need special serialization handling, like custom date formats or naming strategies.
// MyServicePactConfig.kt
object MyServicePactConfig : PactConfiguration("my-service", SpringRestTemplateMockAdapter({providerName ->
Jackson2ObjectMapperBuilder()
.propertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
.serializerByType(
LocalDate::class.java,
serializerAsDefault<LocalDate>("2023-01-01")
).build()
})) {
}
To make your contract deterministic, you will need to provide a custom serializer for the type you want to be invariant.
For example, let's say you have a date which will be generated at each test, you can pass a custom value
for determineConsumerFromUrl
// MyServicePactConfig.kt
object MyServicePactConfig : PactConfiguration("my-service", SpringRestTemplateMockAdapter({providerName ->
Jackson2ObjectMapperBuilder()
.propertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
.serializerByType(
LocalDate::class.java,
serializerAsDefault<LocalDate>("2023-01-01")
).build()
})) {
override fun isDeterministic() = true // <-- force to be deterministic
}
By default, the generated pacts are stored in src/test/resources/pacts
. You can configure that in the pact options:
// MyServicePactConfig.kt
object MyServicePactConfig : PactConfiguration("my-service") {
...
override fun getPactDirectory() = "my-own-directory"
}
pact-jvm-mock is an open-source project and contributions are welcome! If you're interested in contributing, please check out the contributing guidelines.
pact-jvm-mock is licensed under the MIT License.