ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Kotest 예제와 함께 테스트 코드 살펴보기
    📖 개발 공부/kotlin 2024. 2. 4. 16:09

    테스트 코드를 작성하는 이유

    나는 크게 세가지 이유로 테스트 코드를 작성한다.

     

    1. 내가 개발한 기능들이 제대로 잘 동작하는지 확인하기 위함.
    2. 코드에 변경이 생겼을 때, 기존 기능에 영향을 주는지 여부를 확인하기 위함.
      이를 통해 예상하지 못한 버그 발생을 사전에 찾아내고, 배포 시의 불안함을 줄일 수 있다.
    3. 테스트 코드를 통해 실제 코드의 작동 방식을 더 빠르게 이해시키기 위함.
      정적인 문서는 쉽게 방치될 수 있다. (유경험자..) 따라서 테스트 코드는 변경된 기능들을 최신화할 수 밖에 없는! 유일(?)한 방법이라고 생각한다.
      또한 문서라고 하믄, 가독성이 충분히 있어야 한다. 가독성 있는 테스트 코드는 정말 도움되는 문서라고 할 수 있다.

     

    테스트 코드 용어 살펴보기

    sut

    System Under Test - 테스트할 대상을 의미한다.
    만약 UserService 의 동작을 테스트하고 싶을 때 , UserService 를 sut라고 한다.

     

    Test Double(테스트 더블)

    실제 객체 대신 테스트 목적으로 사용되는 모든 종류의 객체를 뜻한다. 목적에 따라 다양한 종류의 테스트 더블이 존재한다.

    이제 각각의 Test Double 유형(Dummy, Fake, Mock, Stub, Spy)을 살펴보자

     

    Dummy
    객체가 필요하지만 내부 기능이 필요하지 않을 때 사용된다. 일반적으로 매개변수 목록을 채우는데만 사용된다.

     

    Fake

    실제로 사용되는 객체는 아니지만 같은 동작을 하는 구현된 객체이다.
    실제 프로덕션에 사용하는 객체와 동일한 인터페이스를 구현하는 Fake 객체를 사용한다. 일반적으로 외부 서비스나 API에 의존하는 것을 테스트해야할 때 사용된다. (ex. 실제 DB보다 훨신 빠른 in-memory로 구현한 Fake 객체)

     

    UserService에서 UserRepository 객체를 주입하는데, 이 UserRepository 가 구현되지 않았거나 in-memory로 구현할 때 Fake 객체를 사용한다.

    Mock

    미리 정의된 동작을 갖는 객체이다. 이 객체가 코드에서 어떻게 사용되는지 확인할 수 있다. (행위 검증)
    행위에 대한 검증으로 verify 를 사용한다.

    // UserRepository mock 객체 생성
    val mockUserRepository = mockk<UserRepository>()
    
    // save 메서드 호출 시 반환값 설정
    every { mockUserRepository.save(any()) } returns true
    
    // 생성된 mock을 이용해 UserService 객체 생성
    val sut = UserService(mockUserRepository)
    
    sut.addUser(User("1", "Mock User"))
    
    // save 메서드 실행했는지 호출(행위) 검증
    verify(exactly = 1) { mockUserRepository.save(User("1", "Mock User")) }
    
     [참고] 'relaxed' 모드

    정의되지 않은 동작이 호출될 때
    'relaxed' 모드가 아닌 경우에만 예외가 발생할 수 있다. 'relaxed' 모드에서는 정의되지 않은 모든 메서드 호출에 대해 자동으로 기본값을 반환하고 예외를 발생시키지 않는다.

     

     

    Stub

    미리 정의된 값을 반환하는 객체이다. (상태 검증)

    Stub은 인수와 함께 호출될 때 항상 동일한 값을 반환하도록 프로그래밍될 수 있다. Stub은 일반적으로 코드를 실행하는 데 필요한 데이터를 제공하는 데 사용된다. 이 데이터는 하드 코딩되거나 동적으로 생성될 수 있다.

    // UserRepository mock 객체 생성
    val stubUserRepository = mockk<UserRepository>()
    
    // findById 메서드 호출 시 반환값 설정
    every { stubUserRepository.findById("1") } returns User("1", "Stub User")
    
    // 생성된 mock을 이용해 UserService 객체 생성
    val sut = UserService(stubUserRepository)
    
    val result = sut.getUserById("1")
    
    // findById가 호출됐을 때 예상된 결과가 반환되는지 확인
    assert(result == User("1", "Stub User"))

     

    Mock과 Stub의 주요 차이는 Mock이 기대하는 행위(메서드 호출, 인수 전달 등)를 검증하는 데 초점을 맞춘다면, Stub은 테스트 도중 필요한 특정 응답이나 데이터를 반환하는 데 초점을 맞춘다.

     

     

    Spy

    단순 호출 검증으로 테스트가 충분하지 않을 수 있다. 제대로된 값을 반환했는지 확인하고 싶을 때 Spy 객체를 사용한다.

    val userRepository = spyk(UserRepositoryImpl()) // spy 객체는 전달된 객체의 복사본
    
    // 특정 메서드 호출에 대한 동작 오버라이드
    every { userRepository.findById("2") } returns User("2", "Spy User")
    
    val resultOverride = userRepository.findById("2") // 오버라이드된 동작 반환
    
    // 오버라이드된 메서드와 실제 메서드 동작이 예상대로 수행되는지 검증
    assert(resultOverride == User("2", "Spy User"))

     

     

    마지막으로 인상깊게 보았던 Microsoft에서 제공한 테스트 코드 작성에 대한 조언을 작성해보려고 한다.

    • 테스트 코드는 가능한 한 명확하고, 의도를 잘 표현해야 한다.
    • 복잡한 로직은 분할하여 각각의 테스트가 단 하나의 기능만을 검증하도록 해야 한다. 이는 테스트의 가독성과 유지 보수성을 높이는 데 중요하다.

     

     

    아직 일을 하면서 테스트 코드를 많이 작성해보진 않았다.

    좀 더 경험을 쌓고 나서 테스트 코드 개념뿐이 아닌 나의 경험담과 팁들을 공유하는 글도 써보겠당!

     

    반응형

    댓글

Designed by Tistory.