Cuckoo의 ArgumentCaptor를 소개합니다

원티드의 iOS 앱 프로젝트에서 사용하고 있는 모킹 프레임워크 Cuckoo에 포함된 ArgumentCaptor 가 무엇이고, 어떻게 사용하는지 간단하게 알아보도록 하겠습니다.

이게 뭔가요?

ArgumentCaptor 는 Mock 객체에 입력되는 파라미터를 가로채서 직접 무언가를 할 수 있게 도와주는 객체입니다. verify(_:) 메소드를 통해 검증을 진행할 때 메소드의 파라미터로 이 ArgumentCaptor 객체를 넣어주게 되면 해당 메소드의 파라미터로 들어오는 값을 모아주게 됩니다.

Cuckoo의 Github Repo에 소개된 예제 코드 를 보면 다음과 같습니다. Mock 객체의 특정 프로퍼티에 들어오는 값을 ArgumentCaptor 객체로 잡아내어 어떤 값들이 들어오는지 확인하는 코드입니다.

mock.readWriteProperty = 10
mock.readWriteProperty = 20
mock.readWriteProperty = 30

let argumentCaptor = ArgumentCaptor<Int>()
verify(mock, times(3)).readWriteProperty.set(argumentCaptor.capture())
argumentCaptor.value // Returns 30
argumentCaptor.allValues // Returns [10, 20, 30]

그냥 Matcher 만 써도 되는거 아닌가요? 🤔

기존 Matcher 와 다른 점이 있다면, 이 ArgumentCaptorEquatable 이 “불완전하게 구현”되어 있는 상태에서 파라미터 객체의 일부분만을 비교해야 할 때 쓰기가 좋습니다. 예를 한번 들어볼게요.

enum Response: Equatable {
    case content(Community.Post, UIImage?)
    case shareSheet(URL)
    case commentWriter(CommentWriter)

    static func == (lhs: Self, rhs: Self) -> Bool {
        switch (lhs, rhs) {
        case (.content(let lp, _), .content(let rp, _)):
            return lp.id == rp.id
        case (.shareSheet, .shareSheet), (.commentWriter, .commentWriter):
            return true
        default:
            return false
        }
    }
}

// Post의 ID만 비교하기 때문에 Post의 다른 요소를 확인할 수 없다
verify(presenter).present(.content(post, nil))

위와 상황에서 Post 객체의 내부 프로퍼티를 비교해야 할 일이 생긴다면, Custom Matcher를 구현해도 되겠지만 ArgumentCaptor 를 사용해서 검증을 진행할 수도 있습니다.

Mock 객체가 받은 파라미터를 ArgumentCaptor 가 가로채서 가지고 있으니 우리는 이걸 가지고 비교를 하면 되겠죠. 실제 구현된 테스트 코드를 예로 들어 확인해 볼게요.

context("로그인 된 사용자인 경우") {
    it("댓글 작성 영역에 유저 관련 정보가 함께 출력되어야 한다.") {
        sut.process(Community.Read.Request.OnLoad())
    
        let argumentCaptor = ArgumentCaptor<Response>()
        verify(presenter, atLeastOnce()).present(argumentCaptor.capture())
    
        // OnLoad 의 경우 presenter.present(_:) 함수가 여러번 불릴 수 있기 때문에 
        // filter(_:) 를 사용해 원하는 Response만 추출해 냅니다.
        let response = argumentCaptor.allValues.filter {
            if case .commentWriter = $0 { return true }
            else { return false }
        }.first
    
        // 내부 파라미터를 비교하기 위해 guard case 문을 사용합니다
        guard case .commentWriter(let param) = response else {
            assertionFailure()
            return
        }

        expect(param.thumbnail).to(equal(sampleThumbnailURL))
    }
}

결론

ArgumentCaptor 는 테스트 코드를 좀 더 자유롭게 쓸 수 있게 도와주며, 기존 코드가 이미 존재하는 상황에서 테스트를 작성할 때도 도움이 됩니다. 개인적으로는 Clean Swift의 Presenter에 넘어가는 Response 값을 비교할 때 주로 사용했던 것 같네요. 테스트 코드 작성하실 때 조금이나마 도움이 되셨으면 좋겠습니다 😄

참고 자료

GitHub - Brightify/Cuckoo: Boilerplate-free mocking framework for Swift!