본문 바로가기

iOS/스몰토크

[스몰토크] Swift Stubbing 통합테스트

 

단위테스트는 알고리즘을 검증하는 테스트입니다. 

통합테스트를 하기 위해 어떤 것들이 필요한 지 알아봅시다.

 

통합테스트

우리는 객체간 메시지 통신을 어떻게 확인할 수 있을까요...?

 

객체간 메시지 전달은 런타임에서 일어나기 때문에 런타임에서만 확인할 수 있습니다.

 

어떻게? 

자바에서는 Mockito 라는 강력한 Testing 도구가 있습니다.

Mockito 에 대한 자세한 설명은 생략합니다. 

한글로 자세한 설명은 여기를 확인해주세요.

 

 

객체통신망에서 테스트 객체에서 메시지를 보낸 뒤 이웃의 Mock 객체들이 메시지를 수신하였는지 확인합니다.

 

 

// interface 뿐 아니라 구체 클래스도 mock으로 만들 수 있다.
LinkedList mockedList = mock(LinkedList.class);
 
// stubbing
when(mockedList.get(0)).thenReturn("first");
when(mockedList.get(1)).thenThrow(new RuntimeException());
 
// 첫 번째 element를 출력한다.
System.out.println(mockedList.get(0));
 
// runtime exception이 발생한다.
System.out.println(mockedList.get(1));
 
// 999번째 element 얻어오는 부분은 stub되지 않았으므로 null이 출력
System.out.println(mockedList.get(999));
  
// stubbing 된 부분이 호출되는지 확인할 수 있긴 하지만 불필요한 일입니다.
// 만일 코드에서 get(0)의 리턴값을 확인하려고 하면, 다른 어딘가에서 테스트가 깨집니다.
// 만일 코드에서 get(0)의 리턴값에 대해 관심이 없다면, stubbing되지 않았어야 합니다. 더 많은 정보를 위해서는 여기를 읽어보세요.
verify(mockedList).get(0)

 

어떤것들이 있죠?

Mocking, Network Stubbing 등 단계별로 알아봅시다. 

Mocking

Cuckoo

Cuckoo 는 제가 사용하는 라이브러리입니다. 

자바의 Mockito 와 Interface 가 친숙하고 MVVMTDD 예제에서도 Cuckoo 를 사용하였습니다.

 

Stubber

역시 전수열님의 오픈소스를 뺄 수 없을 것 같습니다.

Stubber 는 전수열님이 오픈소스로 개발하신 라이브러리입니다.

 

 

어떻게 활용할 수 있을까?

가장 간단한 예제는 MVVMTDD 를 참고하시면 됩니다.

 

약간은 복잡한 형태의 2가지를 소개할려고 합니다. 

 

1. List Paging Stubbing

아신의 코드리뷰를 진행하면서 Github Search 의 간단한 앱을 ReactorKit 을 활용하여 Paging 기능이 포함된 코드리뷰 건이 있었습니다. 

 

과연 Paging 을 Stubbing 할 수 있을까요? 

대답은 YES 입니다.

 

아래 테스트코드는 Paging 기능을 검증하였던 코드입니다. 

1번의 Fetch 그리고 2번의 Load More 가 발생했지만 총 7번의 Event 가 발생하였습니다.

 

func testUserItemPaging() {
        // 페이징 동작이 잘 동작하는 지 검증
        let expect = [
            [Fixture.UserItems.sampleUserItems.shuffled().first!],
            [Fixture.UserItems.sampleUserItems.shuffled().first!],
            [Fixture.UserItems.sampleUserItems.shuffled().first!]
        ]
        reset(api)
        api.setUserItemsPaging(expect)

        let rxExpect = RxExpect()
        rxExpect.retain(reactor)

        rxExpect.input(reactor.action, [
                .next(0, .updateQuery("a")),
                .next(10, .loadNextPage),
                .next(20, .loadNextPage),
            ])

        // Reivew: [성능] NextPage를 2번 가지고 오면 총 7번의 연산이 들어갑니다.
        // 3번의 Action을 수행했기 때문에 3번만 Item을 가지고 올 수 있도록 전략이 필요합니다.
        // 배열의 크기가 커지면 치명적인 성능저하가 일어납니다.
        rxExpect.assert(reactor.state.map { $0.userItems }.filterEmpty()) { events in
            XCTAssertEqual(events.count, 7)
            XCTAssertEqual(events[0], .next(0, expect[0]))
            XCTAssertEqual(events[1], .next(10, expect[0]))
            XCTAssertEqual(events[2], .next(10, expect[0] + expect[1]))
            XCTAssertEqual(events[3], .next(10, expect[0] + expect[1]))
            XCTAssertEqual(events[4], .next(20, expect[0] + expect[1]))
            XCTAssertEqual(events[5], .next(20, expect[0] + expect[1] + expect[2]))
            XCTAssertEqual(events[6], .next(20, expect[0] + expect[1] + expect[2]))
        }
    }

 

1 First Fetch
2 Loading(True), First Fetch
3 Second Fetch
4 Loading(False), Second Fetch
5 Loading(True), Second Fetch
6 Third Fetch
7 Loading(False), Third Fetch

 

눈치채신 분들도 계셨겠지만 Loading 상태값이 변경될 때마다 이전에 Fetch Data 의 Event 를 전달하고 있습니다. 

이 점에 대해 성능 Comment 를 드렸습니다. 

 

 

Cuckoo 를 사용하였으며 Stubbing 코드도 상당히 간단합니다. 

 

    @discardableResult
    func setUserItemsPaging(_ items: [[UserItem]]) -> [[UserItem]] {
        stub(self, block: { mock in
            for (index, item) in items.enumerated() {
                when(mock.fetchUsers(with: any(), page: equal(to: index + 1))).thenReturn(Observable.just((item, index + 2)))
            }
        })
        return items
    }

 

아이디어는 fetchUsers 를 호출할 때 전달되는 Page 에 따라 미리 정해둔 Data 를 전달하는 것입니다.

 

Page 1 First
Page 2 Second
Page 3 Third

 

 

2. Networking Stubbing 

통합테스트를 넘어서 시스템 테스트로 넘어가는 과정에서 Stubbing 은 한계가 존재합니다. 

그렇타면 어떻게 완전한 테스트 환경을 구축할 수 있을까요?

 

아이디어는 GET 방식으로 http://localhost:8080/Your-Path 호출하면 미리 정해둔 응답값을 전달해 주는 것입니다.

 

Hippolyte 예제

 

guard let gitUrl = URL(string: "https://api.github.com/users/shashikant86") else { return }
        
var stub = StubRequest(method: .GET, url: gitUrl)
var response = StubResponse()
...
stub.response = response
Hippolyte.shared.add(stubbedRequest: stub)
Hippolyte.shared.start()

Mockingjay  예제

 

guard let gitUrl = URL(string: "https://api.github.com/users/shashikant86") else { return }
self.stub(uri("https://api.github.com/users/shashikant86"), jsonData(data as Data))        

 

Embassy, EnvoyAmbassador  예제

 

위에 3개의 라이브러리는 정말로 테스트를 위한 테스트 라이브러리인 것 같습니다. 

 

우리가 원하는 것은 Production Code 를 오염을 시키지 않으면서 Networking Stubbing 을 하는 것입니다.

 

Swifter 예제 는 그것을 도와줍니다.

 

UnitTest 에서는 사용할 순 없으며 UI Test 에서만 사용할 수 있을 것 같습니다.

 

그 이유는 launchEnvironment  -> ProcessInfo.processInfo.environment 는 UITest 에서만 지정할 수 있기 때문입니다.

 

// UITest
app.launchEnvironment = ["BASEURL" : "http://localhost:8080"]

// Production Code
let baseUrl = ProcessInfo.processInfo.environment["BASEURL"]!
guard let gitUrl = URL(string: baseUrl + "/users/shashikant86") else { return }

 

마지막으로 

 

Vapor, Kitura 는 Swift Server Framework 입니다.

 

테스트를 실행하기 전 Swift Server 를 실행할 수 있습니다.

 

Kitura예제는 그것을 도와줍니다.

 

그러나 한번 실행하기 위해 빌드에 소요되는 시간이 길기 때문에 최후의 수단으로 사용할 수 있을 것 같습니다.

 

cd ~/ProjectDIR/SwiftServerTestUITests/Server

swift build
swift run

 

 

Fixture

Fixture는 어떻게 관리해야 깔끔할까요? 

이 부분에 대해서 Json 파일을 deserialization 한다면 조금 더 관리하기 편할 수 있습니다.

 

Fixture.swift

 

 

import Foundation
@testable import MyGithubUserSearch

struct Fixture {
    struct UserItems {
        static let sampleUserItems: [UserItem] = ResourcesLoader.loadJson("sample_user_item")
        static var first: UserItem {
            return sampleUserItems.first!
        }
    }
    struct Organizations {
        static let sampleOrganization: [OrganizationItem] = ResourcesLoader.loadJson("sample_organization")
        static var first: OrganizationItem {
            return sampleOrganization.first!
        }
    }
}

 

참조: 

https://github.com/mockito/mockito/wiki/Mockito-features-in-Korean

https://youtu.be/Qa3dRrSbeQI?list=PLBNdLLaRx_rI-UsVIGeWX_iv-e8cxpLxS&t=3867

https://site.mockito.org/

Network Stubbing options for XCTest and XCUITest in Swift