From 82efd0def224df5ee3b82ac8b1e919221c931008 Mon Sep 17 00:00:00 2001 From: Maxime Petit Date: Mon, 20 May 2024 00:13:56 +0200 Subject: [PATCH 1/5] build: add mockito and spring boot starter test dependencies --- pom.xml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pom.xml b/pom.xml index e28858a..cd07a4c 100644 --- a/pom.xml +++ b/pom.xml @@ -26,6 +26,7 @@ 21 + 4.11.0 @@ -58,6 +59,18 @@ spring-kafka-test test + + org.springframework.boot + spring-boot-starter-test + test + + + org.mockito + mockito-core + ${mockito.version} + test + + From 9794d792cab49e0a472d138b28261300dcbc2279 Mon Sep 17 00:00:00 2001 From: Maxime Petit Date: Mon, 20 May 2024 00:14:19 +0200 Subject: [PATCH 2/5] test: add unit tests for DataEventBroadcaster and DataEventListener --- .../broadcaster/DataEventBroadcasterTest.java | 99 ++++++++++++ .../event/listener/DataEventListenerTest.java | 151 ++++++++++++++++++ 2 files changed, 250 insertions(+) create mode 100644 src/test/java/com/sipios/spring/data/event/broadcaster/DataEventBroadcasterTest.java create mode 100644 src/test/java/com/sipios/spring/data/event/listener/DataEventListenerTest.java diff --git a/src/test/java/com/sipios/spring/data/event/broadcaster/DataEventBroadcasterTest.java b/src/test/java/com/sipios/spring/data/event/broadcaster/DataEventBroadcasterTest.java new file mode 100644 index 0000000..87031ab --- /dev/null +++ b/src/test/java/com/sipios/spring/data/event/broadcaster/DataEventBroadcasterTest.java @@ -0,0 +1,99 @@ +package com.sipios.spring.data.event.broadcaster; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.*; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.CallbackException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.kafka.core.KafkaTemplate; + +public class DataEventBroadcasterTest { + private DataEventBroadcaster broadcaster; + private KafkaTemplate kafkaTemplate; + private ObjectMapper objectMapper; + + @BeforeEach + void beforeEach() { + kafkaTemplate = mock(KafkaTemplate.class); + objectMapper = new ObjectMapper(); + broadcaster = new DataEventBroadcaster(kafkaTemplate, objectMapper); + } + + @Test + void testBroadcastEntityCreated() throws Exception { + TestEntity entity = new TestEntity(1, "Test Name", true); + String expectedJson = objectMapper.writeValueAsString(entity); + + broadcaster.broadcastEntityCreated(entity, "testEntity.created"); + + verify(kafkaTemplate).send(eq("testEntity.created"), eq(expectedJson)); + } + + @Test + void testBroadcastEntityUpdated() throws Exception { + TestEntity entity = new TestEntity(1, "Test Name", false); + String expectedJson = objectMapper.writeValueAsString(entity); + + broadcaster.broadcastEntityUpdated(entity, "testEntity.updated"); + + verify(kafkaTemplate).send(eq("testEntity.updated"), eq(expectedJson)); + } + + @Test + void testBroadcastEntityDeleted() throws Exception { + TestEntity entity = new TestEntity(1, "Test Name", true); + String expectedJson = objectMapper.writeValueAsString(entity); + + broadcaster.broadcastEntityDeleted(entity, "testEntity.deleted"); + + verify(kafkaTemplate).send(eq("testEntity.deleted"), eq(expectedJson)); + } + + @Test + void testBroadcastEntityCreatedJsonProcessingException() throws Exception { + objectMapper = mock(ObjectMapper.class); + broadcaster = new DataEventBroadcaster(kafkaTemplate, objectMapper); + TestEntity entity = new TestEntity(1, "Test Name", true); + + when(objectMapper.writeValueAsString(entity)).thenThrow(new JsonProcessingException("JSON processing error") {}); + + assertThrows(CallbackException.class, () -> broadcaster.broadcastEntityCreated(entity, "testEntity.created")); + } + + @Test + void testBroadcastEntityUpdatedJsonProcessingException() throws Exception { + objectMapper = mock(ObjectMapper.class); + broadcaster = new DataEventBroadcaster(kafkaTemplate, objectMapper); + TestEntity entity = new TestEntity(1, "Test Name", false); + + when(objectMapper.writeValueAsString(entity)).thenThrow(new JsonProcessingException("JSON processing error") {}); + + assertThrows(CallbackException.class, () -> broadcaster.broadcastEntityUpdated(entity, "testEntity.updated")); + } + + @Test + void testBroadcastEntityDeletedJsonProcessingException() throws Exception { + objectMapper = mock(ObjectMapper.class); + broadcaster = new DataEventBroadcaster(kafkaTemplate, objectMapper); + TestEntity entity = new TestEntity(1, "Test Name", true); + + when(objectMapper.writeValueAsString(entity)).thenThrow(new JsonProcessingException("JSON processing error") {}); + + assertThrows(CallbackException.class, () -> broadcaster.broadcastEntityDeleted(entity, "testEntity.deleted")); + } + + @Getter + @Setter + @AllArgsConstructor + public static class TestEntity { + private int id; + private String name; + private boolean active; + } +} diff --git a/src/test/java/com/sipios/spring/data/event/listener/DataEventListenerTest.java b/src/test/java/com/sipios/spring/data/event/listener/DataEventListenerTest.java new file mode 100644 index 0000000..e61c6de --- /dev/null +++ b/src/test/java/com/sipios/spring/data/event/listener/DataEventListenerTest.java @@ -0,0 +1,151 @@ +package com.sipios.spring.data.event.listener; + +import com.sipios.spring.data.event.annotation.DataEventEntity; +import com.sipios.spring.data.event.broadcaster.DataEventBroadcaster; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.event.spi.PostDeleteEvent; +import org.hibernate.event.spi.PostInsertEvent; +import org.hibernate.event.spi.PostUpdateEvent; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +public class DataEventListenerTest { + @Mock + private DataEventBroadcaster dataEventBroadcaster; + + @InjectMocks + private DataEventListener listener; + + @Nested + class AnnotatedEntityTests { + + @Test + public void testOnPostInsert() { + PostInsertEvent event = mock(PostInsertEvent.class); + TestEntity entity = new TestEntity(1, "Test Name", true); + when(event.getEntity()).thenReturn(entity); + + listener.onPostInsert(event); + + verify(dataEventBroadcaster).broadcastEntityCreated(entity, "testEntity.created"); + } + + @Test + public void testOnPostUpdate() { + PostUpdateEvent event = mock(PostUpdateEvent.class); + TestEntity entity = new TestEntity(1, "Test Name", true); + when(event.getEntity()).thenReturn(entity); + + listener.onPostUpdate(event); + + verify(dataEventBroadcaster).broadcastEntityUpdated(entity, "testEntity.updated"); + } + + @Test + public void testOnPostDelete() { + PostDeleteEvent event = mock(PostDeleteEvent.class); + TestEntity entity = new TestEntity(1, "Test Name", true); + when(event.getEntity()).thenReturn(entity); + + listener.onPostDelete(event); + + verify(dataEventBroadcaster).broadcastEntityDeleted(entity, "testEntity.deleted"); + } + + @Test + public void testOnPostInsertJsonProcessingException() throws Exception { + PostInsertEvent event = mock(PostInsertEvent.class); + TestEntity entity = new TestEntity(1, "Test Name", true); + when(event.getEntity()).thenReturn(entity); + doThrow(new RuntimeException("JSON processing error")).when(dataEventBroadcaster).broadcastEntityCreated(any(), any()); + + assertThrows(RuntimeException.class, () -> listener.onPostInsert(event)); + } + + @Test + public void testOnPostUpdateJsonProcessingException() throws Exception { + PostUpdateEvent event = mock(PostUpdateEvent.class); + TestEntity entity = new TestEntity(1, "Test Name", true); + when(event.getEntity()).thenReturn(entity); + doThrow(new RuntimeException("JSON processing error")).when(dataEventBroadcaster).broadcastEntityUpdated(any(), any()); + + assertThrows(RuntimeException.class, () -> listener.onPostUpdate(event)); + } + + @Test + public void testOnPostDeleteJsonProcessingException() throws Exception { + PostDeleteEvent event = mock(PostDeleteEvent.class); + TestEntity entity = new TestEntity(1, "Test Name", true); + when(event.getEntity()).thenReturn(entity); + doThrow(new RuntimeException("JSON processing error")).when(dataEventBroadcaster).broadcastEntityDeleted(any(), any()); + + assertThrows(RuntimeException.class, () -> listener.onPostDelete(event)); + } + + @DataEventEntity(creationTopic = "testEntity.created", updateTopic = "testEntity.updated", deletionTopic = "testEntity.deleted") + @Getter + @Setter + @AllArgsConstructor + private static class TestEntity { + private int id; + private String name; + private boolean active; + } + } + + @Nested + class NonAnnotatedEntityTests { + + @Test + public void testOnPostInsert() { + PostInsertEvent event = mock(PostInsertEvent.class); + NonAnnotatedEntity entity = new NonAnnotatedEntity(1, "Test Name", true); + when(event.getEntity()).thenReturn(entity); + + listener.onPostInsert(event); + + verify(dataEventBroadcaster, never()).broadcastEntityCreated(any(), any()); + } + + @Test + public void testOnPostUpdate() { + PostUpdateEvent event = mock(PostUpdateEvent.class); + NonAnnotatedEntity entity = new NonAnnotatedEntity(1, "Test Name", true); + when(event.getEntity()).thenReturn(entity); + + listener.onPostUpdate(event); + + verify(dataEventBroadcaster, never()).broadcastEntityUpdated(any(), any()); + } + + @Test + public void testOnPostDelete() { + PostDeleteEvent event = mock(PostDeleteEvent.class); + NonAnnotatedEntity entity = new NonAnnotatedEntity(1, "Test Name", true); + when(event.getEntity()).thenReturn(entity); + + listener.onPostDelete(event); + + verify(dataEventBroadcaster, never()).broadcastEntityDeleted(any(), any()); + } + + @Getter + @Setter + @AllArgsConstructor + private static class NonAnnotatedEntity { + private int id; + private String name; + private boolean active; + } + } +} From 434329b0b653607513175a4beef6746667691913 Mon Sep 17 00:00:00 2001 From: Maxime Petit Date: Mon, 20 May 2024 11:25:14 +0200 Subject: [PATCH 3/5] test(DataEventBroadcasterTest): increase test coverage for empty topic labels --- .../broadcaster/DataEventBroadcasterTest.java | 38 +++++++++++++------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/src/test/java/com/sipios/spring/data/event/broadcaster/DataEventBroadcasterTest.java b/src/test/java/com/sipios/spring/data/event/broadcaster/DataEventBroadcasterTest.java index 87031ab..c43235a 100644 --- a/src/test/java/com/sipios/spring/data/event/broadcaster/DataEventBroadcasterTest.java +++ b/src/test/java/com/sipios/spring/data/event/broadcaster/DataEventBroadcasterTest.java @@ -11,6 +11,8 @@ import org.hibernate.CallbackException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import org.springframework.kafka.core.KafkaTemplate; public class DataEventBroadcasterTest { @@ -25,34 +27,46 @@ void beforeEach() { broadcaster = new DataEventBroadcaster(kafkaTemplate, objectMapper); } - @Test - void testBroadcastEntityCreated() throws Exception { + @ParameterizedTest + @CsvSource({ + "testEntity.created, testEntity.created", + "'', testentity.created" + }) + void testBroadcastEntityCreated(String topicLabel, String expectedTopic) throws Exception { TestEntity entity = new TestEntity(1, "Test Name", true); String expectedJson = objectMapper.writeValueAsString(entity); - broadcaster.broadcastEntityCreated(entity, "testEntity.created"); + broadcaster.broadcastEntityCreated(entity, topicLabel); - verify(kafkaTemplate).send(eq("testEntity.created"), eq(expectedJson)); + verify(kafkaTemplate).send(eq(expectedTopic), eq(expectedJson)); } - @Test - void testBroadcastEntityUpdated() throws Exception { + @ParameterizedTest + @CsvSource({ + "testEntity.updated, testEntity.updated", + "'', testentity.updated" + }) + void testBroadcastEntityUpdated(String topicLabel, String expectedTopic) throws Exception { TestEntity entity = new TestEntity(1, "Test Name", false); String expectedJson = objectMapper.writeValueAsString(entity); - broadcaster.broadcastEntityUpdated(entity, "testEntity.updated"); + broadcaster.broadcastEntityUpdated(entity, topicLabel); - verify(kafkaTemplate).send(eq("testEntity.updated"), eq(expectedJson)); + verify(kafkaTemplate).send(eq(expectedTopic), eq(expectedJson)); } - @Test - void testBroadcastEntityDeleted() throws Exception { + @ParameterizedTest + @CsvSource({ + "testEntity.deleted, testEntity.deleted", + "'', testentity.deleted" + }) + void testBroadcastEntityDeleted(String topicLabel, String expectedTopic) throws Exception { TestEntity entity = new TestEntity(1, "Test Name", true); String expectedJson = objectMapper.writeValueAsString(entity); - broadcaster.broadcastEntityDeleted(entity, "testEntity.deleted"); + broadcaster.broadcastEntityDeleted(entity, topicLabel); - verify(kafkaTemplate).send(eq("testEntity.deleted"), eq(expectedJson)); + verify(kafkaTemplate).send(eq(expectedTopic), eq(expectedJson)); } @Test From f40b0cb17c2d5502f94b7877b11024122aa441c4 Mon Sep 17 00:00:00 2001 From: Maxime Petit Date: Mon, 20 May 2024 12:26:06 +0200 Subject: [PATCH 4/5] build: add JaCoCo code coverage plugin --- pom.xml | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index cd07a4c..bf36dc4 100644 --- a/pom.xml +++ b/pom.xml @@ -135,9 +135,27 @@ false + + org.jacoco + jacoco-maven-plugin + 0.8.12 + + + + prepare-agent + + + + report + test + + report + + + + - scm:git:https://github.com/sipios/spring-data-event.git scm:git:git@github.com:sipios/spring-data-event.git From ddb82763ed410e43b26fd3ec10ae7026923f9a8f Mon Sep 17 00:00:00 2001 From: Maxime Petit Date: Mon, 20 May 2024 16:20:37 +0200 Subject: [PATCH 5/5] ci: add JaCoCo code coverage check --- .github/workflows/pr.yml | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 .github/workflows/pr.yml diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml new file mode 100644 index 0000000..9437b7c --- /dev/null +++ b/.github/workflows/pr.yml @@ -0,0 +1,40 @@ +name: Test & Coverage +on: + pull_request: + branches: + - '*' + +jobs: + test-and-coverage: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Set up JDK 21 + uses: actions/setup-java@v3 + with: + java-version: '21' + distribution: 'temurin' + + - name: Set up the Maven dependencies caching + uses: actions/cache@v3 + with: + path: ~/.m2 + key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} + restore-keys: ${{ runner.os }}-m2 + + - name: Run tests + run: mvn --batch-mode --update-snapshots verify -Dgpg.skip=true + + - name: Add coverage + uses: madrapps/jacoco-report@v1.6.1 + with: + paths: | + ${{ github.workspace }}/**/target/site/jacoco/jacoco.xml + token: ${{ secrets.GITHUB_TOKEN }} + title: '### :zap: Coverage report' + update-comment: true + min-coverage-overall: 80 + min-coverage-changed-files: 60 + continue-on-error: false