Skip to content

Commit 8bdcb43

Browse files
fcannizzohzJackPGreenJamesHazelcast
authored
Added new module with samples of the Hazelcast test support. (#735)
--------- Co-authored-by: Jack Green <[email protected]> Co-authored-by: James Holgate <[email protected]>
1 parent fe16e33 commit 8bdcb43

37 files changed

+2581
-1
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ Included sample folders
3939
* **/spi** — Includes code samples that create a simple counter application using Hazelcast’s Service Provider Interface (SPI).
4040
* **/sql** — Examples of using SQL directly, or via JDBC.
4141
* **/transactions** — Code samples showing how to use the TransactionalMap and TransactionalTask interfaces.
42+
* **/testing** — Code samples showing how to use Hazelcast test support classes for unit/system/integration testing Hazelcast powered applications.
4243

4344
Included helper folders
4445
-----------------------

pom.xml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@
8888
<module>generic-record</module>
8989
<module>sql</module>
9090
<module>ai</module>
91+
<module>testing</module>
9192
</modules>
9293

9394
<build>
@@ -230,6 +231,13 @@
230231
<scope>import</scope>
231232
<type>pom</type>
232233
</dependency>
234+
<dependency>
235+
<groupId>org.mockito</groupId>
236+
<artifactId>mockito-bom</artifactId>
237+
<version>5.20.0</version>
238+
<scope>import</scope>
239+
<type>pom</type>
240+
</dependency>
233241
</dependencies>
234242
</dependencyManagement>
235243

@@ -262,7 +270,6 @@
262270
<dependency>
263271
<groupId>org.mockito</groupId>
264272
<artifactId>mockito-core</artifactId>
265-
<version>5.8.0</version>
266273
</dependency>
267274
<dependency>
268275
<groupId>org.assertj</groupId>

testing/README.md

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
# Testing samples
2+
3+
Testing applications that use Hazelcast (for caching, co-located compute and streaming) requires care to validate the behavior at
4+
various levels - from
5+
unit to system tests - given Hazelcast’s distributed, eventually/strongly consistent and asynchronous behavior.
6+
7+
Hazelcast provides tools to simplify writing unit/component/integration tests of such applications.
8+
9+
This project demonstrates the use of these tools. Full documentation to setup and configure your dependencies is available in
10+
the [Hazelcast official documentation](https://docs.hazelcast.com/hazelcast/latest/test/testing-apps).
11+
12+
## Testing complex test scenarios
13+
14+
The sample code in this project illustrates how to test applications that use Hazelcast for caching and stream processing. It
15+
includes two services: `Order` and `Customer` and it demonstrates testing each service independently as well as together.
16+
17+
Tests are available using both JUnit4 and JUnit5. The `com.hazelcast.samples.testing.samples` package contains basic testing support
18+
API usage examples.
19+
20+
### Testing the integration of two services
21+
22+
In the following example, two services (`Customer` and `Order`) share state via Hazelcast. Functionality can be tested as
23+
follows:
24+
25+
```java
26+
@Test
27+
public void testCustomerAndOrderServicesIntegration() {
28+
// Create a shared Hazelcast instance
29+
instance = createHazelcastInstance();
30+
31+
// Instantiate both services using same cluster
32+
CustomerService customerService = new HzCustomerService(instance);
33+
OrderService orderService = new HzOrderService(instance);
34+
35+
// Add customer
36+
Customer alice = new Customer("c1", "Alice");
37+
customerService.save(alice);
38+
39+
// Place an order for Alice
40+
Order order = new Order("o1", "c1", "Laptop");
41+
orderService.placeOrder(order);
42+
43+
// Verify state across services
44+
assertEquals("Alice", customerService.findCustomer("c1").name());
45+
assertEquals("Laptop", orderService.getOrder("o1").product());
46+
}
47+
```
48+
49+
### Testing a component integration with its dependencies
50+
51+
Another typical scenario consists of testing the integration of a component, in isolation, but integrated with its dependencies:
52+
53+
```java
54+
@Test
55+
public void customerServiceWithMapStoreInteractions()
56+
throws Exception {
57+
// Set up H2 in-memory DB
58+
Connection conn = DriverManager.getConnection("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1");
59+
conn.createStatement().execute("CREATE TABLE customers (id VARCHAR PRIMARY KEY, name VARCHAR)");
60+
61+
// Set up Hazelcast config with MapStore
62+
Config config = new Config();
63+
config.getMapConfig("customers").getMapStoreConfig().setEnabled(true).setImplementation(new CustomerMapStore(conn));
64+
65+
hz = createHazelcastInstance(config);
66+
CustomerService service = new HzCustomerService(hz);
67+
68+
// Act
69+
service.save(new Customer("c1", "Alice")); // should go into both IMap and DB
70+
Customer fromMap = service.findCustomer("c1"); // should be from IMap
71+
72+
// Clear IMap to test reloading from DB
73+
hz.getMap("customers").evictAll();
74+
Customer fromStore = service.findCustomer("c1"); // should be reloaded from H2
75+
76+
// Assert
77+
assertEquals("Alice", fromMap.name());
78+
assertEquals("Alice", fromStore.name());
79+
}
80+
81+
```
82+
83+
### Testing integrated behaviour
84+
85+
`HazelcastTestSupport` supports testing of the application using the Hazelcast capabilities, for example, in this case, the
86+
execution of a listener:
87+
88+
```java
89+
@Test
90+
public void testOrderServiceListener() throws Exception {
91+
instance = createHazelcastInstance();
92+
// set a customer
93+
instance.getMap("customers").put("c1", new Customer("c1", "Alice"));
94+
95+
OrderService sut = new HzOrderService(instance, mockConsumer);
96+
97+
Order order = new Order("o1", "c1", "Laptop");
98+
sut.placeOrder(order);
99+
// Update the order so hazelcast triggers the event
100+
sut.updateOrder(order.confirm());
101+
102+
// Verify that only mockConsumer#accept(Order) has been invoked, within 100ms
103+
verify(mockConsumer, timeout(100).only()).accept(any(Order.class));
104+
}
105+
```
106+
107+
The test class this method is extracted from is [OrderServiceWithListenerTest](src/test/java/com/hazelcast/samples/testing/junit4/OrderServiceWithListenerTest.java).
108+
109+
### Testing streaming applications
110+
111+
Test streaming applications is also supported - this is done extending `JetTestSupport` (itself an extension
112+
of `HazelcastTestSupport`). The [Hazelcast docs](https://docs.hazelcast.com/hazelcast/latest/test/testing-streaming) provide
113+
further details.
114+
115+
To use `JetTestSupport` the following dependencies must be included:
116+
117+
```xml
118+
<dependency>
119+
<groupId>org.apache.logging.log4j</groupId>
120+
<artifactId>log4j-core</artifactId>
121+
<version>{log4j.version}</version> <!-- or whatever latest you want -->
122+
</dependency>
123+
<dependency>
124+
<groupId>org.apache.logging.log4j</groupId>
125+
<artifactId>log4j-api</artifactId>
126+
<version>{log4j.version}</version>
127+
</dependency>
128+
```
129+
130+
With `JetTestSupport`, utility methods available and distributed jobs can be tested as follows:
131+
132+
```java
133+
@Test
134+
public void testJetOrderEnrichmentWithHazelcastState() {
135+
HazelcastInstance instance = createHazelcastInstance();
136+
137+
JetService jet = instance.getJet();
138+
139+
IMap<String, Customer> customerMap = instance.getMap("customers");
140+
customerMap.put("c1", new Customer("c1", "Alice"));
141+
customerMap.put("c2", new Customer("c2", "Bob"));
142+
143+
BatchSource<Order> source = TestSources.items(
144+
new Order("o1", "c1", "Laptop"),
145+
new Order("o2", "c2", "Phone")
146+
);
147+
Job job = jet.newJob(OrderEnrichmentPipeline.build(source));
148+
job.join(); // wait for completion
149+
150+
IList<EnrichedOrder> result = instance.getList("enriched-orders");
151+
152+
assertEquals(2, result.size());
153+
assertTrue(result.stream().anyMatch(o -> o.customerName().equals("Alice")));
154+
assertTrue(result.stream().anyMatch(o -> o.customerName().equals("Bob")));
155+
}
156+
```
157+
158+
The approach shown in the previous example doesn't work in cases where streams are never ending.
159+
In this case, an approach based on a set of `assert*` utilities that can be injected in the pipeline is preferable.
160+
161+
For example, here we inject a data source and assert on the number of received items in the output stream.
162+
163+
The full test class is [OrderEnrichmentPipelineTest](src/test/java/com/hazelcast/samples/testing/junit5/OrderEnrichmentPipelineTest.java).
164+
165+
```java
166+
@Test
167+
public void testStreamingEnrichmentWithInlineAssertion() {
168+
IMap<String, Customer> customerMap = instance.getMap("customers");
169+
customerMap.put("c1", new Customer("c1", "Alice"));
170+
customerMap.put("c2", new Customer("c2", "Bob"));
171+
172+
// Streaming source
173+
StreamSource<Order> source = TestSources.itemStream(50, (ts, seq) -> {
174+
String customerId = seq % 2 == 0 ? "c1" : "c2";
175+
return new Order("o" + seq, customerId, "Product" + seq);
176+
});
177+
178+
Pipeline pipeline = Pipeline.create();
179+
OrderEnrichmentPipeline.enrich(pipeline, source)
180+
.apply(Assertions.assertCollectedEventually(5,
181+
list -> assertTrue("Expected at least 10 enriched orders", list.size() >= 10)));
182+
183+
Job job = instance.getJet().newJob(pipeline);
184+
185+
// The assertion will stop the job automatically via AssertionCompletedException by assertCollectedEventually
186+
try {
187+
job.join();
188+
fail("Expected job to terminate with AssertionCompletedException");
189+
} catch (CompletionException e) {
190+
if (!causedBy(e, AssertionCompletedException.class)) {
191+
throw e; // rethrow if it wasn't the expected assertion exit
192+
}
193+
}
194+
}
195+
```
196+
197+
More assertions are available and [documented](https://docs.hazelcast.com/hazelcast/latest/test/testing#assertions).

testing/pom.xml

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xmlns="http://maven.apache.org/POM/4.0.0"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
<parent>
7+
<groupId>com.hazelcast.samples</groupId>
8+
<artifactId>code-samples</artifactId>
9+
<version>0.1-SNAPSHOT</version>
10+
</parent>
11+
12+
<artifactId>testing</artifactId>
13+
<name>Test Samples</name>
14+
15+
<properties>
16+
<!-- needed for checkstyle/findbugs -->
17+
<main.basedir>${project.parent.basedir}</main.basedir>
18+
</properties>
19+
20+
<dependencyManagement>
21+
<dependencies>
22+
<dependency>
23+
<groupId>org.apache.logging.log4j</groupId>
24+
<artifactId>log4j-bom</artifactId>
25+
<version>2.25.2</version>
26+
<type>pom</type>
27+
<scope>import</scope>
28+
</dependency>
29+
</dependencies>
30+
</dependencyManagement>
31+
32+
<dependencies>
33+
<dependency>
34+
<groupId>com.hazelcast</groupId>
35+
<artifactId>hazelcast</artifactId>
36+
<version>${hazelcast.version}</version>
37+
</dependency>
38+
<dependency>
39+
<groupId>com.hazelcast</groupId>
40+
<artifactId>hazelcast</artifactId>
41+
<version>${hazelcast.version}</version>
42+
<classifier>tests</classifier>
43+
</dependency>
44+
<dependency>
45+
<groupId>org.apache.logging.log4j</groupId>
46+
<artifactId>log4j-core</artifactId>
47+
</dependency>
48+
<dependency>
49+
<groupId>org.apache.logging.log4j</groupId>
50+
<artifactId>log4j-api</artifactId>
51+
</dependency>
52+
<dependency>
53+
<groupId>org.mockito</groupId>
54+
<artifactId>mockito-junit-jupiter</artifactId>
55+
<scope>test</scope>
56+
</dependency>
57+
<dependency>
58+
<groupId>org.opentest4j</groupId>
59+
<artifactId>opentest4j</artifactId>
60+
<version>1.3.0</version>
61+
<scope>test</scope>
62+
</dependency>
63+
<dependency>
64+
<groupId>com.h2database</groupId>
65+
<artifactId>h2</artifactId>
66+
<version>2.3.232</version>
67+
<scope>test</scope>
68+
</dependency>
69+
</dependencies>
70+
</project>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.hazelcast.samples.testing;
2+
3+
import java.io.Serializable;
4+
5+
/**
6+
* Customer domain object.
7+
*
8+
* <p>Immutable and serializable so it can be stored in Hazelcast
9+
* data structures. Both {@code id} and {@code name} are required.
10+
*
11+
* @param id unique customer identifier
12+
* @param name customer name
13+
*/
14+
public record Customer(String id, String name)
15+
implements Serializable {
16+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.hazelcast.samples.testing;
2+
3+
/**
4+
* Service interface for working with {@link Customer} objects.
5+
*
6+
* <p>Intended to demonstrate persisting and retrieving state
7+
* through Hazelcast.
8+
*/
9+
public interface CustomerService {
10+
11+
/**
12+
* Look up a customer by identifier.
13+
*
14+
* @param number customer identifier
15+
* @return the matching customer, or {@code null} if not found
16+
*/
17+
Customer findCustomer(String number);
18+
19+
/**
20+
* Persist a customer.
21+
*
22+
* @param customer customer instance to store
23+
*/
24+
void save(Customer customer);
25+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.hazelcast.samples.testing;
2+
3+
import java.io.Serializable;
4+
5+
/**
6+
* Business view of an order including customer details.
7+
*
8+
* <p>Used to represent an order after enrichment, for example
9+
* joining order data with customer information inside Hazelcast.
10+
*
11+
* @param orderId unique order identifier
12+
* @param customerName name of the customer placing the order
13+
* @param product product being ordered
14+
*/
15+
public record EnrichedOrder(String orderId, String customerName, String product)
16+
implements Serializable {
17+
}

0 commit comments

Comments
 (0)