Study

테스트 대역

voider 2021. 12. 21. 08:34

테스트 대상이 되는 오브젝트 기능만을 충실하게 수행하면서 빠르게 자주 테스트를 실행할 수 있도록 사용하는 오브젝트를 통틀어서 테스트 대역test double이라고 한다.

이를테면 회원가입 비즈니스 로직에 회원가입을 축하한다는 메일을 보내는 로직이 포함되어 있다고 가정하자.

UserService

class UserService(
    private val mailSender: MailSender
) {
    fun signUp(user: User) { 
        //가입 로직
        //...

        //회원가입 메일 전송
        val mailMessage = SimpleMailMessage()
        mailMessage.setTo(user.email)
        mailMessage.setFrom("useradmin@ksug.org")
        mailMessage.setSubject("회원가입 축하 메일")
        mailMessage.setText("${user.name}님 반갑습니다.")

        mailSender.send(mailMessage)
    }
}

엄밀하게 메일 전송이 되었느냐, 는 테스트 불가능(수신자의 메일을 확인해야 하기 때문)하다. 그리고 이미 검증된 메일 API를 사용하므로 sendMail() 메소드가 호출되었다는 것만 확인하면 메일이 정상적으로 발송되었다고 신뢰할 수 있다.

사실 signUp의 진짜 관심사는 회원 가입 요청을 처리하는 것이지, 메일이 잘 전송되었는지 확인하는 것이 아니다.

UserService 테스트 구조

메일 발송 작업 때문에 signUp 메서드 자체에 지장을 주지 않기 위해 도입한 것이 DummyMailSender . 아무 기능도 없지만 UserService가 테스트 시에는 JavaMailSender가 아닌 DummyMailSender를 의존하도록 하면, 메일 전송 로직에 신경을 덜 쓰면서 핵심 로직을 테스트 할 수 있다.

DummyMailSender는 정말 빈 구현체다

class DummyMailSender: MailSender {
    override fun send(simpleMessage: SimpleMailMessage) {
    }

    override fun send(vararg simpleMessages: SimpleMailMessage?) {
    }
}

테스트 대상이 되는 오브젝트가 또다른 오브젝트에 의존하는 일은 매우 흔하다. 이렇게 하나의 오브젝트가 사용하는 오브젝트를 DI에서는 의존 오브젝트라고 불러왔다. 의존한다는 말은 종속되거나, 기능을 사용한다는 뜻이다. 작은 기능이라도 다른 오브젝트의 기능을 사용하면 사용하는 오브젝트의 기능이 바뀌었을 때 자신이 영향 받을 수 있기 때문에 의존하고 있다고 말한다. 의존 오브젝트를 협력 오브젝트라고도 한다. 함께 협력해서 일을 처리하는 대상이기 때문이다.

아무튼 이렇게 테스트 대상인 오브젝트가 의존 오브젝트를 가지고 있기 때문에 발생하는 여러 가지 테스트 상 문제점이 있다. 대표적으로 간단한 로직을 테스트 하는데 메일 전송이라든지 하는 것 때문에 테스트하기 까다롭다면 빈 오브젝트로 대체하는 것이 해결책이다. DI를 사용해서 같은 타입의 다른 오브젝트를 주입해주면 된다.

테스트 대역 종류와 특징

이렇게 테스트용으로 사용되는 특별한 오브젝트들이 있다. 대부분 테스트 대상인 오브젝트가 의존하는 오브젝트들이다. DataSource 라던지, MailSender 타입 오브젝트라던지. 테스트 대상이 되는 오브젝트의 기능만을 충실하게 테스트하면서 빠르게, 자주실행할 수 있는 환경을 갖춰주기 위해서 사용하는 오브젝트를 통틀어서 테스트 대역test double이라고 부른다.

대표적으로 테스트 스텁test stub이 있다. 테스트 스텁은 테스트 대상 오브젝트의 의존 객체로서 존재한다. 테스트 동안에 코드가 정상적으로 수행할 수 있도록 돕는 것을 말한다. 일반적으로 테스트 스텁은 메소드를 통해 전달하는 파라미터와 달리, 테스트 코드 내부에서 간접적으로 사용된다. 따라서 DI 등을 통해 미리 의존 오브젝트를 테스트 스텁으로 변경해야 한다. DummyMailSender는 가장 단순하고 심플한 테스트 스텁의 예다.

많은 경우 테스트 스텁이 결과를 돌려줘야 할 때도 있다. MailSender 처럼 호출만 하면 그만인 것도 있지만, return 값을 사용하는 메서드를 이용하는 경우에는 값을 돌려줘야 한다. 이럴 땐 스텁에 미리 테스트 중에 필요한 정보를 리턴해주도록 만들 수도 있다. 또는 어떤 스텁은 메소드를 호출하면 강제로 예외를 발생시키게 해서 테스트 대상 오브젝트가 예외 상황에서 어떻게 반응하는지 테스트할 수도 있다.

테스트가 원활하게 돌아가도록 의존 오브젝트로서 간접적인 도움을 준다는 개념과 달리, 어떤 테스트 대역은 테스트 과정에 매우 적극적으로 참여할 수도 있다.

테스트는 보통 어떤 입력이 주어졌을 때 기대하는 출력이 나오는지를 검증한다. 단위 테스트에서는 보통 입력 값을 테스트 대상 오브젝트의 메소드 파라미터로 전달하고, 메소드의 리턴 값을 출력 값으로 보고 검증한다. 그런데 스텁을 이용하면 간접적인 입력 값을 지정해줄 수도 있다. 마찬가지로 어떤 스텁은 간접적인 출력 값을 받게 할 수 있다. DummyMailSender 는 테스트 오브젝트에 돌려주는 것은 없지만, 테스트 오브젝트인 UserService 로부터 전달받는 것은 있다.

테스트 대상 오브젝트의 메소드가 돌려주는 결과뿐 아니라, 테스트 오브젝트가 간접적으로 의존 오브젝트에 넘기는 값과 그 행위 자체에 대해서도 검증하고 싶다면 어떻게 해야 할까? 이 경우 단순하게 메소드의 리턴 값을 assertThat() 으로 검증하는 것으로는 불가능하다.

이런 경우 테스트 대상의 간접적인 출력 결과를 검증하고, 테스트 대상 오브젝트와 의존 오브젝트 사이에서 일어나는 일을 검증할 수 있도록 특별히 설계된 Mock 오브젝트를 사용해야 한다. mock 오브젝트는 스텁처럼 테스트 오브젝트가 정상적으로 실행되도록 도와주면서, 테스트 오브젝트와 자신 사이에서 일어나는 커뮤니케이션 내용을 저장해뒀다가 테스트 결과를 검증하는 데 활용할 수 있다.

이 표에서 5번을 제외하면 스텁이라고 봐도 된다. 테스트는 테스트 대상이 되는 오브젝트에 직접 입력 값을 제공하고, 테스트 오브젝트가 돌려주는 출력 값, 즉 리턴 값을 가지고 결과를 확인한다. 테스트 대상이 받게 될 입력 값을 제어하면서 그 결과가 어떻게 달라지는지 확인하기도 한다.

문제는 테스트 대상 오브젝트는 테스트로부터만 입력을 받는 것이 아니라는 점이다. 테스트가 수행되는 동안 실행되는 코드는 테스트 대상이 의존하고 있는 다른 의존 오브젝트와도 커뮤니케이션 한다. 테스트 대상은 의존 오브젝트에게 값을 출력하기도 하고 값을 입력 받기도 한다. 출력은 무시한다고 칠 수 있지만, 간접적으로 테스트 대상이 받아야 할 입력 값은 필요하다. 이를 위해 별도로 준비해둔 스텁 오브젝트가 메소드 호출 시 특정 값을 리턴하도록 특정 값을 만들어두면 된다.

때론 테스트 대상 오브젝트가 의존 오브젝트에게 출력한 값에 관심이 있을 경우가 있다. 또는 의존 오브젝트를 얼마나 사용했는가 하는 커뮤니케이션 행위 자체에 관심이 있을 수 있다. 문제는 이 정도는 테스트에서는 직접 알 수 없다는 점이다. 이때는 테스트 대상과 의존 오브젝트 사이에 주고 받는 정보를 보존해두는 기능을 가진 테스트용 의존 오브젝트인 목 오브젝트를 만들어서 사용해야 한다. 테스트 대상 오브젝트의 메소드 호출이 끝나면, 목 오브젝트에게 테스트 대상과 목 오브젝트 사이에 일어났던 일에 대해 확인을 요청해서 그것을 테스트 검증 자료로 삼을 수 있다.

이런 느낌이다. Mock 객체를 생성해서 send를 호출할 때마다 request 필드에 추가해두면, 검증 단계에서 send() 메소드가 몇 번 호출되었는지, 원하는 만큼 호출되었는지 검증할 수 있다.

inner class MockMailSender(
        val request: MutableList<String> = mutableListOf()
    ): MailSender {
        override fun send(simpleMessage: SimpleMailMessage) {
            simpleMessage.to?.let { request.add(it[0]) }
        }

        override fun send(vararg simpleMessages: SimpleMailMessage?) {
            TODO("Not yet implemented")
        }
    }

'Study' 카테고리의 다른 글

LearingSQL #4,5,6  (0) 2022.06.02
type check는 왜 필요한가?  (0) 2022.05.18
@ParameterlizedTest  (0) 2021.09.08
파일업로드 input[files] FileList 동적으로 변경하기  (10) 2021.06.12
[Database] Transaction, Lock  (0) 2021.05.25