Consumer Driven Testing with Pact & Spring Boot
Recently a colleague of mine stumbled across Pact.io, Our current application had grown to over 50 services and we we’re starting to have some integration test failures and a brittle dev / acceptance test environment. So we decided to have a look at ways to try help with this.
I started out by reading: https://docs.pact.io/faq/convinceme.html
Then watching: https://www.youtube.com/watch?v=-6x6XBDf9sQ&feature=youtu.be
Those 2 resources convinced me to give it a shot.
So I set out and created a quick set of Spring boot apps, the GitHub repo here, to test out the concepts and get everything working.
To highlight some important bits from the demo.
Consumer:
As Pact is a consumer driven test framework. This is where you define a unit test, that test mocks the http server response and you assert against that.
Once the test is successful it creates a pact json file in the /pacts directory.
public class TestProvider { @Rule public PactProviderRule provider = new PactProviderRule("test_provider", "localhost", 8081, this); @Pact(state = "default", provider = "test_provider", consumer = "test_consumer") public PactFragment createFragment(PactDslWithProvider builder) { Map<String, String> headers = new HashMap<>(); headers.put("content-type", "application/json"); return builder .given("default") .uponReceiving("Test User Service") .path("/user/1") .method("GET") .willRespondWith() .status(200) .headers(headers) .body("{" + " \"userName\": \"Bob\",\n" + " \"userId\": \"1\",\n" + " \"firstName\": null,\n" + " \"lastName\": null,\n" + " \"email\": null,\n" + " \"groups\": null\n" + "}") .toFragment(); } @Test @PactVerification("test_provider") public void runTest() throws IOException { final RestTemplate call = new RestTemplate(); final User expectedResponse = new User(); expectedResponse.setUserName("Bob"); expectedResponse.setUserId("1"); final User forEntity = call.getForObject(provider.getConfig().url() + "/user/1", User.class); assertThat(forEntity, sameBeanAs(expectedResponse)); } }
So after the “mock” test is run and the pact file has been created. You need to include a maven plugin …pact… that is then used to publish the content of the pacts/ folder to the pact broker… which is defined in the pom as below.
<dependencies> <!-- https://mvnrepository.com/artifact/au.com.dius/pact-jvm-consumer-junit_2.11 --> <dependency> <groupId>au.com.dius</groupId> <artifactId>pact-jvm-consumer-junit_2.11</artifactId> <version>3.3.6</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>2.18</version> <configuration> <systemPropertyVariables> <pact.rootDir>pacts</pact.rootDir> <buildDirectory>${project.build.directory}</buildDirectory> </systemPropertyVariables> </configuration> </plugin> <plugin> <groupId>au.com.dius</groupId> <artifactId>pact-jvm-provider-maven_2.11</artifactId> <version>3.3.4</version> <configuration> <pactDirectory>pacts</pactDirectory> <pactBrokerUrl>http://localhost:80</pactBrokerUrl> <projectVersion>1.0.1</projectVersion> </configuration> </plugin> </plugins> </build>
Producer:
This uses the JUnit integration from Pact.io to download the pacts from the broker and then run against an running service.
Since this already uses a @RunWith annotation, I could not use the spring boot runner. So to get around that as a before class step, I start the Spring boot application, the pacts then gets run against that running instance… and the boot application gets stopped again after the tests. Depending on your use case I guess it would also be an option to do this with @Before so you get a new service instance started before each pack, but that would slow down the execution tremendously.
The @State annotation, allows for clients to define a specific state, which the producer can the use to setup additional data / conditions required for the test to run.
Once the pacts have executed against the service there are reports generated in the target folder.
@RunWith(PactRunner.class) @Provider("test_provider" ) @PactBroker(host = "localhost", port = "80") @VerificationReports({"console", "markdown"}) public class TestPacts { private static ConfigurableApplicationContext application; @TestTarget public final Target target = new HttpTarget(8080); @BeforeClass public static void startSpring(){ application = SpringApplication.run(ProviderServiceApplication.class); } @State("default") public void toDefaultState() { System.out.println("Now service in default state"); } @State("extra") public void toExtraState() { System.out.println("Now service in extra state"); } @AfterClass public static void kill(){ application.stop(); } }
Setting up the Pact Broker
1. Grab the public images from Docker Hub.
docker pull dius/pact_broker docker pull postgres
2. Then setup the Postgres DB
docker run --name pactbroker-db -e POSTGRES_PASSWORD=ThePostgresPassword -e POSTGRES_USER=admin -d postgres docker run -it --link pactbroker-db:postgres --rm postgres psql -h postgres -U admin CREATE USER pactbrokeruser WITH PASSWORD 'TheUserPassword'; CREATE DATABASE pactbroker WITH OWNER pactbrokeruser; GRANT ALL PRIVILEGES ON DATABASE pactbroker TO pactbrokeruser;
3. Once the DB is up, run the actual Broker:
docker run --name pactbroker --link pactbroker-db:postgres -e PACT_BROKER_DATABASE_USERNAME=pactbrokeruser -e PACT_BROKER_DATABASE_PASSWORD=TheUserPassword -e PACT_BROKER_DATABASE_HOST=postgres -e PACT_BROKER_DATABASE_NAME=pactbroker -d -p 80:80 dius/pact_broker
Extra References:
- https://docs.pact.io/documentation/
- https://docs.pact.io/documentation/sharings_pacts.html
- https://github.com/DiUS/pact-jvm
- https://github.com/DiUS/pact-jvm/tree/master/pact-jvm-consumer-junit
Get the example project
Reference: | Consumer Driven Testing with Pact & Spring Boot from our JCG partner Brian Du Preez at the Zen in the art of IT blog. |
Hi
I have a query that for doing the PACT Consumer driven contract testing we have to STUB the API in our local machine and then test it