Skip to content

Commit

Permalink
Support of Spring RabbitMQ
Browse files Browse the repository at this point in the history
Fixes #256
  • Loading branch information
ludorival committed Jan 21, 2025
1 parent dcab710 commit f3018ee
Show file tree
Hide file tree
Showing 33 changed files with 565 additions and 342 deletions.
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.

34 changes: 31 additions & 3 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,6 +77,18 @@ 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,
Expand Down Expand Up @@ -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
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)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package io.github.ludorival.pactjvm.mock.test.orderservice

import org.springframework.amqp.core.Binding
import org.springframework.amqp.core.BindingBuilder
import org.springframework.amqp.core.Queue
import org.springframework.amqp.core.TopicExchange
import org.springframework.amqp.rabbit.connection.ConnectionFactory
import org.springframework.amqp.rabbit.core.RabbitTemplate
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter
import org.springframework.amqp.support.converter.MessageConverter
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration
import io.github.ludorival.pactjvm.mock.test.shoppingservice.config.RabbitMQConfig

@Configuration
class RabbitMQConfiguration {

@Bean
fun queue(): Queue = Queue(RabbitMQConfig.ROUTING_KEY)

@Bean
fun exchange(): TopicExchange = TopicExchange(RabbitMQConfig.EXCHANGE_NAME)

@Bean
fun binding(queue: Queue, exchange: TopicExchange): Binding =
BindingBuilder.bind(queue).to(exchange).with(RabbitMQConfig.ROUTING_KEY)

@Bean
fun messageConverter(): MessageConverter = Jackson2JsonMessageConverter()

@Bean
fun rabbitTemplate(connectionFactory: ConnectionFactory, messageConverter: MessageConverter): RabbitTemplate =
RabbitTemplate(connectionFactory).apply {
this.messageConverter = messageConverter
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package io.github.ludorival.pactjvm.mock.test.shoppingservice.config

import org.springframework.amqp.core.TopicExchange
import org.springframework.amqp.rabbit.connection.ConnectionFactory
import org.springframework.amqp.rabbit.core.RabbitTemplate
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
open class RabbitMQConfig {

companion object {
const val EXCHANGE_NAME = "shopping.topic"
const val ROUTING_KEY = "shopping.list.ordered"
}

@Bean
open fun exchange(): TopicExchange = TopicExchange(EXCHANGE_NAME)

@Bean
open fun messageConverter() = Jackson2JsonMessageConverter()

@Bean
open fun rabbitTemplate(connectionFactory: ConnectionFactory, messageConverter: Jackson2JsonMessageConverter): RabbitTemplate {
return RabbitTemplate(connectionFactory).apply {
this.messageConverter = messageConverter
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.github.ludorival.pactjvm.mock.test.shoppingservice.messaging

import io.github.ludorival.pactjvm.mock.test.orderservice.OrderMessage
import io.github.ludorival.pactjvm.mock.test.shoppingservice.ShoppingList
import io.github.ludorival.pactjvm.mock.test.shoppingservice.config.RabbitMQConfig
import org.springframework.amqp.rabbit.core.RabbitTemplate
import org.springframework.stereotype.Component

@Component
class ShoppingListOrderPublisher(private val rabbitTemplate: RabbitTemplate) {

fun publishOrderFromShoppingList(shoppingList: ShoppingList) {
val orderMessage = OrderMessage.fromShoppingList(shoppingList)
rabbitTemplate.convertAndSend(
RabbitMQConfig.EXCHANGE_NAME,
RabbitMQConfig.ROUTING_KEY,
orderMessage
)
}
}
Loading

0 comments on commit f3018ee

Please sign in to comment.