Skip to content

Commit ce91dd9

Browse files
committed
add Pact
1 parent fe0ae58 commit ce91dd9

6 files changed

Lines changed: 222 additions & 0 deletions

File tree

spring-boot-example/pom.xml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
<jfrunit.version>1.0.0.Alpha2</jfrunit.version>
4646
<jmh-core.version>1.37</jmh-core.version>
4747
<pitest.version>1.18.0</pitest.version>
48+
<pact.version>4.6.17</pact.version>
4849
</properties>
4950

5051
<dependencyManagement>
@@ -84,6 +85,10 @@
8485
<groupId>org.springframework.boot</groupId>
8586
<artifactId>spring-boot-starter-web</artifactId>
8687
</dependency>
88+
<dependency>
89+
<groupId>org.springframework.boot</groupId>
90+
<artifactId>spring-boot-starter-webflux</artifactId>
91+
</dependency>
8792
<dependency>
8893
<groupId>org.springframework.boot</groupId>
8994
<artifactId>spring-boot-starter-thymeleaf</artifactId>
@@ -284,6 +289,18 @@
284289
<artifactId>archunit-junit5</artifactId>
285290
<version>${archunit.version}</version>
286291
</dependency>
292+
<dependency>
293+
<groupId>au.com.dius.pact.consumer</groupId>
294+
<artifactId>junit5</artifactId>
295+
<version>${pact.version}</version>
296+
<scope>test</scope>
297+
</dependency>
298+
<dependency>
299+
<groupId>au.com.dius.pact.provider</groupId>
300+
<artifactId>junit5</artifactId>
301+
<version>${pact.version}</version>
302+
<scope>test</scope>
303+
</dependency>
287304

288305
<!-- Performance Testing -->
289306
<dependency>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package de.rieckpil.blog.portfolio;
2+
3+
4+
import org.springframework.web.reactive.function.client.WebClient;
5+
6+
public class StockApiClient {
7+
private final WebClient webClient;
8+
9+
public StockApiClient(WebClient webClient) {
10+
this.webClient = webClient;
11+
}
12+
13+
public StockPrice getStockPrice(String symbol) {
14+
return webClient.get()
15+
.uri("/api/stocks/{symbol}/price", symbol)
16+
.retrieve()
17+
.bodyToMono(StockPrice.class)
18+
.block();
19+
}
20+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package de.rieckpil.blog.portfolio;
2+
3+
import com.fasterxml.jackson.databind.ObjectMapper;
4+
import com.fasterxml.jackson.databind.node.ObjectNode;
5+
import org.springframework.web.bind.annotation.GetMapping;
6+
import org.springframework.web.bind.annotation.PathVariable;
7+
import org.springframework.web.bind.annotation.RequestMapping;
8+
import org.springframework.web.bind.annotation.RestController;
9+
10+
@RestController
11+
@RequestMapping("/api/stocks")
12+
public class StockController {
13+
14+
private final ObjectMapper objectMapper;
15+
16+
public StockController(ObjectMapper objectMapper) {
17+
this.objectMapper = objectMapper;
18+
}
19+
20+
// Dummy implementation that would reside in a separate project/application
21+
@GetMapping
22+
@RequestMapping("/{SYMBOL}/price")
23+
public ObjectNode getStockPrice(@PathVariable String symbol) {
24+
return objectMapper.createObjectNode()
25+
.put("symbol", symbol)
26+
.put("price", 42.42)
27+
.put("currency", "USD")
28+
.put("timestamp", "2020-01-01T12:00:00Z");
29+
}
30+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package de.rieckpil.blog.portfolio;
2+
3+
import java.math.BigDecimal;
4+
import java.time.Instant;
5+
6+
public class StockPrice {
7+
private String symbol;
8+
private BigDecimal price;
9+
private String currency;
10+
private Instant timestamp;
11+
12+
public String getSymbol() {
13+
return symbol;
14+
}
15+
16+
public void setSymbol(String symbol) {
17+
this.symbol = symbol;
18+
}
19+
20+
public BigDecimal getPrice() {
21+
return price;
22+
}
23+
24+
public void setPrice(BigDecimal price) {
25+
this.price = price;
26+
}
27+
28+
public String getCurrency() {
29+
return currency;
30+
}
31+
32+
public void setCurrency(String currency) {
33+
this.currency = currency;
34+
}
35+
36+
public Instant getTimestamp() {
37+
return timestamp;
38+
}
39+
40+
public void setTimestamp(Instant timestamp) {
41+
this.timestamp = timestamp;
42+
}
43+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package de.rieckpil.blog.pact;
2+
3+
import java.math.BigDecimal;
4+
5+
import au.com.dius.pact.consumer.MockServer;
6+
import au.com.dius.pact.consumer.dsl.PactDslJsonBody;
7+
import au.com.dius.pact.consumer.dsl.PactDslWithProvider;
8+
import au.com.dius.pact.consumer.junit5.PactConsumerTestExt;
9+
import au.com.dius.pact.consumer.junit5.PactTestFor;
10+
import au.com.dius.pact.core.model.V4Pact;
11+
import au.com.dius.pact.core.model.annotations.Pact;
12+
import de.rieckpil.blog.portfolio.StockApiClient;
13+
import de.rieckpil.blog.portfolio.StockPrice;
14+
import org.junit.jupiter.api.Test;
15+
import org.junit.jupiter.api.extension.ExtendWith;
16+
import org.springframework.web.reactive.function.client.WebClient;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
20+
@ExtendWith(PactConsumerTestExt.class)
21+
@PactTestFor(providerName = "stock-api")
22+
class StockApiContractTest {
23+
24+
@Pact(consumer = "portfolio-dashboard")
25+
public V4Pact createPact(PactDslWithProvider builder) {
26+
// Define what we expect from the Stock API
27+
return builder
28+
.given("Stock AAPL exists and market is open")
29+
.uponReceiving("A request for AAPL stock price")
30+
.path("/api/stocks/AAPL/price")
31+
.method("GET")
32+
.willRespondWith()
33+
.status(200)
34+
.body(new PactDslJsonBody()
35+
.stringType("symbol", "AAPL")
36+
.decimalType("price", 150.25)
37+
.stringType("currency", "USD")
38+
.datetime("timestamp", "yyyy-MM-dd'T'HH:mm:ss'Z'"))
39+
.toPact(V4Pact.class);
40+
}
41+
42+
@Test
43+
@PactTestFor(pactMethod = "createPact")
44+
void testGetStockPrice(MockServer mockServer) {
45+
// Create client pointing to mock server
46+
StockApiClient client = new StockApiClient(WebClient.builder().baseUrl(mockServer.getUrl()).build());
47+
48+
// Test the interaction
49+
StockPrice price = client.getStockPrice("AAPL");
50+
51+
assertThat(price)
52+
.isNotNull()
53+
.satisfies(p -> {
54+
assertThat(p.getSymbol()).isEqualTo("AAPL");
55+
assertThat(p.getPrice()).isGreaterThan(BigDecimal.ZERO);
56+
assertThat(p.getCurrency()).isEqualTo("USD");
57+
});
58+
}
59+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package au.com.dius.pactworkshop.provider;
2+
3+
import au.com.dius.pact.provider.junit5.HttpTestTarget;
4+
import au.com.dius.pact.provider.junit5.PactVerificationContext;
5+
import au.com.dius.pact.provider.junit5.PactVerificationInvocationContextProvider;
6+
import au.com.dius.pact.provider.junitsupport.Provider;
7+
import au.com.dius.pact.provider.junitsupport.State;
8+
import au.com.dius.pact.provider.junitsupport.loader.PactBroker;
9+
import au.com.dius.pact.provider.junitsupport.loader.PactBrokerConsumerVersionSelectors;
10+
import au.com.dius.pact.provider.junitsupport.loader.SelectorBuilder;
11+
import org.apache.hc.core5.http.HttpRequest;
12+
import org.junit.jupiter.api.BeforeEach;
13+
import org.junit.jupiter.api.TestTemplate;
14+
import org.junit.jupiter.api.extension.ExtendWith;
15+
import org.springframework.boot.test.context.SpringBootTest;
16+
import org.springframework.boot.test.web.server.LocalServerPort;
17+
18+
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
19+
20+
@Provider("stock-api")
21+
@PactBroker
22+
@SpringBootTest(webEnvironment = RANDOM_PORT)
23+
public class StockApiProviderTest {
24+
25+
@PactBrokerConsumerVersionSelectors
26+
public static SelectorBuilder consumerVersionSelectors() {
27+
// Select Pacts for consumers deployed or released to production, those on the main branch
28+
// and those on a named branch step11, for use in our workshop
29+
return new SelectorBuilder()
30+
.deployedOrReleased()
31+
.mainBranch()
32+
.branch("step11");
33+
}
34+
35+
@LocalServerPort
36+
int port;
37+
38+
@BeforeEach
39+
void setUp(PactVerificationContext context) {
40+
context.setTarget(new HttpTestTarget("localhost", port));
41+
}
42+
43+
@TestTemplate
44+
@ExtendWith(PactVerificationInvocationContextProvider.class)
45+
void verifyPact(PactVerificationContext context, HttpRequest request) {
46+
context.verifyInteraction();
47+
}
48+
49+
@State("Stock data exists for AAPL")
50+
void toAppleStockExists() {
51+
// Prepare provider data for the test, e.g., inserting stock data into a mock database
52+
}
53+
}

0 commit comments

Comments
 (0)