이재용의 iOS

상태 기반 테스트 vs 행위 기반 테스트

2022년 9월 20일 • ☕️ 3 min read

test-doubles

Test Doubles는 테스트 목적으로 production 객체를 대체하는 객체를 부르는 모든 용어를 말한다. 그리고 이러한 Test Double에는 다섯 가지 종류가 있다

dummy, fake, stub, mock, spy

이 Test Doubles들의 차이점을 알아보기 전에 알아야 하는 개념이 있는데,
상태 기반 테스트 vs 행위 기반 테스트이다.

상태 기반 테스트

상태 기반 테스트는 특정한 메소드를 거친 후, 객체의 상태에 대한 예상 값과 비교하는 방식이다.

func test_add() {
  //given
  let expectation = 10
  //when
  let result = self.calculator.add(4, 6)
  //then
  XCTAssertEqual(result, expectation)
}

행위 기반 테스트

올바른 로직 수행에 대한 판단의 근거로 특정한 동작의 수행 여부를 이용한다. 메소드의 리턴 값이 없거나 리턴 값을 확인하는 것만으로 예상대로 동작했음을 보증하기 어려운 경우에 사용한다. 즉, “행위”를 점검하는 것으로 테스트 케이스를 만든다. (테스트 Spy, Mock 객체 사용)

예를들어, 로그아웃 기능을 구현할 때 로그아웃 네트워크 요청을 성공하면 로컬 디비에 저장된 토큰과 유저 정보를 지워야하는 상황이 있다고 가정하자. 네트워크 요청을 보내고 응답이 온 후, 로컬 디비의 토큰을 지우는 메소드와 유저정보를 지우는 메소드가 호출되는 지를 확인하는 것이 테스트 시나리오가 된다.

protocol LogoutUseCase {
    func logout() -> Observable<AuthModels.Empty.Response>
}

final class LogoutUseCaseImpl: LogoutUseCase {
    private let authRepository: AuthRepository
    private let accessTokenRepository: AccessTokenRepository
    private let userAccountRepository: UserAccountRepository
    init(
        authRepository: AuthRepository = AuthRepositoryImpl(),
        accessTokenRepository: AccessTokenRepository = AccessTokenRepositoryImpl(),
        userAccountRepository: UserAccountRepository = UserAccountRepositoryImpl()
    ) {
        self.authRepository = authRepository
        self.accessTokenRepository = accessTokenRepository
        self.userAccountRepository = userAccountRepository
    }
    
    func logout() -> Observable<AuthModels.Empty.Response> {
        authRepository.logout()
            .do { [weak self] _ in
                _ = self?.accessTokenRepository.deleteAccessToken()
                _ = self?.userAccountRepository.deleteLocalUserAccount()
            }
    }
}

물론, 로그아웃 네트워크 요청을 보내고 로컬 디비에 토큰과 유저 정보의 저장 상태 유무로 로그아웃 기능을 테스트할 수 있지만, 행위 기반 테스트가 필요한 이유는 순서때문이다. 네트워크 요청이 오지 않은 상태로 토큰과 유저정보를 지워버리면 오류가 발생한다.

따라서 행위 기반 테스트를 만들 때는, 예상하는 행위들을 미리 시나리오로 만들어놓고 해당 시나리오대로 동작이 발생했는지 여부를 확인하는 것이 핵심이 된다.


상태 기반 vs 행위 기반

상태 기반과 행위 기반의 차이점을 이해하기 쉬운 에제를 하나 더 들어보자.

서울에서 포항으로 5kg 이하인 택배를 배달한다. 10월 1일에 서울에서 출발해 10월 3일에 포항에 있는 필자의 집으로 배달된다. 서울에서 포항까지 너무 멀기 때문에 택배 기사님은 중간 지점인 대전까지는 차로 이동했고, 대전에서 비행기를 타고 포항에 갔다.

서울 -> 대전 -> 대구 -> 포항

위의 예시에서 택배 도착 여부에 대한 상태 기반 테스트를 한다면,

1. 택배는 5kg 아래인가?
2. 택배는 101일에 서울에서 출발했나?
3. 택배는 103일에 포항에 도착했나?

라고 물어볼 수 있다. 차, 비행기와 같이 이동수단에 대해서는 무시한다. 결국 택배가 서울에서 포항으로 제시간에 맞춰서 도착했나라는 택배의 상태를 기반으로 테스트하는 것이 주목적이기 때문이다.

행위 기반 테스트를 한다면,

1. 택배는 5kg 아래인가?
2. 택배는 101일에 서울에서 출발했나?
3. 택배는 차를 통해 서울에서 대전으로 이동했나? // ✅
4. 택배는 비행기를 통해 대전에서 포항으로 이동했나? // ✅
5. 택배는 103일에 포항에 도착했나?

라는 것들을 테스트해볼 수 있다. 올바른 프로세스를 통해 택배가 배달되었나를 주목적으로 테스트하는 것이 행위 기반이다.

만일, 중간에 인천으로 차로 이동한 후, 인천에서 비행기를 타고 포항에 도착했다고 상황이 바뀌면 테스트 질문은 행위 기반에서만 수정되어야 한다. 택배의 상태에는 변함이 없다.

1. 택배는 5kg 아래인가?
2. 택배는 101일에 서울에서 출발했나?
3. 택배는 차를 통해 서울에서 인천으로 이동했나? // ✅
4. 택배는 비행기를 통해 인천에서 대구로 이동했나? // ✅
5. 택배는 103일에 포항에 도착했나?

마무리

아직 테스트 코드를 많이 작성하지 않아 행위 기반 테스트를 작성해본 적이 없다. 😓 최대한 많이 작성해보면서 훈련해보자. 그래도 차이점을 알고 필요에 맞게 필요한 test double을 사용해야겠다.