diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..4dafb39 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,65 @@ +name: CI + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build-and-test: + name: Build, test and report coverage + runs-on: ubuntu-latest + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up JDK 8 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '8' + + - name: Cache Gradle packages + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle', 'gradle/wrapper/gradle-wrapper.properties') }} + restore-keys: ${{ runner.os }}-gradle- + + - name: Make Gradle wrapper executable + run: chmod +x ./gradlew + + - name: Run tests with coverage + run: ./gradlew clean test jacocoTestReport jacocoTestCoverageVerification --no-daemon --stacktrace + + - name: Coverage summary + if: always() + run: | + REPORT=build/reports/jacoco/test/jacocoTestReport.csv + if [ -f "$REPORT" ]; then + awk -F, 'NR>1 { lm += $8; lc += $9 } END { + total = lm + lc; + pct = (total > 0) ? lc * 100 / total : 0; + printf "## JaCoCo coverage\n\n| Metric | Value |\n|---|---|\n| Lines covered | %d / %d |\n| Line coverage | %.2f%% |\n", lc, total, pct + }' "$REPORT" >> "$GITHUB_STEP_SUMMARY" + else + echo "No JaCoCo report found." >> "$GITHUB_STEP_SUMMARY" + fi + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results + path: build/reports/tests/test + + - name: Upload coverage report + if: always() + uses: actions/upload-artifact@v4 + with: + name: jacoco-coverage-report + path: build/reports/jacoco/test diff --git a/.gitignore b/.gitignore index 57f1cb2..14e5aca 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ -/.idea/ \ No newline at end of file +/.idea/ +/.gradle/ +/build/ diff --git a/build.gradle b/build.gradle index 09f1083..6788a23 100644 --- a/build.gradle +++ b/build.gradle @@ -10,6 +10,7 @@ buildscript { apply plugin: 'java' apply plugin: 'eclipse' apply plugin: 'idea' +apply plugin: 'jacoco' apply plugin: 'org.springframework.boot' apply plugin: 'io.spring.dependency-management' @@ -25,8 +26,52 @@ repositories { sourceCompatibility = 1.8 targetCompatibility = 1.8 +ext { + junitVersion = '5.2.0' +} + dependencies { compile("org.springframework.boot:spring-boot-starter-web") - testCompile("junit:junit") + compile("org.springframework.boot:spring-boot-starter-jdbc") + compile("com.h2database:h2") + runtime("org.springframework.boot:spring-boot-properties-migrator") + + testImplementation("org.junit.jupiter:junit-jupiter-api:${junitVersion}") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:${junitVersion}") +} + +test { + useJUnitPlatform() + testLogging { + events "passed", "skipped", "failed" + } + finalizedBy jacocoTestReport +} + +jacoco { + toolVersion = "0.8.5" +} + +jacocoTestReport { + dependsOn test + reports { + xml.enabled true + html.enabled true + csv.enabled true + } +} + +jacocoTestCoverageVerification { + violationRules { + rule { + element = 'BUNDLE' + limit { + counter = 'LINE' + value = 'COVEREDRATIO' + minimum = 0.60 + } + } + } } +check.dependsOn jacocoTestCoverageVerification diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/src/test/java/hello/controller/GreetingControllerTest.java b/src/test/java/hello/controller/GreetingControllerTest.java new file mode 100644 index 0000000..bf7ccc4 --- /dev/null +++ b/src/test/java/hello/controller/GreetingControllerTest.java @@ -0,0 +1,28 @@ +package hello.controller; + +import hello.model.Greeting; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class GreetingControllerTest { + + @Test + void greetingUsesDefaultNameAndIncrementsCounter() { + GreetingController controller = new GreetingController(); + + Greeting first = controller.greeting("World"); + assertEquals(1L, first.getId()); + assertEquals("Hello, World!", first.getContent()); + + Greeting second = controller.greeting("World"); + assertEquals(2L, second.getId()); + } + + @Test + void greetingUsesProvidedName() { + GreetingController controller = new GreetingController(); + Greeting greeting = controller.greeting("Matthew"); + assertEquals("Hello, Matthew!", greeting.getContent()); + } +} diff --git a/src/test/java/hello/controller/HelloControllerTest.java b/src/test/java/hello/controller/HelloControllerTest.java new file mode 100644 index 0000000..f6ff611 --- /dev/null +++ b/src/test/java/hello/controller/HelloControllerTest.java @@ -0,0 +1,48 @@ +package hello.controller; + +import hello.service.TopicService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Field; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class HelloControllerTest { + + private HelloController controller; + + @BeforeEach + void setUp() throws Exception { + controller = new HelloController(); + Field field = HelloController.class.getDeclaredField("topicService"); + field.setAccessible(true); + field.set(controller, new TopicService()); + } + + @Test + void indexReturnsDateTimeGreeting() { + String result = controller.index(); + assertNotNull(result); + assertTrue(result.startsWith("Greetings from Spring Boot!")); + assertTrue(result.contains("Is this a leap year ?")); + } + + @Test + void showStringOperationCombinesServiceResults() { + String result = controller.showStringOperation(); + assertTrue(result.contains("spring:java:javascript")); + assertTrue(result.contains("java:javascript")); + assertTrue(result.contains("[spring]")); + } + + @Test + void showFileOperationReturnsCombinedFileResults() { + String result = controller.showFileOperation(); + assertNotNull(result); + assertFalse(result.contains("IO exception")); + assertTrue(result.contains("Find all files in path and sort")); + } +} diff --git a/src/test/java/hello/controller/TopicControllerTest.java b/src/test/java/hello/controller/TopicControllerTest.java new file mode 100644 index 0000000..0fa24c9 --- /dev/null +++ b/src/test/java/hello/controller/TopicControllerTest.java @@ -0,0 +1,69 @@ +package hello.controller; + +import hello.model.Topic; +import hello.service.TopicService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +class TopicControllerTest { + + private TopicController controller; + + @BeforeEach + void setUp() throws Exception { + controller = new TopicController(); + Field field = TopicController.class.getDeclaredField("topicService"); + field.setAccessible(true); + field.set(controller, new TopicService()); + } + + private List ids(List topics) { + return topics.stream().map(Topic::getId).collect(Collectors.toList()); + } + + @Test + void getAllTopicsDelegatesToService() { + assertEquals(Arrays.asList("spring", "java", "javascript"), ids(controller.getAllTopics())); + } + + @Test + void getTopicWithIdDelegatesToService() { + assertEquals("Core Java", controller.getTopicWithID("java").getSubjectName()); + } + + @Test + void addTopicDelegatesToService() { + controller.addTopic(new Topic("go", "Go Lang", "Go Description")); + assertEquals(4, controller.getAllTopics().size()); + } + + @Test + void updateTopicDelegatesToService() { + controller.updateTopic("java", new Topic("java", "Modern Java", "Updated")); + assertEquals("Modern Java", controller.getTopicWithID("java").getSubjectName()); + } + + @Test + void deleteTopicDelegatesToService() { + controller.deleteTopic("java"); + assertFalse(ids(controller.getAllTopics()).contains("java")); + } + + @Test + void filterMinimumLengthForIdDelegatesToService() { + assertEquals(Arrays.asList("javascript"), ids(controller.filterMinimumLengthForId(6))); + } + + @Test + void sortTopicsWithIdDelegatesToService() { + assertEquals(Arrays.asList("java", "javascript", "spring"), ids(controller.sortTopicsWithID())); + } +} diff --git a/src/test/java/hello/declaration/TimeClientTest.java b/src/test/java/hello/declaration/TimeClientTest.java new file mode 100644 index 0000000..b633da5 --- /dev/null +++ b/src/test/java/hello/declaration/TimeClientTest.java @@ -0,0 +1,33 @@ +package hello.declaration; + +import hello.model.SimpleTimeClient; +import org.junit.jupiter.api.Test; + +import java.time.ZoneId; +import java.time.ZonedDateTime; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class TimeClientTest { + + @Test + void getZoneIdReturnsRequestedZoneWhenValid() { + assertEquals(ZoneId.of("Europe/London"), TimeClient.getZoneId("Europe/London")); + } + + @Test + void getZoneIdFallsBackToSystemDefaultWhenInvalid() { + assertEquals(ZoneId.systemDefault(), TimeClient.getZoneId("Not/AZone")); + } + + @Test + void getZonedDateTimeUsesLocalDateTimeAndZone() { + SimpleTimeClient client = new SimpleTimeClient(); + client.setDateAndTime(2021, 6, 20, 10, 30, 45); + + ZonedDateTime zoned = client.getZonedDateTime("America/New_York"); + + assertEquals(client.getLocalDateTime(), zoned.toLocalDateTime()); + assertEquals(ZoneId.of("America/New_York"), zoned.getZone()); + } +} diff --git a/src/test/java/hello/model/ModelPojoTest.java b/src/test/java/hello/model/ModelPojoTest.java new file mode 100644 index 0000000..fd9fd71 --- /dev/null +++ b/src/test/java/hello/model/ModelPojoTest.java @@ -0,0 +1,81 @@ +package hello.model; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ModelPojoTest { + + @Test + void topicAllArgsConstructorAndGetters() { + Topic topic = new Topic("java", "Core Java", "Java Description"); + assertEquals("java", topic.getId()); + assertEquals("Core Java", topic.getSubjectName()); + assertEquals("Java Description", topic.getSubjectDescription()); + } + + @Test + void topicNoArgsConstructorAndSetters() { + Topic topic = new Topic(); + topic.setId("go"); + topic.setSubjectName("Go Lang"); + topic.setSubjectDescription("Go Description"); + assertEquals("go", topic.getId()); + assertEquals("Go Lang", topic.getSubjectName()); + assertEquals("Go Description", topic.getSubjectDescription()); + } + + @Test + void greetingIsImmutable() { + Greeting greeting = new Greeting(42L, "Hello, World!"); + assertEquals(42L, greeting.getId()); + assertEquals("Hello, World!", greeting.getContent()); + } + + @Test + void customerGettersSettersAndToString() { + Customer customer = new Customer(1L, "Josh", "Bloch"); + assertEquals(1L, customer.getId()); + assertEquals("Josh", customer.getFirstName()); + assertEquals("Bloch", customer.getLastName()); + + customer.setId(2L); + customer.setFirstName("Jeff"); + customer.setLastName("Dean"); + assertEquals(2L, customer.getId()); + assertEquals("Jeff", customer.getFirstName()); + assertEquals("Dean", customer.getLastName()); + + String text = customer.toString(); + assertTrue(text.contains("Jeff")); + assertTrue(text.contains("Dean")); + assertTrue(text.contains("id=2")); + } + + @Test + void valueGettersSettersAndToString() { + Value value = new Value(); + value.setId(7L); + value.setQuote("Stay hungry"); + assertEquals(Long.valueOf(7L), value.getId()); + assertEquals("Stay hungry", value.getQuote()); + assertTrue(value.toString().contains("Stay hungry")); + } + + @Test + void quoteGettersSettersAndToString() { + Value value = new Value(); + value.setId(1L); + value.setQuote("Work hard"); + + Quote quote = new Quote(); + quote.setType("success"); + quote.setValue(value); + + assertEquals("success", quote.getType()); + assertEquals(value, quote.getValue()); + assertTrue(quote.toString().contains("success")); + assertTrue(quote.toString().contains("Work hard")); + } +} diff --git a/src/test/java/hello/model/SimpleTimeClientTest.java b/src/test/java/hello/model/SimpleTimeClientTest.java new file mode 100644 index 0000000..74f8769 --- /dev/null +++ b/src/test/java/hello/model/SimpleTimeClientTest.java @@ -0,0 +1,40 @@ +package hello.model; + +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class SimpleTimeClientTest { + + @Test + void setDateAndTimeSetsFullTimestamp() { + SimpleTimeClient client = new SimpleTimeClient(); + client.setDateAndTime(2021, 6, 20, 10, 30, 45); + assertEquals(LocalDateTime.of(2021, 6, 20, 10, 30, 45), client.getLocalDateTime()); + } + + @Test + void toStringMatchesLocalDateTime() { + SimpleTimeClient client = new SimpleTimeClient(); + client.setDateAndTime(2021, 6, 20, 10, 30, 45); + assertEquals(client.getLocalDateTime().toString(), client.toString()); + } + + @Test + void setTimeKeepsDateAndUpdatesTime() { + SimpleTimeClient client = new SimpleTimeClient(); + client.setDateAndTime(2021, 6, 20, 10, 30, 45); + client.setTime(1, 2, 3); + assertEquals(LocalDateTime.of(2021, 6, 20, 1, 2, 3), client.getLocalDateTime()); + } + + @Test + void setDateKeepsTimeAndUpdatesDate() { + SimpleTimeClient client = new SimpleTimeClient(); + client.setDateAndTime(2021, 6, 20, 10, 30, 45); + client.setDate(2000, 1, 1); + assertEquals(LocalDateTime.of(2000, 1, 1, 10, 30, 45), client.getLocalDateTime()); + } +} diff --git a/src/test/java/hello/service/TopicServiceTest.java b/src/test/java/hello/service/TopicServiceTest.java new file mode 100644 index 0000000..36d11bd --- /dev/null +++ b/src/test/java/hello/service/TopicServiceTest.java @@ -0,0 +1,182 @@ +package hello.service; + +import hello.model.Topic; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class TopicServiceTest { + + private TopicService service; + + @BeforeEach + void setUp() { + service = new TopicService(); + } + + private List ids(List topics) { + return topics.stream().map(Topic::getId).collect(Collectors.toList()); + } + + @Nested + @DisplayName("CRUD list operations") + class CrudOperations { + + @Test + void getAllTopicsReturnsSeededTopics() { + List all = service.getAllTopics(); + assertEquals(3, all.size()); + assertEquals(Arrays.asList("spring", "java", "javascript"), ids(all)); + } + + @Test + void getTopicWithIdReturnsMatchingTopic() { + Topic topic = service.getTopicWithId("java"); + assertEquals("java", topic.getId()); + assertEquals("Core Java", topic.getSubjectName()); + } + + @Test + void getTopicWithIdThrowsWhenMissing() { + assertThrows(NoSuchElementException.class, () -> service.getTopicWithId("missing")); + } + + @Test + void addTopicAppendsToList() { + service.addTopic(new Topic("go", "Go Lang", "Go Description")); + assertEquals(4, service.getAllTopics().size()); + assertEquals("go", service.getTopicWithId("go").getId()); + } + + @Test + void updateTopicReplacesExistingEntry() { + service.updateTopic("java", new Topic("java", "Modern Java", "Updated")); + assertEquals("Modern Java", service.getTopicWithId("java").getSubjectName()); + assertEquals(3, service.getAllTopics().size()); + } + + @Test + void updateTopicIsNoOpWhenIdNotFound() { + service.updateTopic("missing", new Topic("missing", "Nope", "Nope")); + assertEquals(3, service.getAllTopics().size()); + assertEquals(Arrays.asList("spring", "java", "javascript"), ids(service.getAllTopics())); + } + + @Test + void deleteTopicRemovesMatchingEntry() { + service.deleteTopic("java"); + assertEquals(2, service.getAllTopics().size()); + assertFalse(ids(service.getAllTopics()).contains("java")); + } + } + + @Nested + @DisplayName("Filtering and sorting") + class FilteringAndSorting { + + @Test + void filterMinimumLengthForIdKeepsLongerIds() { + List filtered = service.filterMinimumLengthForId(6); + assertEquals(Arrays.asList("javascript"), ids(filtered)); + } + + @Test + void filterMinimumLengthForIdReturnsEmptyWhenNoneQualify() { + assertTrue(service.filterMinimumLengthForId(100).isEmpty()); + } + + @Test + void sortTopicsWithIdSortsAlphabetically() { + List sorted = service.sortTopicsWithID(); + assertEquals(Arrays.asList("java", "javascript", "spring"), ids(sorted)); + } + } + + @Nested + @DisplayName("String / stream transforms") + class StringOperations { + + @Test + void returnAllTopicIdWithStringSlicingJoinsWithColon() { + assertEquals("spring:java:javascript", service.returnAllTopicIDWithStringSlicing()); + } + + @Test + void makeDistinctAndSortCharactersRemovesDuplicatesAndSorts() { + assertEquals("abcr", service.makeDistinctAndSortCharacters("cabbra")); + } + + @Test + void makeDistinctAndSortCharactersHandlesColonsAndLetters() { + assertEquals(":acgijnprstv", service.makeDistinctAndSortCharacters("spring:java:javascript")); + } + + @Test + void makeDistinctAndSortCharactersEmptyInput() { + assertEquals("", service.makeDistinctAndSortCharacters("")); + } + + @Test + void splitAllIdSelectsOnlyEntriesContainingJavaSortedAndJoined() { + String result = service.splitAllIdWithColonSelectIDWithJavaKeywordThenSortThenJoin( + "spring:java:javascript"); + assertEquals("java:javascript", result); + } + + @Test + void splitAllIdReturnsEmptyWhenNoJavaKeyword() { + assertEquals("", service.splitAllIdWithColonSelectIDWithJavaKeywordThenSortThenJoin("spring:go:rust")); + } + + @Test + void findIdHavingCharacterReturnsIdsContainingLetterG() { + assertEquals("[spring]", service.findIdHavingCharacter()); + } + } + + @Nested + @DisplayName("File / NIO operations") + class FileOperations { + + @Test + void findAllFilesInPathAndSortListsProjectFiles() { + String result = service.findAllFilesInPathAndSort(); + assertNotNull(result); + assertFalse(result.startsWith(" Error")); + assertTrue(result.contains("src"), "expected listing to contain project sources: " + result); + } + + @Test + void findParticularFileInPathAndSortFindsGradleFiles() { + String result = service.findParticularFileInPathAndSort(); + assertNotNull(result); + assertFalse(result.contains("IO exception")); + assertTrue(result.contains("grad"), "expected to find gradle files: " + result); + } + + @Test + void findParticularFileInPathAndSortWithWalkFunctionFindsGradleFiles() { + String result = service.findParticularFileInPathAndSortWithWalkFunction(); + assertNotNull(result); + assertFalse(result.contains("IO exception")); + assertTrue(result.contains("grad"), "expected to find gradle files: " + result); + } + + @Test + void readFileWithStreamFunctionExtractsPrintLines() { + assertEquals(" Hello, this, is, Rehman", service.readFileWithStreamFunction()); + } + } +}