diff --git a/src/main/kotlin/com/example/demo/config/ProducerConfig.kt b/src/main/kotlin/com/example/demo/config/ProducerConfig.kt index c7e29ab..c4fea98 100644 --- a/src/main/kotlin/com/example/demo/config/ProducerConfig.kt +++ b/src/main/kotlin/com/example/demo/config/ProducerConfig.kt @@ -5,6 +5,7 @@ import com.example.demo.services.kafka.ProducerImpl import com.example.demo.services.kafka.dto.serializer.ProductSerializer import org.apache.kafka.clients.producer.ProducerConfig import org.apache.kafka.common.serialization.StringSerializer +import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Value import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration @@ -34,7 +35,7 @@ class ProducerConfig( ) @Bean - fun producer(): Producer = ProducerImpl( - kafkaTemplate(), + fun producer(@Autowired kafkaTemplate: KafkaTemplate): Producer = ProducerImpl( + kafkaTemplate, ) } \ No newline at end of file diff --git a/src/test/kotlin/com/example/demo/BaseFeatureTest.kt b/src/test/kotlin/com/example/demo/BaseFeatureTest.kt index f8582fa..714fa90 100644 --- a/src/test/kotlin/com/example/demo/BaseFeatureTest.kt +++ b/src/test/kotlin/com/example/demo/BaseFeatureTest.kt @@ -15,5 +15,5 @@ import org.testcontainers.junit.jupiter.Testcontainers @EnableJdbcRepositories class BaseFeatureTest { @MockBean - private lateinit var producer: Producer + lateinit var producer: Producer } \ No newline at end of file diff --git a/src/test/kotlin/com/example/demo/controllers/ProductControllerTest.kt b/src/test/kotlin/com/example/demo/controllers/ProductControllerTest.kt index b614f07..65952cf 100644 --- a/src/test/kotlin/com/example/demo/controllers/ProductControllerTest.kt +++ b/src/test/kotlin/com/example/demo/controllers/ProductControllerTest.kt @@ -7,7 +7,6 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import org.hamcrest.Matchers.contains import org.hamcrest.Matchers.nullValue import org.junit.jupiter.api.Test -import org.mockito.kotlin.doReturn import org.mockito.kotlin.eq import org.mockito.kotlin.verifyNoInteractions import org.mockito.kotlin.whenever @@ -48,7 +47,8 @@ class ProductControllerTest(@Autowired val mockMvc: MockMvc) { whenever(productService.findByGuid( eq(guid), - )) doReturn product + )) + .thenReturn(product) mockMvc.get("/api/product/$guid") .andExpect { status { status { isOk() } } } @@ -66,7 +66,8 @@ class ProductControllerTest(@Autowired val mockMvc: MockMvc) { whenever(productService.findByGuid( eq(guid), - )) doReturn null + )) + .thenReturn(null) mockMvc.get("/api/product/$guid") .andExpect { status { status { isNotFound() } } } @@ -89,16 +90,17 @@ class ProductControllerTest(@Autowired val mockMvc: MockMvc) { eq(name), eq(price), eq(description) - )) doReturn Product( - id = productId, - guid = UUID.randomUUID(), - name = name, - description = description, - price = price, - createdAt = OffsetDateTime.now(), - updatedAt = null, - deletedAt = null, - ) + )) + .thenReturn(Product( + id = productId, + guid = UUID.randomUUID(), + name = name, + description = description, + price = price, + createdAt = OffsetDateTime.now(), + updatedAt = null, + deletedAt = null, + )) mockMvc.post("/api/product") { contentType = MediaType.APPLICATION_JSON @@ -118,8 +120,6 @@ class ProductControllerTest(@Autowired val mockMvc: MockMvc) { mapOf("description" to description, "price" to price) ) - verifyNoInteractions(productService) - mockMvc.post("/api/product") { contentType = MediaType.APPLICATION_JSON content = reqBody @@ -128,6 +128,8 @@ class ProductControllerTest(@Autowired val mockMvc: MockMvc) { .andExpect { content { contentType(MediaType.APPLICATION_JSON) } } .andExpect { jsonPath("\$.status") { value(ResponseStatus.BAD_REQUEST.status) } } .andExpect { jsonPath("\$.cause") { contains("name") } } + + verifyNoInteractions(productService) } @Test @@ -139,8 +141,6 @@ class ProductControllerTest(@Autowired val mockMvc: MockMvc) { mapOf("name" to "", "description" to description, "price" to price) ) - verifyNoInteractions(productService) - mockMvc.post("/api/product") { contentType = MediaType.APPLICATION_JSON content = reqBody @@ -149,6 +149,8 @@ class ProductControllerTest(@Autowired val mockMvc: MockMvc) { .andExpect { content { contentType(MediaType.APPLICATION_JSON) } } .andExpect { jsonPath("\$.status") { value(ResponseStatus.UNPROCESSABLE.status) } } .andExpect { jsonPath("\$.cause") { value(MethodArgumentNotValidException::class.qualifiedName) } } + + verifyNoInteractions(productService) } @Test @@ -157,16 +159,17 @@ class ProductControllerTest(@Autowired val mockMvc: MockMvc) { whenever(productService.delete( eq(guid), - )) doReturn Product( - id = 2133, - guid = guid, - name = "name", - description = "description", - price = 210202, - createdAt = OffsetDateTime.now(), - updatedAt = null, - deletedAt = OffsetDateTime.now(), - ) + )) + .thenReturn(Product( + id = 2133, + guid = guid, + name = "name", + description = "description", + price = 210202, + createdAt = OffsetDateTime.now(), + updatedAt = null, + deletedAt = OffsetDateTime.now(), + )) mockMvc.delete("/api/product/${guid}") .andExpect { status { status { isOk() } } } diff --git a/src/test/kotlin/com/example/demo/services/ProductServiceImplFeatureTest.kt b/src/test/kotlin/com/example/demo/services/ProductServiceImplFeatureTest.kt new file mode 100644 index 0000000..f83b534 --- /dev/null +++ b/src/test/kotlin/com/example/demo/services/ProductServiceImplFeatureTest.kt @@ -0,0 +1,69 @@ +package com.example.demo.services + +import com.example.demo.BaseFeatureTest +import com.example.demo.exceptions.NotFoundException +import com.example.demo.exceptions.UnprocessableException +import com.example.demo.models.Product +import com.example.demo.provider.ProductRepository +import org.junit.jupiter.api.assertThrows +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.test.context.ContextConfiguration +import java.util.* +import kotlin.test.* + +@ContextConfiguration(classes = [ProductRepository::class]) +class ProductServiceImplFeatureTest: BaseFeatureTest() { + private lateinit var productService: ProductServiceImpl + @Autowired + private lateinit var productRepository: ProductRepository + + @BeforeTest + fun setUp() { + productService = ProductServiceImpl( + defaultSyncTopic = "some-default-topic", + productRepository = productRepository, + producer = producer + ) + } + + @Test + fun createFindDelete_success() { + val name = "new-product-name" + val price = 33333.toLong() + val description = "some-description" + var product: Product? = null + + try { + product = productService.create(name = name, price = price, description = description) + assertNotNull(product.id) + assertEquals(name, product.name) + assertEquals(price, product.price) + assertEquals(333.33, product.getPriceDouble()) + + val dbProduct = productService.findByGuid(product.guid) + assertNotNull(dbProduct) + assertEquals(product.id, dbProduct.id) + assertFalse(dbProduct.isDeleted()) + + val deletedProduct = productService.delete(product.guid) + assertNotNull(deletedProduct) + assertEquals(product.id, deletedProduct.id) + assertNotNull(deletedProduct.deletedAt) + assertTrue(deletedProduct.isDeleted()) + + // try to delete already deleted product + assertThrows { + productService.delete(product.guid) + } + + assertThrows { + productService.delete(UUID.randomUUID()) + } + } finally { + val id = product?.id + if (id != null) { + productRepository.deleteById(id) + } + } + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/example/demo/services/ProductServiceImplTest.kt b/src/test/kotlin/com/example/demo/services/ProductServiceImplTest.kt index 9b697b2..e61d07f 100644 --- a/src/test/kotlin/com/example/demo/services/ProductServiceImplTest.kt +++ b/src/test/kotlin/com/example/demo/services/ProductServiceImplTest.kt @@ -1,49 +1,102 @@ package com.example.demo.services -import com.example.demo.BaseFeatureTest +import com.example.demo.exceptions.NotFoundException import com.example.demo.models.Product import com.example.demo.provider.ProductRepository -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.test.context.ContextConfiguration -import kotlin.test.* +import com.example.demo.services.kafka.Producer +import com.example.demo.services.kafka.exceptions.InvalidArgumentException +import org.junit.jupiter.api.assertThrows +import org.junit.runner.RunWith +import org.mockito.kotlin.* +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.mock.mockito.MockBean +import org.springframework.test.context.junit4.SpringRunner +import java.time.OffsetDateTime +import java.util.* +import kotlin.test.BeforeTest +import kotlin.test.Test -@ContextConfiguration(classes = [ProductRepository::class, ProductServiceImpl::class]) -class ProductServiceImplTest: BaseFeatureTest() { - @Autowired +@RunWith(SpringRunner::class) +@SpringBootTest +class ProductServiceImplTest { + private val defaultTopic = "some-default-topic" private lateinit var productService: ProductServiceImpl - @Autowired + + @MockBean + @Qualifier("producer") + private lateinit var producer: Producer + @MockBean private lateinit var productRepository: ProductRepository + @BeforeTest + fun setUp() { + productService = ProductServiceImpl( + defaultSyncTopic = defaultTopic, + productRepository = productRepository, + producer = producer, + ) + } + @Test - fun createFindDelete_success() { - val name = "new-product-name" - val price = 33333.toLong() - val description = "some-description" - var product: Product? = null + fun syncToKafka_success() { + val guid = UUID.randomUUID() + val product = Product( + id = 123, + guid = guid, + name = "name", + description = "description", + price = 10050, + createdAt = OffsetDateTime.now().minusDays(1), + updatedAt = OffsetDateTime.now().minusHours(2), + deletedAt = OffsetDateTime.now(), + ) - try { - product = productService.create(name = name, price = price, description = description) - assertNotNull(product.id) - assertEquals(name, product.name) - assertEquals(price, product.price) - assertEquals(333.33, product.getPriceDouble()) + whenever(productRepository.findByGuid(eq(guid))) + .thenReturn(product) + whenever(producer.produceProductInfo(defaultTopic, product)).doAnswer{} - val dbProduct = productService.findByGuid(product.guid) - assertNotNull(dbProduct) - assertEquals(product.id, dbProduct.id) - assertFalse(dbProduct.isDeleted()) + productService.syncToKafka(guid, null) + } - val deletedProduct = productService.delete(product.guid) - assertNotNull(deletedProduct) - assertEquals(product.id, deletedProduct.id) - assertNotNull(deletedProduct.deletedAt) - assertTrue(deletedProduct.isDeleted()) - } finally { - val id = product?.id - if (id != null) { - productRepository.deleteById(id) - } + @Test + fun syncToKafka_notFound() { + val specificTopic = "specificNotice" + val guid = UUID.randomUUID() + whenever(productRepository.findByGuid(eq(guid))) + .thenReturn(null) + + assertThrows { + productService.syncToKafka(guid, specificTopic) + } + + verifyNoInteractions(producer) + } + + @Test + fun syncToKafka_invalidArgumentException() { + val specificTopic = "specificNotice" + val guid = UUID.randomUUID() + + val product = Product( + id = 123, + guid = guid, + name = "name", + description = "description", + price = 10050, + createdAt = OffsetDateTime.now().minusDays(1), + updatedAt = OffsetDateTime.now().minusHours(2), + deletedAt = OffsetDateTime.now(), + ) + + whenever(productRepository.findByGuid(eq(guid))) + .thenReturn(product) + whenever(producer.produceProductInfo(specificTopic, product)) + .doThrow(InvalidArgumentException("some error")) + + assertThrows< InvalidArgumentException> { + productService.syncToKafka(guid, specificTopic) } } } \ No newline at end of file