Notice
Recent Posts
Recent Comments
Link
반응형
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 | 31 |
Tags
- 엘라스틱서치
- 카카오
- 자바스크립트
- Effective Java
- Effective Java 3
- 알고리즘정렬
- kubernetes
- ElasticSearch
- 알고리즘
- 오블완
- JavaScript
- 이펙티브 자바
- 이펙티브자바
- java
- k8s
- Kotlin
- 김영한
- 예제로 배우는 스프링 입문
- Sort
- effectivejava
- 클린아키텍처
- 자바
- Spring
- 스프링
- 카카오 면접
- 스프링부트
- 스프링핵심원리
- 스프링 핵심원리
- 이차전지관련주
- 티스토리챌린지
Archives
- Today
- Total
Kim-Baek 개발자 이야기
Kotlin 테스트 완벽 가이드 | Mockito-Kotlin, JUnit5, Spring Boot Test 본문
반응형
Mockito-Kotlin으로 더 간결하고 강력한 테스트 코드 작성하기
목차
- Kotlin 테스트 환경 설정
- JUnit5 기본 테스트
- Mockito-Kotlin 소개
- Mock 객체 생성과 스터빙
- Argument Matchers와 Captors
- Verification - 호출 검증
- Spring Boot 통합 테스트
- 실전 테스트 패턴
들어가며
Java에서 Mockito를 사용하다가 Kotlin으로 넘어왔을 때, 처음에는 당황스러웠습니다. when()이 Kotlin 예약어라서 컴파일 오류가 나고, any() matcher가 null을 허용하지 않아서 NPE가 발생했습니다.
// ❌ 컴파일 오류
when(userRepository.findById(1L)).thenReturn(user)
// ❌ NPE 발생
verify(userService).createUser(any())
Mockito-Kotlin 라이브러리를 발견하고 나서는 이런 문제들이 모두 해결되었습니다. Kotlin 전용 DSL과 Extension Functions 덕분에 테스트 코드가 훨씬 읽기 쉬워졌습니다.
// ✅ Mockito-Kotlin
whenever(userRepository.findById(1L)).thenReturn(user)
verify(userService).createUser(any())
오늘은 Kotlin에서 테스트 코드를 작성하는 모든 방법을 다뤄보겠습니다.


1. Kotlin 테스트 환경 설정
1.1 의존성 추가
build.gradle.kts
dependencies {
// JUnit5
testImplementation("org.junit.jupiter:junit-jupiter:5.10.1")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
// Mockito-Kotlin
testImplementation("org.mockito.kotlin:mockito-kotlin:5.2.1")
// AssertJ (선택사항)
testImplementation("org.assertj:assertj-core:3.24.2")
// Kotest (선택사항)
testImplementation("io.kotest:kotest-runner-junit5:5.8.0")
testImplementation("io.kotest:kotest-assertions-core:5.8.0")
// Spring Boot Test
testImplementation("org.springframework.boot:spring-boot-starter-test") {
exclude(group = "org.mockito", module = "mockito-core")
}
// MockK (Kotlin 전용, 선택사항)
testImplementation("io.mockk:mockk:1.13.8")
}
tasks.withType<Test> {
useJUnitPlatform()
}
1.2 테스트 디렉토리 구조
src/
├── main/
│ └── kotlin/
│ └── com/example/
│ ├── domain/
│ ├── service/
│ └── controller/
└── test/
└── kotlin/
└── com/example/
├── domain/
├── service/
│ └── UserServiceTest.kt
└── controller/
└── UserControllerTest.kt
2. JUnit5 기본 테스트
2.1 기본 테스트 작성
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Assertions.*
class CalculatorTest {
@Test
fun `덧셈 테스트`() {
// Given
val calculator = Calculator()
// When
val result = calculator.add(2, 3)
// Then
assertEquals(5, result)
}
@Test
fun `나눗셈 테스트`() {
val calculator = Calculator()
val result = calculator.divide(10, 2)
assertEquals(5, result)
}
@Test
fun `0으로 나누면 예외 발생`() {
val calculator = Calculator()
assertThrows<ArithmeticException> {
calculator.divide(10, 0)
}
}
}
2.2 테스트 라이프사이클
import org.junit.jupiter.api.*
class UserServiceTest {
companion object {
@JvmStatic
@BeforeAll
fun setupAll() {
println("모든 테스트 실행 전 1번 실행")
}
@JvmStatic
@AfterAll
fun teardownAll() {
println("모든 테스트 실행 후 1번 실행")
}
}
@BeforeEach
fun setup() {
println("각 테스트 실행 전")
}
@AfterEach
fun teardown() {
println("각 테스트 실행 후")
}
@Test
fun `테스트 1`() {
println("테스트 1 실행")
}
@Test
fun `테스트 2`() {
println("테스트 2 실행")
}
}
2.3 Assertions
import org.junit.jupiter.api.Assertions.*
@Test
fun `다양한 Assertions`() {
// 동등성
assertEquals(5, 2 + 3)
assertNotEquals(10, 2 + 3)
// 참/거짓
assertTrue(5 > 3)
assertFalse(5 < 3)
// Null 체크
val value: String? = "hello"
assertNotNull(value)
assertNull(null)
// 동일성 (같은 객체)
val obj1 = User("John")
val obj2 = obj1
assertSame(obj1, obj2)
// 컬렉션
val list = listOf(1, 2, 3)
assertTrue(list.contains(2))
assertEquals(3, list.size)
// 예외
assertThrows<IllegalArgumentException> {
throw IllegalArgumentException("오류")
}
// 여러 assertion 동시 실행
assertAll(
{ assertEquals(5, 2 + 3) },
{ assertTrue(5 > 3) },
{ assertNotNull("test") }
)
}
2.4 Parameterized Tests
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.*
class ParameterizedTestExample {
@ParameterizedTest
@ValueSource(ints = [1, 2, 3, 4, 5])
fun `양수 테스트`(number: Int) {
assertTrue(number > 0)
}
@ParameterizedTest
@ValueSource(strings = ["", " ", "\t", "\n"])
fun `빈 문자열 테스트`(input: String) {
assertTrue(input.isBlank())
}
@ParameterizedTest
@CsvSource(
"1, 2, 3",
"10, 20, 30",
"100, 200, 300"
)
fun `덧셈 테스트`(a: Int, b: Int, expected: Int) {
assertEquals(expected, a + b)
}
@ParameterizedTest
@MethodSource("provideUsers")
fun `사용자 유효성 테스트`(user: User) {
assertTrue(user.age >= 18)
}
companion object {
@JvmStatic
fun provideUsers() = listOf(
User("John", 20),
User("Jane", 25),
User("Bob", 30)
)
}
}
3. Mockito-Kotlin 소개
3.1 왜 Mockito-Kotlin인가?
Java Mockito의 문제점
// ❌ when은 Kotlin 예약어
when(userRepository.findById(1L)).thenReturn(user)
// ❌ any()는 null을 반환해서 NPE 발생
verify(userService).createUser(any())
// ❌ 타입 추론 문제
when(userRepository.save(any(User::class.java))).thenReturn(user)
Mockito-Kotlin의 해결책
// ✅ whenever로 대체
whenever(userRepository.findById(1L)).thenReturn(user)
// ✅ Kotlin nullable 지원
verify(userService).createUser(any())
// ✅ 타입 추론
whenever(userRepository.save(any())).thenReturn(user)
3.2 Import 가이드
// Mockito-Kotlin
import org.mockito.kotlin.*
// JUnit5
import org.junit.jupiter.api.*
import org.junit.jupiter.api.Assertions.*
// 또는 AssertJ 사용
import org.assertj.core.api.Assertions.*
4. Mock 객체 생성과 스터빙
4.1 Mock 객체 생성
import org.mockito.kotlin.*
class UserServiceTest {
// 방법 1: mock() 함수
private val userRepository: UserRepository = mock()
private val emailService: EmailService = mock()
// 방법 2: mock<T>() 타입 추론
private val userRepository = mock<UserRepository>()
// 방법 3: @Mock 어노테이션 (JUnit 확장 필요)
@Mock
private lateinit var userRepository: UserRepository
private lateinit var userService: UserService
@BeforeEach
fun setup() {
userService = UserService(userRepository, emailService)
}
}
4.2 Stub (스터빙)
기본 스터빙
@Test
fun `사용자 조회 테스트`() {
// Given
val userId = 1L
val expectedUser = User(id = userId, name = "John", email = "john@example.com")
whenever(userRepository.findById(userId))
.thenReturn(Optional.of(expectedUser))
// When
val result = userService.getUser(userId)
// Then
assertEquals(expectedUser, result)
}
여러 값 반환
@Test
fun `연속 호출 테스트`() {
val user1 = User(1L, "John")
val user2 = User(2L, "Jane")
whenever(userRepository.findById(any()))
.thenReturn(Optional.of(user1))
.thenReturn(Optional.of(user2))
assertEquals(user1, userService.getUser(1L))
assertEquals(user2, userService.getUser(2L))
}
예외 던지기
@Test
fun `사용자 없을 때 예외 발생`() {
// Given
whenever(userRepository.findById(any()))
.thenThrow(NotFoundException("User not found"))
// When & Then
assertThrows<NotFoundException> {
userService.getUser(999L)
}
}
조건부 스터빙
@Test
fun `조건에 따른 다른 응답`() {
whenever(userRepository.findById(1L))
.thenReturn(Optional.of(User(1L, "John")))
whenever(userRepository.findById(2L))
.thenReturn(Optional.empty())
assertNotNull(userService.getUser(1L))
assertThrows<NotFoundException> {
userService.getUser(2L)
}
}
4.3 doReturn / doThrow
@Test
fun `doReturn 사용`() {
// when-thenReturn 대신 doReturn-whenever
doReturn(Optional.of(user))
.whenever(userRepository).findById(1L)
val result = userService.getUser(1L)
assertEquals(user, result)
}
@Test
fun `doThrow 사용`() {
doThrow(RuntimeException("DB 오류"))
.whenever(userRepository).findById(any())
assertThrows<RuntimeException> {
userService.getUser(1L)
}
}
4.4 Unit 반환 함수 스터빙
@Test
fun `void 메서드 스터빙`() {
// Unit 반환 함수는 doNothing 사용
doNothing().whenever(emailService).send(any(), any())
// 또는 아무것도 하지 않음 (기본 동작)
userService.sendWelcomeEmail(user)
verify(emailService).send(user.email, "Welcome")
}
4.5 Answer 사용
@Test
fun `Answer로 동적 응답`() {
whenever(userRepository.save(any<User>())).thenAnswer { invocation ->
val user = invocation.getArgument<User>(0)
user.copy(id = 1L) // ID 할당
}
val newUser = User(name = "John", email = "john@example.com")
val savedUser = userService.createUser(newUser)
assertEquals(1L, savedUser.id)
}
5. Argument Matchers와 Captors
5.1 기본 Matchers
import org.mockito.kotlin.*
@Test
fun `Argument Matchers 사용`() {
// any() - 모든 값
whenever(userRepository.findById(any())).thenReturn(Optional.of(user))
// eq() - 특정 값
whenever(userRepository.findByEmail(eq("test@example.com")))
.thenReturn(user)
// anyOrNull() - null 포함 모든 값
whenever(userService.createUser(anyOrNull())).thenReturn(user)
// isNull() / isNotNull()
whenever(userRepository.findByEmail(isNotNull())).thenReturn(user)
}
5.2 타입별 Matchers
@Test
fun `타입별 Matchers`() {
// 문자열
whenever(userRepository.findByEmail(anyString())).thenReturn(user)
// 숫자
whenever(orderService.calculatePrice(anyInt(), anyDouble()))
.thenReturn(100.0)
// 컬렉션
whenever(userService.findUsers(anyList())).thenReturn(emptyList())
// 커스텀 타입
whenever(userRepository.save(any<User>())).thenReturn(user)
}
5.3 Argument Captors
@Test
fun `Argument Captor로 인자 캡처`() {
// Given
val userCaptor = argumentCaptor<User>()
// When
userService.createUser(UserCreateRequest("John", "john@example.com"))
// Then
verify(userRepository).save(userCaptor.capture())
val capturedUser = userCaptor.firstValue
assertEquals("John", capturedUser.name)
assertEquals("john@example.com", capturedUser.email)
}
@Test
fun `여러 번 캡처`() {
// Given
val captor = argumentCaptor<String>()
// When
emailService.send("user1@example.com", "Message 1")
emailService.send("user2@example.com", "Message 2")
// Then
verify(emailService, times(2)).send(captor.capture(), any())
val capturedEmails = captor.allValues
assertEquals(2, capturedEmails.size)
assertEquals("user1@example.com", capturedEmails[0])
assertEquals("user2@example.com", capturedEmails[1])
}
5.4 커스텀 Matchers
@Test
fun `커스텀 Matcher 사용`() {
// 이메일 형식 검증 Matcher
fun isValidEmail() = argThat<String> {
this.contains("@") && this.contains(".")
}
whenever(userRepository.findByEmail(isValidEmail()))
.thenReturn(user)
// 사용
assertNotNull(userService.findByEmail("valid@example.com"))
assertThrows<NotFoundException> {
userService.findByEmail("invalid-email")
}
}
@Test
fun `복잡한 객체 Matcher`() {
fun isAdultUser() = argThat<User> {
this.age >= 18
}
whenever(userRepository.save(isAdultUser()))
.thenReturn(user)
val adultUser = User(name = "John", age = 25)
val minorUser = User(name = "Jane", age = 15)
assertDoesNotThrow { userService.createUser(adultUser) }
}
5.5 check 함수
@Test
fun `check로 인자 검증`() {
userService.createUser(UserCreateRequest("John", "john@example.com"))
verify(userRepository).save(check { user ->
assertEquals("John", user.name)
assertTrue(user.email.contains("@"))
assertNotNull(user.createdAt)
})
}
6. Verification - 호출 검증
6.1 기본 Verification
@Test
fun `메서드 호출 검증`() {
// Given
val userId = 1L
// When
userService.getUser(userId)
// Then - 1번 호출 확인 (기본)
verify(userRepository).findById(userId)
// 정확히 1번
verify(userRepository, times(1)).findById(userId)
}
6.2 호출 횟수 검증
@Test
fun `호출 횟수 검증`() {
// 여러 번 호출
repeat(3) {
userService.getUser(1L)
}
// 정확히 3번
verify(userRepository, times(3)).findById(1L)
// 최소 2번
verify(userRepository, atLeast(2)).findById(1L)
// 최대 5번
verify(userRepository, atMost(5)).findById(1L)
// 1번 이상
verify(userRepository, atLeastOnce()).findById(1L)
}
6.3 호출 안 됨 검증
@Test
fun `메서드가 호출되지 않음을 검증`() {
userService.getUser(1L)
// 특정 메서드는 호출 안 됨
verify(userRepository, never()).deleteById(any())
// 아무 메서드도 호출 안 됨
verifyNoInteractions(emailService)
// 더 이상 호출 없음
verify(userRepository).findById(1L)
verifyNoMoreInteractions(userRepository)
}
6.4 호출 순서 검증
@Test
fun `호출 순서 검증`() {
// When
userService.createAndNotify(request)
// Then - 순서대로 호출 확인
inOrder(userRepository, emailService) {
verify(userRepository).save(any())
verify(emailService).send(any(), any())
}
}
6.5 Timeout 검증
@Test
fun `타임아웃 내 호출 검증`() {
// 비동기 작업
userService.createUserAsync(request)
// 2초 내에 호출되어야 함
verify(userRepository, timeout(2000)).save(any())
}
6.6 실전 예제
@Test
fun `사용자 생성 시 이메일 발송 확인`() {
// Given
val request = UserCreateRequest("John", "john@example.com")
val savedUser = User(1L, "John", "john@example.com")
whenever(userRepository.save(any<User>())).thenReturn(savedUser)
doNothing().whenever(emailService).send(any(), any())
// When
userService.createUser(request)
// Then
verify(userRepository).save(any<User>())
verify(emailService).send(
eq("john@example.com"),
argThat { it.contains("환영") }
)
verifyNoMoreInteractions(userRepository, emailService)
}
@Test
fun `사용자 삭제 시 연관 데이터도 삭제`() {
// Given
val userId = 1L
val user = User(userId, "John", "john@example.com")
whenever(userRepository.findById(userId)).thenReturn(Optional.of(user))
// When
userService.deleteUser(userId)
// Then - 순서대로 실행 확인
inOrder(userRepository, orderRepository, emailService) {
verify(userRepository).findById(userId)
verify(orderRepository).deleteByUserId(userId)
verify(userRepository).delete(user)
verify(emailService).sendGoodbyeEmail(user.email)
}
}
7. Spring Boot 통합 테스트
7.1 @SpringBootTest
@SpringBootTest
class UserServiceIntegrationTest {
@Autowired
private lateinit var userService: UserService
@Autowired
private lateinit var userRepository: UserRepository
@BeforeEach
fun setup() {
userRepository.deleteAll()
}
@Test
fun `사용자 생성 통합 테스트`() {
// Given
val request = UserCreateRequest("John", "john@example.com")
// When
val createdUser = userService.createUser(request)
// Then
assertNotNull(createdUser.id)
assertEquals("John", createdUser.name)
// DB 확인
val foundUser = userRepository.findById(createdUser.id!!)
assertTrue(foundUser.isPresent)
}
}
7.2 @WebMvcTest (Controller 테스트)
@WebMvcTest(UserController::class)
class UserControllerTest {
@Autowired
private lateinit var mockMvc: MockMvc
@MockBean
private lateinit var userService: UserService
@Autowired
private lateinit var objectMapper: ObjectMapper
@Test
fun `사용자 조회 API 테스트`() {
// Given
val userId = 1L
val user = UserDto(userId, "John", "john@example.com")
whenever(userService.getUser(userId)).thenReturn(user)
// When & Then
mockMvc.perform(get("/api/users/$userId"))
.andExpect(status().isOk)
.andExpect(jsonPath("$.id").value(userId))
.andExpect(jsonPath("$.name").value("John"))
.andExpect(jsonPath("$.email").value("john@example.com"))
}
@Test
fun `사용자 생성 API 테스트`() {
// Given
val request = UserCreateRequest("John", "john@example.com")
val createdUser = UserDto(1L, "John", "john@example.com")
whenever(userService.createUser(any())).thenReturn(createdUser)
// When & Then
mockMvc.perform(
post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request))
)
.andExpect(status().isCreated)
.andExpect(header().exists("Location"))
.andExpect(jsonPath("$.id").value(1L))
.andExpect(jsonPath("$.name").value("John"))
}
@Test
fun `사용자 없을 때 404 반환`() {
// Given
whenever(userService.getUser(any()))
.thenThrow(NotFoundException("User not found"))
// When & Then
mockMvc.perform(get("/api/users/999"))
.andExpect(status().isNotFound)
}
}
7.3 @DataJpaTest (Repository 테스트)
@DataJpaTest
class UserRepositoryTest {
@Autowired
private lateinit var userRepository: UserRepository
@Autowired
private lateinit var testEntityManager: TestEntityManager
@Test
fun `이메일로 사용자 조회`() {
// Given
val user = User(name = "John", email = "john@example.com")
testEntityManager.persist(user)
testEntityManager.flush()
// When
val foundUser = userRepository.findByEmail("john@example.com")
// Then
assertNotNull(foundUser)
assertEquals("John", foundUser?.name)
}
@Test
fun `이메일 존재 여부 확인`() {
// Given
val user = User(name = "John", email = "john@example.com")
testEntityManager.persist(user)
testEntityManager.flush()
// When & Then
assertTrue(userRepository.existsByEmail("john@example.com"))
assertFalse(userRepository.existsByEmail("notexist@example.com"))
}
}
7.4 @MockBean vs @Mock
// @MockBean - Spring 컨텍스트에 Mock 주입
@SpringBootTest
class ServiceWithMockBeanTest {
@MockBean // Spring Bean으로 등록
private lateinit var userRepository: UserRepository
@Autowired // 실제 Bean 주입 (Mock이 주입됨)
private lateinit var userService: UserService
@Test
fun test() {
whenever(userRepository.findById(1L)).thenReturn(Optional.of(user))
// userService는 Mock된 userRepository 사용
}
}
// @Mock - 순수 Mockito Mock
class ServiceWithMockTest {
@Mock // Mockito Mock
private lateinit var userRepository: UserRepository
@InjectMocks // Mock 주입
private lateinit var userService: UserService
@BeforeEach
fun setup() {
MockitoAnnotations.openMocks(this)
}
@Test
fun test() {
whenever(userRepository.findById(1L)).thenReturn(Optional.of(user))
}
}
7.5 TestConfiguration
@TestConfiguration
class TestConfig {
@Bean
@Primary // 테스트용 Bean이 우선
fun testEmailService(): EmailService {
return object : EmailService {
override fun send(to: String, message: String) {
println("테스트 이메일 발송: $to - $message")
}
}
}
}
@SpringBootTest
@Import(TestConfig::class)
class UserServiceTest {
@Autowired
private lateinit var userService: UserService
@Autowired
private lateinit var emailService: EmailService
@Test
fun `이메일 발송 테스트`() {
userService.sendWelcomeEmail(user)
// TestConfig의 EmailService 사용됨
}
}
8. 실전 테스트 패턴
8.1 Given-When-Then 패턴
@Test
fun `사용자 생성 테스트`() {
// Given - 테스트 준비
val request = UserCreateRequest("John", "john@example.com")
val savedUser = User(1L, "John", "john@example.com")
whenever(userRepository.existsByEmail(request.email))
.thenReturn(false)
whenever(userRepository.save(any<User>()))
.thenReturn(savedUser)
// When - 테스트 실행
val result = userService.createUser(request)
// Then - 검증
assertNotNull(result)
assertEquals(1L, result.id)
assertEquals("John", result.name)
verify(userRepository).save(any<User>())
verify(emailService).sendWelcomeEmail(request.email)
}
8.2 Test Fixture 패턴
abstract class TestBase {
protected fun createUser(
id: Long = 1L,
name: String = "John",
email: String = "john@example.com",
age: Int = 25
): User {
return User(
id = id,
name = name,
email = email,
age = age,
createdAt = LocalDateTime.now()
)
}
protected fun createUserRequest(
name: String = "John",
email: String = "john@example.com"
): UserCreateRequest {
return UserCreateRequest(name, email)
}
}
class UserServiceTest : TestBase() {
@Test
fun `Fixture 사용 테스트`() {
// 기본값으로 생성
val user = createUser()
// 일부만 커스터마이징
val customUser = createUser(name = "Jane", age = 30)
val request = createUserRequest(email = "custom@example.com")
}
}
8.3 Object Mother 패턴
object UserMother {
fun createDefault() = User(
id = 1L,
name = "John Doe",
email = "john@example.com",
age = 25,
role = UserRole.USER
)
fun createAdmin() = User(
id = 2L,
name = "Admin",
email = "admin@example.com",
age = 30,
role = UserRole.ADMIN
)
fun createMinor() = User(
id = 3L,
name = "Minor",
email = "minor@example.com",
age = 15,
role = UserRole.USER
)
}
class UserServiceTest {
@Test
fun test() {
val user = UserMother.createDefault()
val admin = UserMother.createAdmin()
}
}
8.4 Builder 패턴
class UserBuilder {
private var id: Long? = 1L
private var name: String = "John"
private var email: String = "john@example.com"
private var age: Int = 25
private var role: UserRole = UserRole.USER
fun id(id: Long) = apply { this.id = id }
fun name(name: String) = apply { this.name = name }
fun email(email: String) = apply { this.email = email }
fun age(age: Int) = apply { this.age = age }
fun role(role: UserRole) = apply { this.role = role }
fun build() = User(
id = id,
name = name,
email = email,
age = age,
role = role
)
}
// 사용
@Test
fun test() {
val user = UserBuilder()
.name("Jane")
.age(30)
.role(UserRole.ADMIN)
.build()
}
8.5 예외 테스트 패턴
@Test
fun `예외 메시지 검증`() {
whenever(userRepository.findById(any()))
.thenReturn(Optional.empty())
val exception = assertThrows<NotFoundException> {
userService.getUser(999L)
}
assertEquals("사용자를 찾을 수 없습니다: 999", exception.message)
}
@Test
fun `예외 원인 검증`() {
val cause = RuntimeException("DB 연결 실패")
whenever(userRepository.findById(any()))
.thenThrow(cause)
val exception = assertThrows<ServiceException> {
userService.getUser(1L)
}
assertEquals(cause, exception.cause)
}
8.6 비동기 테스트
@Test
fun `비동기 작업 테스트`() {
// Given
val latch = CountDownLatch(1)
var result: User? = null
// When
userService.createUserAsync(request) { user ->
result = user
latch.countDown()
}
// Then
assertTrue(latch.await(2, TimeUnit.SECONDS))
assertNotNull(result)
assertEquals("John", result?.name)
}
8.7 실전 예제: 주문 서비스 테스트
class OrderServiceTest {
private val orderRepository: OrderRepository = mock()
private val productRepository: ProductRepository = mock()
private val paymentService: PaymentService = mock()
private val inventoryService: InventoryService = mock()
private val orderService = OrderService(
orderRepository,
productRepository,
paymentService,
inventoryService
)
@Test
fun `주문 생성 성공 테스트`() {
// Given
val request = OrderCreateRequest(
userId = 1L,
productId = 100L,
quantity = 2,
paymentInfo = PaymentInfo("card", "1234-5678-9012-3456")
)
val product = Product(100L, "상품명", 10000, stock = 10)
val payment = Payment(1L, 20000, PaymentStatus.COMPLETED)
val order = Order(1L, 1L, 100L, 2, payment.id)
whenever(productRepository.findById(100L))
.thenReturn(Optional.of(product))
whenever(inventoryService.checkStock(100L, 2))
.thenReturn(true)
whenever(paymentService.processPayment(any()))
.thenReturn(payment)
whenever(orderRepository.save(any<Order>()))
.thenReturn(order)
// When
val result = orderService.createOrder(request)
// Then
assertNotNull(result)
assertEquals(1L, result.id)
// 호출 순서 검증
inOrder(
productRepository,
inventoryService,
paymentService,
orderRepository
) {
verify(productRepository).findById(100L)
verify(inventoryService).checkStock(100L, 2)
verify(paymentService).processPayment(any())
verify(inventoryService).decreaseStock(100L, 2)
verify(orderRepository).save(any<Order>())
}
}
@Test
fun `재고 부족 시 주문 실패`() {
// Given
val request = OrderCreateRequest(1L, 100L, 100, PaymentInfo("card", "1234"))
val product = Product(100L, "상품명", 10000, stock = 5)
whenever(productRepository.findById(100L))
.thenReturn(Optional.of(product))
whenever(inventoryService.checkStock(100L, 100))
.thenReturn(false)
// When & Then
assertThrows<InsufficientStockException> {
orderService.createOrder(request)
}
// 결제 시도하지 않음 확인
verify(paymentService, never()).processPayment(any())
verify(orderRepository, never()).save(any())
}
@Test
fun `결제 실패 시 롤백`() {
// Given
val request = OrderCreateRequest(1L, 100L, 2, PaymentInfo("card", "1234"))
val product = Product(100L, "상품명", 10000, stock = 10)
whenever(productRepository.findById(100L))
.thenReturn(Optional.of(product))
whenever(inventoryService.checkStock(100L, 2))
.thenReturn(true)
whenever(paymentService.processPayment(any()))
.thenThrow(PaymentFailedException("결제 실패"))
// When & Then
assertThrows<PaymentFailedException> {
orderService.createOrder(request)
}
// 재고 차감되지 않음 확인
verify(inventoryService, never()).decreaseStock(any(), any())
verify(orderRepository, never()).save(any())
}
}
마치며
핵심 요약
Mockito-Kotlin의 장점
- Kotlin 친화적: whenever 대신 when 사용 불가 문제 해결
- Null Safety: any() 등이 Kotlin null 타입과 호환
- 타입 추론: 제네릭 타입 추론 개선
- DSL 스타일: 더 읽기 쉬운 테스트 코드
테스트 작성 원칙
- ✅ Given-When-Then 구조 유지
- ✅ 하나의 테스트는 하나의 기능만
- ✅ 테스트 이름은 명확하게 (한글 가능)
- ✅ Fixture/Builder 패턴으로 재사용성 높이기
- ✅ 필요한 것만 Mock, 나머지는 실제 객체
주의사항
- ⚠️ 과도한 Mock은 테스트 신뢰도 저하
- ⚠️ 구현이 아닌 행위를 테스트
- ⚠️ Private 메서드는 테스트하지 않기
- ⚠️ 테스트 코드도 리팩토링 필요
Mock 라이브러리 선택
Mockito-Kotlin: Java 프로젝트 호환, 안정적
MockK: Kotlin 전용, 더 많은 기능
MockK 간단 비교
// Mockito-Kotlin
val mock = mock<UserRepository>()
whenever(mock.findById(1L)).thenReturn(user)
// MockK
val mock = mockk<UserRepository>()
every { mock.findById(1L) } returns user
다음 학습 방향
이제 Kotlin 프로젝트의 전체 개발 사이클을 다룰 수 있습니다:
- ✅ 기본 문법과 클래스
- ✅ Null Safety
- ✅ 함수형 프로그래밍
- ✅ Spring Boot 통합
- ✅ 예외 처리
- ✅ 테스트 작성
더 나아가기
- Kotlin Coroutine과 비동기 처리
- Kotlin DSL 만들기
- Kotlin Multiplatform
- Ktor 프레임워크
함께 보면 좋은 글
▶ Kotlin과 Spring Boot 완벽 통합 | Annotations, DI, 트랜잭션
▶ Kotlin 예외 처리 완벽 가이드 | runCatching, Result 타입
▶ Kotlin 제어 구조와 타입 시스템 | when, Sealed Class
참고 자료
- Mockito-Kotlin GitHub: https://github.com/mockito/mockito-kotlin
- JUnit5 공식 문서: https://junit.org/junit5/docs/current/user-guide/
- Spring Boot Testing: https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.testing
- Baeldung Kotlin Testing: https://www.baeldung.com/kotlin/mockito-kotlin
- MockK: https://mockk.io/
반응형
'개발 > java basic' 카테고리의 다른 글
| Kotlin 함수 완벽 가이드 | fun으로 시작하는 간결한 코드 (1) | 2025.12.11 |
|---|---|
| Kotlin 시작하기 | Java 개발자를 위한 첫 번째 Kotlin 코드 (0) | 2025.12.10 |
| Kotlin 예외 처리 완벽 가이드 | try-catch, runCatching, Result 타입 (1) | 2025.12.06 |
| Kotlin과 Spring Boot 완벽 통합 가이드 | Annotations, DI, 실전 패턴 (0) | 2025.12.05 |
| Kotlin 제어 구조와 타입 시스템 완벽 가이드 | when, if expression, Enum, Sealed Class (0) | 2025.12.04 |
Comments
