Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support of Spring RabbitMQ #257

Merged
merged 4 commits into from
Jan 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions .idea/.gitignore

This file was deleted.

20 changes: 0 additions & 20 deletions .idea/gradle.xml

This file was deleted.

6 changes: 0 additions & 6 deletions .idea/inspectionProfiles/Project_Default.xml

This file was deleted.

6 changes: 0 additions & 6 deletions .idea/kotlinc.xml

This file was deleted.

10 changes: 0 additions & 10 deletions .idea/misc.xml

This file was deleted.

6 changes: 0 additions & 6 deletions .idea/vcs.xml

This file was deleted.

94 changes: 73 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ and reducing the risk of human error.

- Automatically generate Pact contracts from existing [Mockk](https://github.com/mockk/mockk) mocks or [Mockito](https://github.com/mockito/mockito) mocks
- Supports all common mock interactions, such as method calls and property accesses
- Compatible with Spring Rest Template client and fully extensible
- Compatible with Spring Rest Template client and Spring RabbitMQ messaging
- Easy to integrate with your existing testing workflow

## Getting Started in 5 minutes
Expand All @@ -37,7 +37,7 @@ Or if you are using Maven:

**Maven**

````xml
```xml
<!-- For intercepting Mockk calls -->
<dependency>
<groupId>io.github.ludorival</groupId>
Expand All @@ -61,7 +61,7 @@ Or if you are using Maven:
<version>${pactJvmMockVersion}</version>
<scope>test</scope>
</dependency>
````
```

### Configure the pacts

Expand All @@ -77,15 +77,27 @@ import io.github.ludorival.pactjvm.mock.spring.SpringRestTemplateMockAdapter
object MyServicePactConfig : PactConfiguration("my-service", SpringRestTemplateMockAdapter())
```

For RabbitMQ messaging, you can use the `SpringRabbitMQMockAdapter`:

```kotlin
import io.github.ludorival.pactjvm.mock.PactConfiguration
import io.github.ludorival.pactjvm.mock.spring.SpringRabbitMQMockAdapter
import com.fasterxml.jackson.databind.ObjectMapper

object MyServicePactConfig : PactConfiguration(
SpringRabbitMQMockAdapter("my-service", customObjectMapper) // ObjectMapper is optional
)
```

### Extend your tests files

Then, to start writing contract,
you have to extend your test classes where you need to record the interactions with your providers. Like that

```kotlin
import io.github.ludorival.pactjvm.mock.mockk.PactConsumer
import io.github.ludorival.pactjvm.mock.mockk.EnablePactMock

@PactConsumer(MyServicePactConfig::class)
@EnablePactMock(MyServicePactConfig::class)
class ShoppingServiceClientTest
```

Expand All @@ -107,6 +119,22 @@ every {
uponReceiving {
restTemplate.getForEntity(match<String> { it.contains("user-service") }, UserProfile::class.java)
} returns ResponseEntity.ok(USER_PROFILE)

// For RabbitMQ messaging:
uponReceiving {
rabbitTemplate.convertAndSend(
any<String>(), // exchange
any<String>(), // routing key
any<OrderMessage>() // message
)
}.withDescription {
"Order message sent"
}.given {
state("order created", mapOf(
"exchange" to "orders",
"routing_key" to "order.created"
))
}.returns(Unit)
```

All your existing Mockk matchers and features continue to work exactly the same way. The library supports:
Expand Down Expand Up @@ -162,10 +190,10 @@ uponReceiving(restTemplate.exchange(
.thenReturn(ResponseEntity.ok(Arrays.asList(USER_1, USER_2)));
```

Note: Don't forget to configure your test class with the `@PactConsumer` annotation:
Note: Don't forget to configure your test class with the `@EnablePactMock` annotation:

```java
@PactConsumer(MyPactConfiguration.class)
@EnablePactMock(MyPactConfiguration.class)
public class MyTest {
public static class MyPactConfiguration extends PactConfiguration {

Expand Down Expand Up @@ -265,27 +293,51 @@ object MyServicePactConfig : PactConfiguration("my-service", SpringRestTemplateM

### Make your contract deterministic

To make your contract deterministic, you will need to provide a custom serializer for the type you want to be invariant.
When working with Pact contracts, it's important to ensure that your tests are deterministic - meaning they produce the same output every time they run. This is particularly important when dealing with dynamic data like timestamps, UUIDs, or any other values that change between test runs.

There are two ways to make your contract deterministic:

For example, let's say you have a date which will be generated at each test, you can pass a custom value
for `determineConsumerFromUrl`
1. **Enable deterministic mode globally**

Set `isDeterministic = true` in your `PactConfiguration`. When enabled, if the same interaction is recorded with different responses, the test will fail with an `IllegalStateException`:

```kotlin
// MyServicePactConfig.kt
object MyServicePactConfig : PactConfiguration("my-service", SpringRestTemplateMockAdapter({providerName ->
Jackson2ObjectMapperBuilder()
.propertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
.serializerByType(
LocalDate::class.java,
serializerAsDefault<LocalDate>("2023-01-01")
).build()
})) {
object MyServicePactConfig : PactConfiguration("my-service", SpringRestTemplateMockAdapter()) {
override fun isDeterministic() = true // Enable deterministic mode
}
```

override fun isDeterministic() = true // <-- force to be deterministic
}
2. **Handle dynamic values with custom serializers**

For specific fields that are naturally dynamic (like dates or IDs), you can provide custom serializers to ensure consistent values:

```kotlin
object MyServicePactConfig : PactConfiguration(
"my-service",
SpringRestTemplateMockAdapter({ providerName ->
Jackson2ObjectMapperBuilder()
.serializerByType(
LocalDateTime::class.java,
serializerAsDefault<LocalDateTime>("2023-01-01T00:00:00")
)
.serializerByType(
UUID::class.java,
serializerAsDefault<UUID>("123e4567-e89b-12d3-a456-426614174000")
)
.build()
})
) {
override fun isDeterministic() = true
}
```

When deterministic mode is enabled:
- The same interaction must always return the same response
- If an interaction changes, the test will fail with a detailed error message showing the differences
- You must provide unique descriptions for different interactions using `withDescription`
- Dynamic values should be handled with custom serializers

This ensures your contract tests are reliable and reproducible across different test runs.

### Change the pact directory

Expand Down
2 changes: 2 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ project(":pact-jvm-mock-spring") {
api(project(":pact-jvm-mock"))
implementation(kotlin("stdlib-jdk8"))
compileOnly("org.springframework:spring-web:6.2.1")
compileOnly("org.springframework.amqp:spring-rabbit:3.2.1")
compileOnly("com.fasterxml.jackson.core:jackson-databind:2.18.2")
}
}
Expand All @@ -200,6 +201,7 @@ project(":pact-jvm-mock-test") {
implementation(kotlin("stdlib-jdk8"))
implementation("org.springframework.boot:spring-boot-starter-web:3.0.2")
implementation("org.springframework:spring-web:6.0.4")
implementation("org.springframework.boot:spring-boot-starter-amqp:3.0.2")

testImplementation("io.mockk:mockk:1.13.16")
testImplementation(project(":pact-jvm-mock-spring"))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package io.github.ludorival.pactjvm.mock.spring

import au.com.dius.pact.core.model.OptionalBody
import au.com.dius.pact.core.model.ProviderState
import au.com.dius.pact.core.model.messaging.Message
import au.com.dius.pact.core.model.matchingrules.MatchingRulesImpl
import io.github.ludorival.pactjvm.mock.Call
import io.github.ludorival.pactjvm.mock.InteractionBuilder
import io.github.ludorival.pactjvm.mock.PactMockAdapter
import org.springframework.amqp.rabbit.core.RabbitTemplate
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.SerializationFeature

open class SpringRabbitMQMockAdapter(
private val provider: String,
private val objectMapper: ObjectMapper = defaultObjectMapper()
) : PactMockAdapter<Message>() {

constructor(provider: String) : this(provider, defaultObjectMapper())

override fun support(call: Call<*>): Boolean {
return call.self is RabbitTemplate
}

override fun <T> buildInteraction(
interactionBuilder: InteractionBuilder<T>,
providerName: String
): Message {
val call = interactionBuilder.call
val exchange = call.args.firstOrNull { it is String } as? String ?: ""
val routingKey = call.args.getOrNull(1) as? String ?: ""
val message = call.args.lastOrNull()

val messageContent = when (message) {
is String -> message
null -> ""
else -> objectMapper.writeValueAsString(message)
}

return interactionBuilder.build { Message(
description = description,
providerStates = providerStates,
contents = OptionalBody.body(messageContent.toByteArray()),
metadata = mutableMapOf(
"exchange" to exchange,
"routing_key" to routingKey
),
matchingRules = requestMatchingRules
)
}
}

override fun determineConsumerAndProvider(call: Call<*>): Pair<String, String> {
val exchange = call.args.firstOrNull { it is String } as? String
return Pair(exchange?.split(".")?.firstOrNull() ?: "default", provider)
}

companion object {
private fun defaultObjectMapper(): ObjectMapper = ObjectMapper()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ import io.github.ludorival.pactjvm.mock.*
import org.springframework.http.*

@Suppress("TooManyFunctions")
class SpringRestTemplateMockAdapter(private val objectMapperByProvider: (String) -> ObjectMapper? = { null }) :
open class SpringRestTemplateMockAdapter(private val consumer: String, private val objectMapperByProvider: (String) -> ObjectMapper? = { null }) :
PactMockAdapter<RequestResponseInteraction>() {

constructor(consumer: String) : this(consumer, { null })
private val defaultObjectMapper = ObjectMapper().apply {
setSerializationInclusion(JsonInclude.Include.NON_NULL)
}
Expand Down Expand Up @@ -73,9 +74,9 @@ class SpringRestTemplateMockAdapter(private val objectMapperByProvider: (String)
}
}

override fun determineProvider(call: Call<*>): String {
override fun determineConsumerAndProvider(call: Call<*>): Pair<String, String> {
val uri = call.getUri()
return uri.path.split("/").first { it.isNotBlank() }
return Pair(consumer, uri.path.split("/").first { it.isNotBlank() })
}

private fun <T> serializeBody(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.github.ludorival.pactjvm.mock.test.orderservice

import io.github.ludorival.pactjvm.mock.test.shoppingservice.config.RabbitMQConfig
import org.springframework.amqp.rabbit.annotation.RabbitListener
import org.springframework.stereotype.Service

@Service
open class OrderConsumerService {

@RabbitListener(queues = [RabbitMQConfig.ROUTING_KEY])
fun handleShoppingListOrdered(orderMessage: OrderMessage) {
// Process the order message
println("Received order message: $orderMessage")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.github.ludorival.pactjvm.mock.test.orderservice

import io.github.ludorival.pactjvm.mock.test.shoppingservice.ShoppingList

data class OrderMessage(
val shoppingListId: String,
val userId: Long,
val items: List<OrderItem>
) {
data class OrderItem(
val name: String,
val quantity: Int
)

companion object {
fun fromShoppingList(shoppingList: ShoppingList): OrderMessage {
return OrderMessage(
shoppingListId = shoppingList.id.toString(),
userId = shoppingList.userId,
items = shoppingList.items.map { item ->
OrderItem(
name = item.name,
quantity = item.quantity
)
}
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.github.ludorival.pactjvm.mock.test.orderservice

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.context.annotation.ComponentScan

@SpringBootApplication
@ComponentScan(basePackages = ["io.github.ludorival.pactjvm.mock.test.orderservice"])
open class OrderServiceApplication {
companion object {
@JvmStatic
fun main(args: Array<String>) {
org.springframework.boot.runApplication<OrderServiceApplication>(*args)
}
}
}
Loading
Loading