pact-foundation / pact-jvm

JVM version of Pact. Enables consumer driven contract testing, providing a mock service and DSL for the consumer project, and interaction playback and verification for the service provider project.
https://docs.pact.io
Apache License 2.0
1.08k stars 479 forks source link

Question: How to verify spring webflux annotated controller endpoints #1724

Closed ramsimmin closed 1 year ago

ramsimmin commented 1 year ago

Hello, I'm trying to verify my Reactive RestContoller in a spring boot application with no success. Basically, my consumer (consumer1-app) expects a 200 OK success response when posting to the provider's (provider-app) endpoint "/api/ticket/create", thus my consumer contract looks like this:

{
    "provider": {
        "name": "provider-app"
    },
    "consumer": {
        "name": "consumer1-app"
    },
    "interactions": [
        {
            "description": "send the ticket body and expect a successful response",
            "request": {
                "method": "POST",
                "path": "/api/ticket/create",
                "headers": {
                    "Content-Type": "application/json"
                },
                "body": {
                    "message": "string",
                    "recipient": "string",
                    "sender": "string"
                },
                "matchingRules": {
                    "body": {
                        "$.message": {
                            "matchers": [
                                {
                                    "match": "type"
                                }
                            ],
                            "combine": "AND"
                        },
                        "$.sender": {
                            "matchers": [
                                {
                                    "match": "type"
                                }
                            ],
                            "combine": "AND"
                        },
                        "$.recipient": {
                            "matchers": [
                                {
                                    "match": "type"
                                }
                            ],
                            "combine": "AND"
                        }
                    },
                    "path": {

                    },
                    "header": {

                    }
                },
                "generators": {
                    "body": {
                        "$.message": {
                            "type": "RandomString",
                            "size": 20
                        },
                        "$.sender": {
                            "type": "RandomString",
                            "size": 20
                        },
                        "$.recipient": {
                            "type": "RandomString",
                            "size": 20
                        }
                    }
                }
            },
            "response": {
                "status": 200
            },
            "providerStates": [
                {
                    "name": "Create ticket"
                }
            ]
        }
    ],
    "metadata": {
        "pact-specification": {
            "version": "3.0.0"
        },
        "pact-jvm": {
            "version": "3.5.15"
        }
    }
}

In the provider's side, I have a simple rest controller, returning an empty Mono for now.

@RestController  
@RequestMapping(value = "api/ticket")  
@RequiredArgsConstructor  
public class TicketController {  

  @PostMapping(value = "/create")  
  public Mono<Void> saveTicket(@RequestBody TicketRequestDTO ticketRequestDTO) {  
      return Mono.empty();  
  }  
}

Testing this via curl returns http status 200 OK:

curl -d '{"message": "message x", "sender": "sender x", "recipient": "recipient x"}' -H 'Content-Type: application/json' http://localhost:8080/api/ticket/create -i

After placing the contract file into the pacts folder, I have tried to create my pact verification test as follows:

@Provider("provider-app")  
@PactFolder("pacts")  
@SpringBootTest  
@ExtendWith({PactVerificationInvocationContextProvider.class, SpringExtension.class})  
@AutoConfigureWebTestClient  
class ConsumerPactTestsWebflux {  

  @Autowired  
  WebTestClient webTestClient;  

  @TestTemplate  
  void pactVerificationTestTemplate(PactVerificationContext context) {  
    context.verifyInteraction();  
  }  

  @BeforeEach  
  void before(PactVerificationContext context) {  
    context.setTarget(new WebTestClientTarget(webTestClient));  
  }  

  @State({"Create ticket"})  
  public void verifyTicketCreation() { }  
}

and it fails with NoSuchMethodError:

  java.lang.NoSuchMethodError: 'org.springframework.http.HttpStatus org.springframework.test.web.reactive.server.EntityExchangeResult.getStatus()'
    at au.com.dius.pact.provider.spring.junit5.WebFluxBasedTestTarget$DefaultImpls.executeInteraction(WebFluxBasedTestTarget.kt:49)
    at au.com.dius.pact.provider.spring.junit5.WebTestClientTarget.executeInteraction(WebTestClientTarget.kt:9)
    at au.com.dius.pact.provider.junit5.PactVerificationContext.validateTestExecution(PactVerificationContext.kt:107)
    at au.com.dius.pact.provider.junit5.PactVerificationContext.verifyInteraction(PactVerificationContext.kt:61)
    at com.example.providerapi.pacts.ConsumerPactTestsWebflux.pactVerificationTestTemplate(ConsumerPactTestsWebflux.java:43)
...

These are my dependencies (on the providers side):

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-webflux'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'au.com.dius.pact.provider:junit5spring:4.6.3'
}

Is there any way to test my annotated reactive controller? Any help/guidance would be appreciated.

rholshausen commented 1 year ago

That looks like a mismatch with Spring versions. What version of Spring/Springboot are you using?

ramsimmin commented 1 year ago

I'm using spring boot 3.1.4

rholshausen commented 1 year ago

For Springboot 3, you need to use au.com.dius.pact.provider:spring6

ramsimmin commented 1 year ago

Thanks @rholshausen , instead of au.com.dius.pact.provider:junit5spring:4.6.3 I've used

  testImplementation 'au.com.dius.pact.provider:spring6:4.6.3'
  testImplementation 'au.com.dius.pact.provider:junit5:4.6.3'

Changing the test to make use of the PactVerificationSpring6Provider.class worked! Here's my updated test:

@Provider("provider-app")
@PactFolder("pacts")
@ExtendWith({SpringExtension.class})
@SpringBootTest
@AutoConfigureWebTestClient
class ConsumerPactTestsWebflux {

  @Autowired
  WebTestClient webTestClient;

  @Autowired
  TicketController ticketController;

  @TestTemplate
  @ExtendWith(PactVerificationSpring6Provider.class)
  void pactVerificationTestTemplate(PactVerificationContext context) {
    context.verifyInteraction();
  }

  @BeforeEach
  void before(PactVerificationContext context) {
    context.setTarget(new WebTestClientSpring6Target(webTestClient));
  }

  @State({"Create ticket"})
  public void verifyTicketCreation() { }
}