From 9a490baa1e0736de610b817272a1e477c0687eea Mon Sep 17 00:00:00 2001 From: xcodeassociated Date: Tue, 17 Sep 2024 00:39:22 +0200 Subject: [PATCH] add junit tests --- build.gradle | 8 + .../permission/api/PermissionController.kt | 2 + .../http/external/api/ExternalController.kt | 2 + .../permission/api/PermissionITSpec.groovy | 51 ----- .../softeno/template/app/IntegrationTest.kt | 216 ++++++++++++++++++ .../app/config/TestRestTemplateConfig.kt | 22 -- .../app/permission/PermissionFixture.kt | 18 +- src/test/resources/application.properties | 2 +- 8 files changed, 246 insertions(+), 75 deletions(-) delete mode 100644 src/test/groovy/com/softeno/template/app/permission/api/PermissionITSpec.groovy create mode 100644 src/test/kotlin/com/softeno/template/app/IntegrationTest.kt delete mode 100644 src/test/kotlin/com/softeno/template/app/config/TestRestTemplateConfig.kt diff --git a/build.gradle b/build.gradle index c69217b..50caf73 100644 --- a/build.gradle +++ b/build.gradle @@ -42,6 +42,14 @@ dependencies { annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' testImplementation 'org.springframework.boot:spring-boot-starter-test' + // test utils + testImplementation 'io.projectreactor:reactor-test' + testImplementation 'io.mockk:mockk:1.13.12' + testImplementation 'com.ninja-squad:springmockk:4.0.2' + + // wiremock + testImplementation 'org.wiremock:wiremock-standalone:3.9.1' + // monitoring runtimeOnly 'io.micrometer:micrometer-registry-prometheus' implementation 'org.springframework.boot:spring-boot-starter-actuator' diff --git a/src/main/kotlin/com/softeno/template/app/permission/api/PermissionController.kt b/src/main/kotlin/com/softeno/template/app/permission/api/PermissionController.kt index 2978226..39ad30a 100644 --- a/src/main/kotlin/com/softeno/template/app/permission/api/PermissionController.kt +++ b/src/main/kotlin/com/softeno/template/app/permission/api/PermissionController.kt @@ -8,6 +8,7 @@ import org.apache.commons.logging.LogFactory import org.springframework.context.ApplicationEventPublisher import org.springframework.data.domain.Page import org.springframework.http.ResponseEntity +import org.springframework.validation.annotation.Validated import org.springframework.web.bind.annotation.DeleteMapping import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable @@ -18,6 +19,7 @@ import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController @RestController +@Validated class PermissionController( private val permissionService: PermissionService, private val applicationEventPublisher: ApplicationEventPublisher diff --git a/src/main/kotlin/com/softeno/template/sample/http/external/api/ExternalController.kt b/src/main/kotlin/com/softeno/template/sample/http/external/api/ExternalController.kt index e3031c3..667f8d2 100644 --- a/src/main/kotlin/com/softeno/template/sample/http/external/api/ExternalController.kt +++ b/src/main/kotlin/com/softeno/template/sample/http/external/api/ExternalController.kt @@ -3,6 +3,7 @@ package com.softeno.template.sample.http.external.api import com.softeno.template.sample.http.external.client.ExternalServiceClient import org.apache.commons.logging.LogFactory import org.springframework.http.ResponseEntity +import org.springframework.validation.annotation.Validated import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.RequestMapping @@ -10,6 +11,7 @@ import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/external") +@Validated class ExternalController( private val externalServiceClient: ExternalServiceClient ) { diff --git a/src/test/groovy/com/softeno/template/app/permission/api/PermissionITSpec.groovy b/src/test/groovy/com/softeno/template/app/permission/api/PermissionITSpec.groovy deleted file mode 100644 index 5ff9268..0000000 --- a/src/test/groovy/com/softeno/template/app/permission/api/PermissionITSpec.groovy +++ /dev/null @@ -1,51 +0,0 @@ -package com.softeno.template.app.permission.api - -import com.fasterxml.jackson.databind.JsonNode -import com.fasterxml.jackson.databind.ObjectMapper -import com.softeno.template.app.BaseAppSpec -import com.softeno.template.app.permission.PermissionFixture -import com.softeno.template.app.permission.mapper.PermissionDto -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.test.web.client.TestRestTemplate -import org.springframework.http.HttpStatus - -class PermissionITSpec extends BaseAppSpec { - - @Autowired - TestRestTemplate testRestTemplate - - @Autowired - ObjectMapper objectMapper - - def "get permissions ok with no data"() { - expect: - def expectedContent = [] as PermissionDto[] - def expectedTotalElements = 0 - def expectedTotalPages = 0 - - def response = testRestTemplate.getForEntity("/permissions", String.class) - def responseBody = objectMapper.readTree(response.body) as JsonNode - def reader = objectMapper.readerForArrayOf(PermissionDto.class) - - def responseContent = reader.readValue(responseBody.get("content")) as PermissionDto[] - - response.statusCode == HttpStatus.OK - responseBody.get("totalElements").asInt() == expectedTotalElements - responseBody.get("totalPages").asInt() == expectedTotalPages - responseContent == expectedContent - } - - def "post new permission"() { - given: - def expectedPermissionName = UUID.randomUUID().toString() - def expectedPermissionDescription = UUID.randomUUID().toString() - def permission = PermissionFixture.somePermissionDto(expectedPermissionName, expectedPermissionDescription) - - expect: - def response = testRestTemplate.postForEntity("/permissions", permission, String.class) - def result = objectMapper.readValue(response.body, PermissionDto.class) - response.statusCode == HttpStatus.OK - result.name == expectedPermissionName - result.description == expectedPermissionDescription - } -} \ No newline at end of file diff --git a/src/test/kotlin/com/softeno/template/app/IntegrationTest.kt b/src/test/kotlin/com/softeno/template/app/IntegrationTest.kt new file mode 100644 index 0000000..49a51e8 --- /dev/null +++ b/src/test/kotlin/com/softeno/template/app/IntegrationTest.kt @@ -0,0 +1,216 @@ +package com.softeno.template.app + +import com.github.tomakehurst.wiremock.WireMockServer +import com.github.tomakehurst.wiremock.client.WireMock.* +import com.github.tomakehurst.wiremock.client.WireMock.aResponse +import com.github.tomakehurst.wiremock.client.WireMock.urlMatching +import com.github.tomakehurst.wiremock.core.WireMockConfiguration.options +import com.ninjasquad.springmockk.MockkBean +import com.softeno.template.SoftenoMvcJpaApp +import com.softeno.template.app.permission.PermissionFixture.Companion.aPermission +import com.softeno.template.app.permission.PermissionFixture.Companion.aPermissionDto +import com.softeno.template.app.permission.db.PermissionRepository +import io.mockk.every +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.context.properties.ConfigurationPropertiesScan +import org.springframework.boot.context.properties.EnableConfigurationProperties +import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.core.Ordered +import org.springframework.core.annotation.Order +import org.springframework.data.domain.PageImpl +import org.springframework.data.domain.Pageable +import org.springframework.kafka.test.context.EmbeddedKafka +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.web.reactive.function.BodyInserters +import org.springframework.web.reactive.function.client.WebClient +import org.testcontainers.containers.PostgreSQLContainer +import org.testcontainers.junit.jupiter.Container +import org.testcontainers.junit.jupiter.Testcontainers + +@Testcontainers +@SpringBootTest( + classes = [SoftenoMvcJpaApp::class], + properties = ["spring.profiles.active=integration"], + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT +) +@AutoConfigureWebTestClient(timeout = "6000") +@EnableConfigurationProperties +@EmbeddedKafka(partitions = 1, brokerProperties = ["listeners=PLAINTEXT://localhost:9092", "port=9092"]) +@ConfigurationPropertiesScan("com.softeno") +abstract class BaseIntegrationTest { + + @Autowired + lateinit var permissionRepository: PermissionRepository + + @Autowired + lateinit var webTestClient: WebTestClient + + @Container + var postgreSQLContainer = PostgreSQLContainer("postgres:15.2-alpine") + .withDatabaseName("application") + .withUsername("admin") + .withPassword("admin") + + + @BeforeEach + fun init() { + // ... + } + + @AfterEach + fun cleanup() { + permissionRepository.deleteAll() + } + +} + +class ContextLoadsTest : BaseIntegrationTest() { + + @Test + fun testConnection() { + assertTrue(postgreSQLContainer.isRunning) + } +} + +class PermissionTest : BaseIntegrationTest() { + + @Test + fun shouldReturnEmptyPermissionResponse() { + webTestClient.get().uri("/permissions") + .exchange() + .expectStatus().isOk() + .expectBody().jsonPath("content").isEmpty + } + + @Test + fun shouldRetrievePermission() { + val aPermission = aPermission() + permissionRepository.save(aPermission) + + webTestClient.get().uri("/permissions") + .exchange() + .expectStatus().isOk() + .expectBody() + .jsonPath("content.[0].name").isEqualTo(aPermission.name!!) + .jsonPath("content.[0].description").isEqualTo(aPermission.description!!) + } + + @Test + fun shouldPersistPermission() { + val aPermissionDto = aPermissionDto() + + webTestClient.post().uri("/permissions") + .body(BodyInserters.fromValue(aPermissionDto)) + .exchange() + .expectStatus().isOk + + assertEquals(permissionRepository.findAll().size, 1) + assertEquals(permissionRepository.findAll()[0].name!!, aPermissionDto.name) + assertEquals(permissionRepository.findAll()[0].description!!, aPermissionDto.description) + } +} + +class PermissionTestMockk : BaseIntegrationTest() { + + @MockkBean + @Order(value = Ordered.HIGHEST_PRECEDENCE) + lateinit var permissionRepositoryMock: PermissionRepository + + @BeforeEach + fun initMockkRepository() { + every { permissionRepositoryMock.deleteAll() }.answers { } + } + + @Test + fun shouldPersistAndRetrievePermission() { + val aPermission = aPermission() + + every { permissionRepositoryMock.findAll(any()) }.answers { PageImpl(listOf(aPermission)) } + + webTestClient.get().uri("/permissions") + .exchange() + .expectStatus().isOk() + .expectBody() + .jsonPath("content.[0].name").isEqualTo(aPermission.name!!) + .jsonPath("content.[0].description").isEqualTo(aPermission.description!!) + } +} + +data class SampleResponseDto(val data: String) + +class ExternalControllerTest : BaseIntegrationTest(), ExternalApiAbility { + + @Autowired + private lateinit var webclient: WebClient + + private val wiremock: WireMockServer = WireMockServer(options().port(4500)) + + @BeforeEach + fun `setup wiremock`() { + wiremock.start() + } + + @AfterEach + fun `stop wiremock`() { + wiremock.stop() + wiremock.resetAll() + } + + @Test + fun `mock external service with wiremock`() { + // given + mockGetId(wiremock) + + val expected = SampleResponseDto(data = "1") + + // expect + val response = webclient.get().uri("http://localhost:4500/sample/100") + .retrieve() + .bodyToMono(SampleResponseDto::class.java) + .block() + + assertEquals(expected, response) + } + + @Test + fun `test external controller`() { + // given + mockGetId(wiremock) + + // expect + webTestClient.get().uri("/external/1") + .exchange() + .expectStatus().isOk() + .expectBody() + .jsonPath("data").isEqualTo("1") + } +} + +interface ExternalApiAbility { + + fun mockGetId(wiremock: WireMockServer) { + wiremock.stubFor( + get(urlMatching("/sample/.*")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + """ + { + "data": "1" + } + """.trimIndent() + ) + ) + ) + } +} + + diff --git a/src/test/kotlin/com/softeno/template/app/config/TestRestTemplateConfig.kt b/src/test/kotlin/com/softeno/template/app/config/TestRestTemplateConfig.kt deleted file mode 100644 index 2a2241f..0000000 --- a/src/test/kotlin/com/softeno/template/app/config/TestRestTemplateConfig.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.softeno.template.app.config - -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.test.web.client.TestRestTemplate -import org.springframework.boot.web.client.RestTemplateBuilder -import org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration - -@Configuration -class TestRestTemplateConfig { - - @Autowired - lateinit var server: ServletWebServerApplicationContext - - @Bean - fun testRestTemplate(): TestRestTemplate { - val restTemplate = RestTemplateBuilder().rootUri("http://localhost:" + server.webServer.port) - return TestRestTemplate(restTemplate); - } - -} \ No newline at end of file diff --git a/src/test/kotlin/com/softeno/template/app/permission/PermissionFixture.kt b/src/test/kotlin/com/softeno/template/app/permission/PermissionFixture.kt index 7c6f12f..f3dee41 100644 --- a/src/test/kotlin/com/softeno/template/app/permission/PermissionFixture.kt +++ b/src/test/kotlin/com/softeno/template/app/permission/PermissionFixture.kt @@ -5,7 +5,23 @@ import com.softeno.template.app.permission.mapper.PermissionDto class PermissionFixture { companion object { @JvmStatic - fun somePermissionDto(name: String, description: String) = PermissionDto(name = name, description = description, + fun aPermission(name: String = "some permission", description: String = "some description"): Permission { + val permission = Permission() + permission.id = null + permission.createdDate = null + permission.createdBy = null + permission.modifiedBy = null + permission.modifiedDate = null + permission.version = null + permission.name = name + permission.description = description + + return permission + } + + + @JvmStatic + fun aPermissionDto(name: String = "some permission", description: String = "some description") = PermissionDto(name = name, description = description, id = null, version = null, createdBy = null, createdDate = null, modifiedBy = null, modifiedDate = null) } } \ No newline at end of file diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index de61d14..9c4c9fb 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -3,7 +3,7 @@ spring.application.name=SoftenoJpaPostgresApp ### custom: external services -com.softeno.external.url=http://localhost:4500 +com.softeno.external.url=http://localhost:4500/sample com.softeno.external.name=node-service com.softeno.kafka.tx=sample_topic_2