Awesome
pact-jvm-provider-spring-mvc
A test runner to help to write provider pact tests with spring mvc, followed pact spec.
There are already several projects help to write provider test in pact-jvm repository, e.g. gradle, maven, sbt, specs2, but we can't find a convenient way to write for spring-mvc projects.
Spring mvc has already provide a test framework to help to write integration test, the API is like:
when(myResponse.getStatusCode()).thenReturn(HttpStatus.OK);
when(myService.signInWithToken(any(String.class))).thenReturn(myResponse);
standaloneSetup(new MyController(myService))
.build()
.perform(
get("/my/users/current")
.contentType(MediaType.APPLICATION_JSON)
.header("token", "validToken"))
.andExpect(status().isOk());
You can see it used provided standaloneSetup
method to wrap a controller to perform request and valid response,
without starting a real server. In this case, we can easily mocking dependencies and do the testing.
Since pact provider test also needs some preparing tasks (mocking), we can follow the same approach to make thing easier (compared to starting a real server).
How to use it
This project provides a junit test runner: PactRunner
, and also some helper annotations.
An example:
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.when;
import static com.reagroup.pact.provider.PactRunner.uriPathEq;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import org.junit.Before;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.io.FileSystemResource;
import org.springframework.http.*;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.client.RestTemplate;
import com.reagroup.pact.provider.PactFile;
import com.reagroup.pact.provider.PactRunner;
import com.reagroup.pact.provider.ProviderState;
import controller.MyController;
@RunWith(PactRunner.class)
@ContextConfiguration(locations = {"classpath:applicationContextPactTest.xml"})
@PactFile("file:src/pactProviderTest/resources/consumer-project-provider-project.json")
public class ProviderPactTest {
@Autowired
private MyController myController;
@Autowired
private RestTemplate restTemplateMock;
@ProviderState("my-service forbids a request with invalid token")
public MyController myServiceForbidsARequestWithInvalidToken() throws Exception {
HttpEntity<String> myRequest = createRequest("request-invalid-token-1.json", "invalid-token");
when(restTemplateMock.exchange(uriPathEq("/my-service"), eq(HttpMethod.PUT), eq(myRequest), eq(String.class)))
.thenReturn(new ResponseEntity<>(HttpStatus.FORBIDDEN));
return myController;
}
private HttpEntity<String> createRequest(String file, String token) {
return new HttpEntity<>(readTrimmedPactFixture(file), createHeadersWithToken(token));
}
private HttpHeaders createHeadersWithToken(String token) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.add("token", token);
return headers;
}
private String readTrimmedPactFixture(String fileName) {
try {
String path = "src/pactProviderTest/resources/fixtures/" + fileName;
return FileCopyUtils.copyToString(new InputStreamReader(new FileSystemResource(new File(path)).getInputStream(), "UTF-8")).trim();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
@RunWith(PactRunner.class)
Use PactRunner
to run this test case, which will do the following:
- parse the data from the pact json file specified by
@PactFile
, get the all the interactions - find all methods annotated with
ProviderState
, which is used for mocking dependencies and finding the controllers to test - for each interaction,
PactRunner
will do the preparing based on provider state,standaloneSetup
the controller, perform the request, and compare the response
@ContextConfiguration(locations = {"classpath:applicationContextPactTest.xml"})
Tells spring where the context file is. Notice we can override some beans in the ...PactTest.xml
by some mocking beans, e.g.
restTemplateMock
@PactFile("file:src/pactProviderTest/resources/consumer-project-provider-project.json")
Tells PactRunner
where the pact file (upload by consumer) is. Alternatively one can use the @PactFolder
to point the runner towards a directory containing all the pact files, that should be used for the execution.
@ProviderState("my-service forbids a request with invalid token")
Mark a method as a preparing method for a specified provider state. The method will do 2 things:
- Do some mocking in the body
- Return the controller which will be tested against
The content of @ProviderState
should be exactly the same as one of the provider state in the pact JSON file, otherwise
an error will be thrown.
If your method in controller returns a DeferredResponse
instead of immediate response like EntityResponse
, you need to
add deferredResponseInMillis
with a value greater than 0
in ProviderState
, like:
@ProviderState("my-service forbids a request with invalid token", deferredResponseInMillis = 1000)
uriPathEq
You can use the uriPathEq
exposed by object PactRunner
to check if two URIs path equal to each other without the host part,
which may be useful when writing mocking methods.
Say: http://aaa.com:8888/hello/world
are equal to https://bbb.com:9999/hello/world
with uriPathEq
@ProviderContextPath
If the container uses a non-null context path, you can use this annotation to specify it so that MockMvc will be instrumented accordingly.
Run the test
Just run the test as normal junit test, you will get the reports.
A live demo
For Developers
Developing
Before submitting a pull request, make sure the tests pass by running:
./sbt clean test
How to upload to sonatype
-
Import PGP keys from https://rattic.eqx.realestate.com.au/cred/detail/2842/, follow the description there
-
Get the token from https://rattic.eqx.realestate.com.au/cred/detail/2828/, which is used for publishing only
-
Create
~/.sbt/0.13/sonatype.sbt
, fill content:credentials += Credentials("Sonatype Nexus Repository Manager", "oss.sonatype.org", "<token_username>", "<token_password>")
The
token_username
andtoken_password
is coming from step 2 -
When you modified the code, don't forget increase the
version
frombuild.sbt
-
run under sbt:
+publishSigned
(don't miss the+
, which means cross-building) -
release all published jars:
sonatypeReleaseAll
-
wait for several hours before it appears on maven central