Kotest, MockK 에 관하여

wonseok·2023년 4월 2일
0

본 포스팅은 고품격 Kotlin 개발: 테스트 코드를 우아하게 작성하는 방법을 보고 정리한 내용입니다.

1️⃣ 단위 테스트

  • 코틀린으로 테스트 코드를 작성하는 것은 재미있지만, JUnit 및 Mockito 등과 같은 기존 자바 라이브러리 및 프레임워크를 사용하여 테스트하기 때문에, 동시에 코틀린스러운 테스트 코드를 작성하는 것은 어렵습니다.
  • 코틀린의 특징 중 하나인 읽기 쉽고 간결하다는 장점을 활용하여 테스트 코드를 작성하고 싶습니다.

  • 역따옴표(`)로 묶인 함수 이름은 한글과 공백으로 표현할 수 있다.
  • 예외 테스트는 JUnit5의 assertThrowsassertDoesNotThrow와 같은 Kotlin 함수를 사용하면 더 간결합니다.

테스트 팩토리

테스트 픽스처 = 테스트를 위한 전제조건

  • 테스트 픽스처를 반환하는 팩토리 함수를 만들 수 있습니다. (두고두고 유용하게 사용 가능)
  • Kotlin의 기본 인자를 사용하면 빌더 패턴처럼 다양한 테스트를의 픽스처를 생성할 수 있습니다.
  • 기본 인자와 이름 붙인 인자를 적절히 사용하면 테스트하려는 관심사를 드러내는 데 사용할 수 있습니다.

팩토리 메서드만 보고 어떤 과제를 생성해내는 지 눈에 보입니다.
물론 테스트와 관련없는 인자에는 합리적인 기본값(default)을 사용하는 것이 중요합니다.

테스트 확장 함수

코틀린의 확장함수를 사용하여 값을 더 쉽게 표현할 수 있습니다.
이렇게 하면 해당 기능이 어떻게 작동하는지 쉽게 확인할 수 있습니다.

이번에는 확장함수를 이용하여 검증하고자 하는 부분의 가독성을 높여 보았습니다.
테스트 함수의 이름과 테스트 이름이 동일하게 보입니다.

테스트 어설션

소프트 어설션은 일부 어설션이 실패하더라도 테스트가 즉시 중단되지 않고 끝까지 검증됨을 의미합니다.
소프트 어설션은 어떤 프로퍼티 때문에 실패했는 지 바로 알아채기가 어렵습니다.

소프트 어설션을 사용하여 인스턴스의 여러 프로퍼티를 비교할 때, 소프트 어설션 대신 인스턴스를 데이터 클래스로 만들고 비교하는 것을 고려할 수 있습니다.

이제 테스트 실패의 원인이 된 프로퍼티가 무엇인지 확인할 수 있습니다.

😀 많은 개발자들이 JUnit의 Assertions 보다 읽기 쉬운 AssertJ의 Assertions를 선호합니다.
😵 AssertJ의 Assertions를 가져올 때, 어떤 패키지의 어설션을 가져올 지 몰라 혼란을 겪거나 잘못된 import로 인하여 테스트에 실패한 적이 있으신가요?
🫨 아니면 isNull, isTrue와 같은 유효성 검사를 위해 함수를 사용해야 할지, 프로퍼티를 사용해야 할지 헷갈리시지 않으셨나요?
😵‍💫소프트 어설션을 사용하면 작성해야 하는 코드의 양이 기하급수적으로 늘어납니다.

Kotest 어설션

shouldBeNull 혹은 shouldNotBeNull은 스마트 캐스팅도 지원합니다.
한 번 스마트 캐스팅이 되었다면, null에 대해서 굳이 다루지 않아도 되겠지요.

사소해 보일 수 있지만, 여러 사람이 함께 하는 작업 코드에는 자유로운 표현 방식보다 제한적인 표현 방식이 필요할 수 있습니다.

  • Kotest 어설션은 간결하고 기존 JUnit 5와 혼용할 수 있습니다.
  • 스마트 캐스트와 같은 Kotlin 기능을 지원받을 수 있습니다.
  • 여러 사람과 함께 작업하는 코드에는 자유로운 표현 방식보다 제한된 표현 방식이 필요할 수 있습니다.

Kotest

  • 코틀린에는 많은 테스트 라이브러리가 있고, 그 중 Kotest가 가장 많이 사용됩니다.
  • Kotest는 다양한 스타일의 테스트 레이아웃을 제공합니다.
  • 이중에서 StringSpec을 사용하면 굳이 어노테이션을 사용하지 않고 깔끔하게 테스트 코드를 표현할 수 있습니다.

2️⃣ 모의 테스트

Mockito

  • Mockito는 상속 또는 바이트 버디(Byte Buddy)를 사용하여 Mock 객체를 생성합니다.
  • 다만, Mockito는 final 클래스, final 메서드를 mock으로 만들지 못합니다.
  • Kotlin의 확장 함수는 Java의 정적 메서드이며 Mockito는 이를 스텁할 수 없습니다.
  • 그리고, 최상위 함수는 특정 클래스에 속하지 않기 때문에 스텁할 수 없습니다.
  • 이러한 점을 볼 때, Mockito를 사용하는 것에는 많은 단점이 있는 것 같습니다.

MockK

그래서 이번에 소개해드릴 라이브러리는 MockK라는 라이브러리입니다.

  • MockK는 DSL 기반의 Kotlin 모의 라이브러리입니다.
  • 코드 기반, 어노테이션 기반 등 대부분 Mockito와 동일한 사용 방식을 가지고 있습니다.

MockK를 이용한 연쇄 스터빙

  • MockK로 정적 함수를 모의하지 않고 확장 함수를 스텁하여 내부의 멤버 함수를 스텁하는 방법이 있습니다.
  • 실제로 확장함수를 스텁하는 것이 아니라, 내부의 호출되는 메서드를 스텁한다는 점에서 주의해야 합니다.
  • 그렇지만 도구가 이런 기능을 제공한다고 해서 항상 사용해야 하는 것은 아닙니다.
  • 어쩌면 가짜 객체를 사용하는 것도 좋은 방법이 될 수 있겠습니다.

Kotest를 이용한 모의 테스트

  • Kotest에는 BDD를 지원하는 BehaviorSpec, DescriberSpec이 존재합니다.
  • 테스트가 하나의 문서로서 더욱 더 풍부해지는 경험을 할 수 있습니다.
  • 이 때, 위 두 테스트는 중첩 테스트인데, 중첩 테스트의 경우 테스트 수명 주기를 이해하는 것이 매우 중요합니다.

💡 여기서 잠깐, BDD란?

BDD(Behavior Driven Development)
TDD에서 파생된 개발 방법으로 시나리오를 기반으로 테스트 케이스를 작성하기 때문에 테스트를 이해하기 쉽습니다.
Given(주어진 환경에서)-When(이렇게 실현이 된다면)-Then(다음과 같은 결과가 나와야 한다) 구조를 기본 패턴으로 사용합니다.

Kotest의 수명 주기


실제로 이렇게 Given, When, Then으로 나뉘어진 것을 볼 수 있네요.
전체적으로 이 Given, When, Then을 이루는 모든 것을 beforeSpec 또는 afterSpec으로 리슨을 할 수 있습니다.

이 Given, When, Then만 놓고 보면, 이것들을 Container로 부르고, 이 때에는 beforeContainer, afterContainer 라는 메서드로 수명주기를 관리할 수 있습니다.

마지막으로 Then은 beforeEach, afterEach라는 메서드로 수명주기를 관리할 수 있습니다.

이렇게 수명주기에 대해서 알고나면, 어느 시점에 트랜잭션을 롤백해야할 지, 혹은 어느 시점에 mock 객체를 초기화해야할 지 등에 대한 경계를 확실히 알 수 있게 됩니다.

Junit 5 + Mockito


Junit 5와 Mockito를 사용한 테스트 코드입니다.
Junit 5에서는 Given, When, Then을 표현하고자 Nested 클래스를 사용하였으며,
Mockito를 사용하다 보니깐, when과 같은 함수들은 코틀린의 예약어이기 때문에 백틱(역따옴표)이 붙은 것을 확인할 수 있습니다.

Kotest + MockK

이번에는 Kotest와 MockK를 사용한 코드입니다.
아까 봤던 JUnit 5와 Mockito를 사용한 코드보다 훨씬 더 깔끔함을 느낄 수 있습니다.

Kotest의 격리 모드

  • Kotest는 SingleInstance, InstancePerLeaf, InstancePerTest 와 같은 세 가지 값이 있습니다.
  • 기본은 SingleInstance이며 테스트 상황에 맞게 격리 모드를 선택하면 됩니다.
  • 예를 들어, 아까 보셨던 테스트 클래스에서 테스트 클래스 전체에 모의 객체를 만드는 것이 비용이 많이 든다면, SingleInstance를 선택하고 mock객체를 한 번 초기화한 다음에, 그다음부터는 계속 clearMocks()을 호출하는 것이 더 나을 수 있습니다.

역시나 이런 설정들은 프로젝트 구성에서 또는 시스템 속성을 통해 전역적으로 설정할 수 있습니다.

3️⃣ 통합 테스트

  • Spring 5.2부터 @TestConstructor를 사용하면 생성자를 통한 주입이 가능합니다.
  • Kotest는 Spring의 통합 테스트를 지원하기 위해 SpringExtension을 제공합니다.
  • 별도의 어노테이션 없이 생성자 주입이 가능하며, SpringExtension을 통해서 트랜잭션 롤백이 가능합니다.

인수 테스트

💡 인수 테스트란 사용자 관점에서 기능이 올바르게 작동하는지 확인하는 테스트입니다.
😀 일반적으로 사용자 스토리에 따라 Given-When-Then 스타일로 작성됩니다.
🤯 이런 인수 테스트는 연관 관계가 복잡할수록 테스트 픽스처를 만들기가 더 어려워집니다.

인수 테스트를 위한 DSL

  • 연관 관계를 도메인에 특화된 언어로 표현하고 필요한 데이터가 생성되도록 할 수 있습니다.
  • 코드를 처음 접하는 사람들의 도메인 학습에 많은 도움이 됩니다.
  • 만드는 사람이 수고로우면 쓰는 사람이 편하고 만드는 사람이 편하면 쓰는 사람이 수고롭다는 구절이 떠오릅니다.

4️⃣ 결론

  • 테스트의 중요성이 높아짐에 따라 개발 과정에서 많은 테스트 코드가 작성됩니다.
  • 어쩌면 구현 코드보다 테스트 코드를 더 많이 작성할 수도 있습니다.
  • 기존에 JUnit 5를 잘 사용해왔다면, 새로운 도구로 전부 전환하는 것이 아닌, 어설션 혹은 BDD 방식부터 차근차근 도입하는 것을 추천합니다.
  • 무조건 Kotest, MockK를 채택하기 보다는 팀의 숙련도와 상황을 고려해야 합니다.
  • DRY 패턴과 DAMP 패턴 코드 사이에서 균형을 찾는 것이 매우 중요합니다.

레퍼런스

kotest-assertions-core모듈에서 제공하는 Matcher
참고한 블로그

적용 결과

0개의 댓글