|
| 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). |
0 commit comments