-
[Kotlin] Kotest, mockkBackend/공부,개념 2023. 9. 3. 17:50반응형
코틀린 스타일 테스트코드
기존에 사용하던 Junit, AssertJ, Mockito를 이용해서도 테스트코드를 작성할 수 있지만, 중괄호를 활용한 코틀린 dsl 스타일을 적용하기 쉽지 않은데 kotest와 mockk(코틀린에서 사용하는 mocking라이브러리)를 이용하면 Mocking이나 assertion 과정에서 Kotlin dsl이나 infix를 사용해 조금 더 코틀린스럽게 테스트코드를 작성할 수 있다.
Kotest
코틀린 진영에서 가장 많이 사용되는 테스트 프레임워크
코틀린 DSL을 활용해 테스트코드를 작성할 수 있다.
- 다양한 테스트 레이아웃(StringSpec, FunSpec, BehaviorSpec 등 ) 제공
- Kotlin DSL 스타일의 Assertion 기능 제공
# kotlin dsl이란 Domain Specific Language : 특정 도메인에 국한해 사용하는 언어 범용 프로그래밍 언어인 C, C++ 등을 가지고 알고리즘을 푼다. SQL언어는 DB (특정영역, 도메인)에 초점을 맞추고 기능을 제한 특징 - 선언적 문법 ( <-> 명령적 ) - 깔끔한 코드작성이 가능하다. ㄴ 해당 코드가 어떤 로직을 가지고 작업할지 명확하게 이해할 수 있다. kotlin dsl : https://w97ww.tistory.com/119
Kotest Testing Styles
- https://kotest.io/docs/framework/testing-styles.html
- 다양한 Spec이 존재해서 상황에 따라 사용하면 될 듯하다.
- 나는 보통 가볍게 테스트할 때 FunSpec, BehaviorSpec(BDD형태) 를 많이 쓴다.
보통 이런 모양을 가지고 있다.
class Tests: XxxSpec({ keyword("테스트코드 설명") { // keyword는 Spec에 따라서 // test, "string", should, describe, given, when, then 등 다양하다. } xkeyword("테스트 비활성화"){ // keyword앞에 x가 붙으면 비활성화가 된다. } })
sample 1) FunSpec
class MyTests : FunSpec({ test("addCalculator - 두 파라미터를 더한 값이 반환 된다.") { val result = addCalculator(1, 2) result shouldBe 3 } }) fun addCalculator(a: Int, b: Int): Int = a + b
test 부분에 테스트의 설명을 적고, 괄호 안 람다 식에 필요한 assertion문을 넣어 테스트를 하면 된다.
보통 의존성 주입이 필요 없는 도메인 클래스나 도메인 서비스 메소드를 테스트할 때 많이 사용했다.
sample 2) BehaviorSpec
class MyTests : BehaviorSpec({ given("a broomstick") { // Given 도 가능 and("a witch") { // And `when`("The witch sits on it") { // When and("she laughs hysterically") { // And then("She should be able to fly") { // Then // test code } } } } } })
BDD 스타일의 테스트코드를 작성할 때 유용하다. `when`이 예약어라서 사용할 때 \`when`으로 써주어야 해서, 키워드를 대문자로 사용하는 게 편할 수도 있다. And를 이용해서 이어 쓸 수 있고 xGiven, xwhen처럼 x를 붙이면 비활성화할 수 있다.
sample 3) AnnotationSpec
class AnnotationSpecExample : AnnotationSpec() { @BeforeEach fun beforeTest() { println("Before each test") } @Test fun test1() { 1 shouldBe 1 } @Test fun test2() { 3 shouldBe 3 } }
Junit 스타일의 어노테이션 방식을 똑같이 사용할 수 있는 AnnotationSpec도 있다.
수명주기 / 전후 처리
BehaviorSpec같은 중첩 테스트에서는 테스트 수명주기를 이해하고 사용하는 것이 좋다.
Given, When, Then을 만들다 보면 테스트가 중첩으로 생성되기 때문에, 이 수명주기에 따라서 전역으로 관리하거나 mockk 객체를 처리하면 스코프 안에서 얼마나 반복되어 실행되는지, 모킹의 범위가 얼마인지, 어느 시점에 mockk 객체를 초기화해야 될지 등을 알 수 있다.
beforeContainer
,afterContainer
- Given으로 묶이는 그 아래까지 포함한 영역
beforeEach
,afterEach
- Then영역
beforeTest
,afterTest
beforeAny,
afterAny
beforeSpec
,afterSpec
- 테스트 영역 전체를 Spec이라고 한다
- XxxSpec을 구현하는
({})
영역
- 등..
Assertion
- https://kotest.io/docs/assertions/assertions.html
- 보통
shouldXX
형태의 assertion 메소드를 이용해서 검증한다.
io.kotest.matchers
패키지에 종류별로 아주 많이 있다.- shouldBe, shouldBeNot
- shouldHaveSize, shouldContain, ShouldBeGreaterThan ,,
- shouldThrow<>
- 등등
딱히 정해진건 없으니 패키지를 찾아보거나 많이 사용해 보면서 적절한 assertions을 쓰면 될 것 같다.
예를 들면
list.size() shouldBe 2
보단list shouldHaveSize 2
를 쓴다거나, 리스트의 아이템을 직접 조회하며 assert문을 쓰지 않고shouldBeSortedWith(Comparator)
를 쓴다거나,,예외처리에 대한 테스트를 할 때
shouldThrow
로 검증을 할 수도 있고,runCatching
으로 예외를 잡아서 그 결괏값을 다른 assert문으로 검증하는 것 도 괜찮은 것 같다.
Mockk
kotests와 같이 쓸 수 있는 유용한 모킹 라이브러리이다. jUnit을 사용할 때는 mockito를 주로 썼다면, 코틀린을 쓸 때는 mockk를 쓰면 보통 쓰는 편이다.
거의 비슷해서 각자 장/단점을 비교해서 취향껏 선택할 수 있을 정도의 차이인 것 같지만, kotlin dsl 형태로 되어있는 것이나 좀 더 코틀린스러운 테스트코드를 짤 수 있는 느낌이다. 더 자세한 특징은.. 생략 ㅎㅎ
1. 객체 생성
val aService = mockk<AService>()
val aService: AService = mockk()
2. Matcher 사용
참고 링크 : https://mockk.io/#matchers
every { aService.something() } returns SomeObject() every { aService.something() } throws Exception() justRuns { aService.something() } // 결과값이 없는 메소드 단순 호출만 mocking
3. 검증
검증은 보통 kotest의 assertion으로 하거나,
verify
메소드를 사용해서 mocking 한 클래스의 특정 메소드가 호출되었는지 동작만을 검증한다.이때 shouldBe 같은 assert문이 아닌 verify로 메서드가 호출되는지를 검증할 때, 모든 테스트에 걸쳐서 호출 횟수를 세기 때문에 테스트코드의 호출 순서에 따라서 verify문이 실패할 수 있다. (테스트끼리 독립적이지 않다)
그래서 테스트마다 mock을 clear 하고 다시 mocking을 해줘야 하는데 beforeEach, afterTest 등을 이용해 수명주기에 따라서 해당 동작을 전역적으로 관리할 수 있다.
이 부분 때문에 kotest의 수명주기를 잘 알아두어야 테스트코드를 더 잘 작성할 수 있을 듯하다.
mock vs stub
Test Double
- 테스트 목적으로 실제 객체 대신 사용되는 모든 종류의 가상 객체
mock vs stub
아주 살짝 비교를 해본다면
- stub은 실제로 인스턴스화하여 실제로 동작하는 것처럼 만드는 객체
- mock은 호출에 대한 기대를 명세하고, 그 내용에 따라 동작하도록 프로그래밍된 객체.상태 검증 vs 행위 검증
mock과 stub을 비교할 때 상태 검증, 행위 검증이라는 말을 쓰는데 stub은 메서드가 수행된 뒤에 결과 값의 상태를 확인한다면, mock은실제로 동작을 수행했는지 행위만 검증한다.
예제 코드
class AServiceTests : BehaviorSpec({ val dBService: DBService = mockk() val aService = AService( dBService ) Given("id가 주어지고") { val id = "1" When("data가 존재하면") { every { dBService.get(id)} returns Data("1", "content") aService.request(RequestDto("1", "new content")) Then("modify가 호출된다.") { verify(exactly = 1) { dBService.modify(any()) } verify(exactly = 0) { dBService.save(any()) } } } When("data가 존재하지 않으면") { every { dBService.get(id)} returns null aService.request(RequestDto("1", "content")) Then("save가 호출된다.") { verify(exactly = 0) { dBService.modify(any()) } verify(exactly = 1) { dBService.save(any()) } } } } // 전역적으로 clear, mocking할 만한 메소드들은 수명주기 메소드와 함께 설정 beforeTest{ justRun { dBService.modify(any())} justRun { dBService.save(any()) } } afterTest { clearMocks(dBService) } })
요즘에 헥사고날 패턴의 장점을 느낀 건 특정 port를 mocking 해두고 호출되었는지 정도만 검증하면 다른 클래스에 독립적이게 원하는 기능만 테스트할 수 있어서 좋았던 것 같다.
참고할 링크
출처
- kotest https://techblog.woowahan.com/5825/
- 수명주기 관련 https://velog.io/@effirin/Kotest%EC%99%80-BDD-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C-%EC%9E%91%EC%84%B1%ED%95%98%EA%B8%B0-%EC%A0%84-%EC%9D%BD%EC%96%B4%EB%91%90%EB%A9%B4-%EC%A2%8B%EC%9D%84-%EC%9E%90%EB%A3%8C%EB%93%A4
- mock vs stub https://azderica.github.io/00-test-mock-and-stub/
'Backend > 공부,개념' 카테고리의 다른 글
[Kotlin] DSL (Domain Specific Language) 이란 (0) 2023.07.18 [Monitoring] APM, Spring Boot Actuator, micrometer (0) 2023.05.24 Local Cache, Rate Limiter (0) 2023.04.15 헥사고날 아키텍처 공부중.. (0) 2023.03.06 [Spring] Spring Data (0) 2023.02.27