호댕의 iOS 개발

[iOS] 테스트 코드(유닛 테스트, Unit Test)에 대하여 본문

Software Engineering/iOS

[iOS] 테스트 코드(유닛 테스트, Unit Test)에 대하여

호르댕댕댕 2024. 3. 2. 15:20

최근 회사에서 테스트 코드를 지속적으로 추가해주고 있고, 멋쟁이 사자처럼에서도 테스트 코드 관련해서 발표를 진행해서 테스트 코드에 대해 한 번 정리해보고자 한다. 

 

일단 테스트 코드는 채용공고를 보더라도 심심치 않게 보이기도 하고 막연히 테스트를 짜면 좋다는 생각을 가지고 있지만, 못짜는 경우도 많다.

 

테스트가 필요한 이유

그럼에도 테스트는 왜 필요할까?

 

물론 앱 규모가 작다면 매번 직접 시뮬레이터나 실기기에서 실행을 해보면서 원하는 동작이 정상적으로 작동하는지 테스트할 수도 있을 것이다. 오히려 이렇게 하는게 더 빠를 수도 있다. 

 

하지만 이렇게 하면 한계는 존재한다. 

  • 버그나 예상하지 못한 사이드 이펙트가 발생했더라도, 어디서 발생했는지 바로 알 수 없음
  • 앱이 커지고 리팩토링이 지속적으로 발생할 경우 직접 실행하는 것으로 모든 버그와 사이드 이펙트를 확인하는 것은 쉽지 않음
    • 가능하다고 해도 너무 많은 시간이 걸림

 

이런 상황에서 테스트가 잘 짜여져 있다면 위 한계를 해소할 수 있을 것이다. 

처음에 짜는 데에는 시간이 훨씬 더 걸리겠지만 앱이 커질 수록 테스트가 있다면 버그나 사이드 이펙트를 찾는데 효율적이다. 이를 통해 안정적인 확장이나 리팩토링이 가능하게 된다. 

 

그리고 이건 테스트를 짜보면서 느낀 부분인데 Testable한 코드를 짜기 위해선 의존성에 대해 고민할 수 밖에 없다. 

테스트가 필요한 인스턴스를 만들기 위해 여기에 연결된 객체들의 인스턴스를 생성해서 주입하든 목으로 대체를 하든 방법이 필요하다. 

 

이때 의존성이 너무 복잡하게 엮여있다면 테스트를 하기도 어렵기 때문에 의존성을 잘 관리하는 부분도 필요하다고 느꼈다. 

(이건 테스트가 필요한 이유라기 보단 테스트를 짜면서 오는 부수적인 장점인 것 같다)

 

즉, 안정적인 프로덕트의 유지 보수를 위해선 테스트 코드가 필요하다. 

 

테스트의 종류

요건 테스트에 대해 검색해보면 많이 보이는 그림이다.

 

 

우리가 XCTest 프레임워크를 통해 기본적으로 만든 테스트는 대부분 유닛테스트이고 피라미드 위로 올라갈 수록 점점 사용자가 사용하는 것과 유사해지며 테스트하는 범위가 많아져서 자동적으로 테스트의 수는 줄어든다. 

(테스트의 수는 줄어들고 커버하는 코드 자체는 많아진다)

 

  • 엔드 투 엔드 테스트: 프로덕트 전체 플로우를 사용자 액션과 유사하게 테스트
  • 통합 테스트: 여러 개의 모듈, 소프트웨어를 결합하여 함께 작동하는 방식의 테스트
  • 유닛 테스트: 객체, 메서드 등 프로덕트의 개별 구성 요소를 테스트

 

Unit Test 추가하기

Unit Test를 추가하는 것 자체는 아주 간단하다. 

 

새롭게 프로젝트를 생성하는 경우

새로 프로젝트를 생성하는 경우는 아래처럼 Include Tests를 체크한 후 생성을 해주면 Test Target이 자동으로 추가된다. 

 

 

이미 생성된 프로젝트에 테스트를 추가하는 경우

아래 이미지처럼 빨간색으로 표시된 부분을 눌러주면 된다. 

 

좌측 Navigator에서 Show the Test Navigator를 선택한 후 좌측 최하단의 + 버튼을 눌러 New Unit Test Target을 선택해주면 된다.

 

새로 Test를 만들면 어떻게 되어 있을까?

import XCTest

final class GitPracticeTests: XCTestCase {
    override func setUpWithError() throws { }

    override func tearDownWithError() throws { }

    func testExample() throws { }

    func testPerformanceExample() throws {
        measure { }
    }
}

 

import XCTest

https://developer.apple.com/documentation/xctest

 

XCTest | Apple Developer Documentation

Create and run unit tests, performance tests, and UI tests for your Xcode project.

developer.apple.com

일단 자동으로 XCTest를 import하고 있다. 

이는 테스트를 위한 프레임워크로 특정 조건이 충족되었는지 확인 후 이를 기록하게 된다. 

기록된 내용은 Show the Test Navigator / Show the Report Navigator에서 확인이 가능하다. (요 부분은 이후 내용에서도 다룰 예정)

 

XCTestCase

https://developer.apple.com/documentation/xctest/xctestcase

 

XCTestCase | Apple Developer Documentation

The primary class for defining test cases, test methods, and performance tests.

developer.apple.com

테스트 케이스, 방법, 성능 테스트를 정의하기 위한 기본 클래스이다.

 

setUpWithError

https://developer.apple.com/documentation/xctest/xctest/3521150-setupwitherror

 

setUpWithError() | Apple Developer Documentation

Provides an opportunity to reset state and to throw errors before calling each test method in a test case.

developer.apple.com

 

테스트를 시작하기 위해 필요한 의존성 등을 준비함 (각 테스트가 시작하기 전 호출)

 

tearDownWithError

https://developer.apple.com/documentation/xctest/xctest/3521151-teardownwitherror

 

tearDownWithError() | Apple Developer Documentation

Provides an opportunity to perform cleanup and to throw errors after each test method in a test case ends.

developer.apple.com

각 테스트가 끝나고 설정한 값들을 다시 초기화함

 

 

그래서 이를 그림으로 표현하자면 이런 식으로 매번 테스트가 시작되기 전 / 후로 실행이 되게 된다.

 

왜 매번 테스트를 위한 준비를 새롭게 하고 다시 없애는 것일까?

테스트는 서로 영향을 주고 받지 않아야하기 때문이다.

 

테스트 검증을 할 때 특정 객체의 프로퍼티를 통해 하는데 여러 함수에서 해당 프로퍼티를 사용하고 있다면?

함수의 호출되는 순서에 따라서 프로퍼티의 값이 달라질 수 있다.

 

테스트를 실행하는 순서에 따라 테스트의 성공 여부가 매번 바뀐다면 이는 테스트의 신뢰도를 떨어뜨리는 원인이 될 것이다.

그러므로 테스트를 할 때 기본적으로 새롭게 인스턴스를 만들고 테스트가 끝나면 이를 초기화하는 과정으로 설계가 됐다고 생각한다.

 

유닛 테스트는 기본적으로 어떻게 이뤄지나?

테스트의 예상 값과 결과 값을 비교하는 방식으로 이뤄진다. 

 

이때 XCTest 프레임워크의 XCTAssert~ 함수들을 사용해 결과를 검증하게 된다.

(XCTAssertEqual, XCTAssertTrue, XCTAssertNoNil 등 다양함)

 

func add(number1: Int, number2: Int) -> Int {
    number1 + number2
}

func test_add_two_number() {
    let number1 = 1
    let number2 = 3

    let result = add(number1: number1, number2: number2)
    let expectation = 4

    XCTAssertEqual(result, expectation)
}

 

Test는 새로운 타겟이다?

테스트를 생성해보면 알겠지만 Test는 기존 프로덕트와는 다른 새로운 타겟이다.

 

따라서 기존 코드의 객체나 함수가 open / public으로 선언되어있지 않다면 이에 접근 자체가 불가능하다.

필요에 따라 접근 제어자를 open / public으로 선언하는 것은 전혀 문제가 되지 않겠지만 테스트 타겟에서 접근하기 위해 접근제어자를 더 높은 수준으로 올리는 것은 옳지 않다. 

 

기존에 외부로 드러나지 않아도 되는 코드를 노출시켜야 하기 때문이다. 

 

설계 시 은닉화를 염두해두고 설계를 한 코드를 높은 수준의 접근 제어자로 올리게 되면 해당 함수나 객체가 외부에 사용될 수 있고, 이는 불필요한 의존 관계를 생성하기 때문에 옳지 않다.

 

그래서 Apple은 @testable를 제공한다. 

 

@testable

프로젝트의 빌드 옵션을 보면 Enable Testability를 보면 Debug는 yes라고 되어 있다. 

 

이는 컴파일을 할 때 Xcode에 자동으로 enable-testing 플래그를 두고 internal 보다 높은 접근 제어자의 경우 open으로 높여서 외부에서 접근이 가능하도록 한다. 

(fileprivate, private으로 선언한 경우 이는 접근제어자가 유지)

 

이렇게 되면 코드의 수정 없이 테스트 코드 타겟에서 기존 프로덕트 코드에 접근이 가능하게 되며, 간단하게 프로덕트 코드를 테스트할 수 있게 된다.

 

@testable import {프로덕트 모듈 이름}

사용할 때에는 위 코드처럼 사용을 하면 된다.

 

단위 테스트 구성

Given - When - Then 패턴

AAA 패턴도 존재하지만 요게 좀 더 직관적으로 이름이 이해되기 때문에 해당 패턴을 주로 사용했다. 

 

func add(_ number1: Int, _ number2: Int) -> Int {
    number1 + number2
}

func test_add_two_number() {
	// Given
    let number1 = 1
    let number2 = 3

	// When
    let result = add(number1, number2)

	// Then
    let expectation = 4

    XCTAssertEqual(result, expectation)
}

 

  • Given : 해당 유닛테스트를 위해 필요한 값이나 객체 등을 준비
    • setUpWithError는 공통적으로 준비해야 될 부분을 작성하고 여기선 개별적으로 필요한 것들을 준비시킨다
  • When : 테스트할 메서드 실행
  • Then : 실행한 메서드의 결과를 검증

 

이름과 설명을 보면 어느정도 직관적으로 와닿을 것이다. 

 

모든 테스트에 Given / When / Then이 포함되지는 않아도 된다. (Given 같은 경우 상황에 따라 빠질 수도 있음)

 

이렇게 일정 패턴을 정해두고 테스트 코드를 작성하면 추후 테스트를 확인하거나 다른 사람들이 해당 테스트를 볼 때 가독성이 올라가서 테스트가 코드에 대한 안내를 하는 역할도 좀 더 충실히 할 수 있다. 

 

sut

테스트 코드 예시를 보면 sut라는 단어를 본 적이 있을 것이다. 

이는 Swift에서만 사용되는 것이 아니며 소프트웨어 전반에서 공통적으로 사용되는 용어이다. 

 

System Under Test의 줄인말로 테스트를 하게 되는 객체를 정의하게 된다. 

즉, 테스트할 대상(객체)인 것이다.

 

sut의 경우 해당 테스트 전반에서 필요하기 때문에 대부분 setUpWithError에 생성하고 tearDownWithError에서 초기화하는 식으로 구성된다.

 

테스트 코드 네이밍

네이밍은 항상 어렵다... 

 

테스트는 프로덕의 가이드 라인이 될 수 있기 때문에 네이밍을 잘 하는 것도 중요하다. 

언제 메서드가 성공 / 실패를 하고 메서드를 어떻게 사용하면 되는지 테스트 코드를 보면 쉽게 알 수 있는데 네이밍까지 이해하기 좋게 되어 있다면 이런 역할을 더욱 잘 해낼 수 있을 것이다. 

 

  • 비 개발자들이 봤을 때에도 명확히 어떤 행동을 하는지 이해할 수 있는 네이밍
    • 엄격하게 네이밍 규칙을 정해두는 것보다는 최대한 이해하기 쉬운 네이밍이 더 나을 수 있음
      (개인적으론 테스트 코드는 예외적으로 한글로 네이밍해도 괜찮은 경우가 있었음)
  • test는 앞에 항상 붙어야 함
  • 카멜케이스가 아닌 스네이크 케이스로 표기

 

요 정도를 염두해두고 최대한 이해하기 쉬운 네이밍을 고민해보는 것이 필요한 것 같다. 

 

다양한 방면에서 검증

프로덕트에 문제가 있음에도 테스트가 이를 발견하지 못한다면 이를 거짓 음성이라고 한다.

이는 테스트의 신뢰도를 낮추고, 테스트를 점점 사용하지 않는 길로 갈 수 있는 매우 안 좋은 현상이다. 

 

따라서 테스트를 구성할 때 Then 절을 다양한 방면으로 검증해보는 것이 필요하다. 

(테스트가 실패하는 경우도 테스트를 해보는 것이 좋다)

 

이렇게 하다보면 개인적으로 생각해보지 못했던 사이드 이펙트나 결과도 고민해볼 수 있어 앱 내 버그를 최소화할 수 있다고 생각한다.

 

이래서 TDD(Test Driven Development)가 나오지 않았나 싶다. 

 

물론 나도 실제 개발에서 TDD를 적용해서 개발을 한 경험은 없지만 이렇게 테스트로 미리 함수나 객체가 실패하는 경우부터 성공하는 경우까지 고려를 하고 개발을 하면 앱의 안정성이 엄청 올라갈 수 있고, 설계에서도 좀 더 좋은 설계가 나올 수 있다고 생각하기 때문이다. 

 

(나는 개발의 속도가 더뎌진다는 측면에서 도입을 해보진 못했다. 따라서 이 부분은 실제 적용하다보면 얻게 되는 장점과 단점에 대해 깊이있게 생각하지 못했을 수 있다)

 

Test Coverage

테스트 코드가 기존 프로덕트 코드를 얼마나 실행하고 있는지를 나타낸다. 

이를 통해 현재 테스트 코드가 프로덕트를 얼마나 검증하고 있는지 알 수 있다. 

 

 

Test Coverage는 위 이미지의 역할 정도를 한다. 

커버리지가 높다고 항상 좋은 것은 아니지만 낮다면 테스트를 추가할 필요가 있다는 것이다. 

 

커버리지는 Show the Report Navigator에서 확인이 가능하다. 

 

이는 Edit Scheme을 선택한 후 

Test Plans를 선택한다. 

 

여기서 Configurations 탭을 선택하면 Code Coverage가 on으로 되어 있다.

확인하는 방법이 Xcode가 업데이트를 하면서 이런식으로 변경이 되었다. 

 

비동기 함수 테스트

네트워크 요청같은 작업을 했다면 이에 대한 테스트 코드를 짤 수도 있을 것이다. 

 

이런 네트워크 요청은 다른 스레드에 작업을 시키고 작업 종료를 기다리지 않고 다음 작업을 실행하는 비동기 작업이다. 

그래서 기존처럼 테스트를 하다보면 요렇게 통과를 하긴 한다. 

하지만 XCTAssert~ 쪽에 브레이크 포인트를 걸어놓고 실행을 시켜보면 브레이크 포인트가 걸리지 않는다. 

 

비동기 작업이기 때문에 작업이 끝나고 실행되는 completionHandler가 호출되기 전에 test_network 함수가 끝나버리면서 검증을 하지 않고 테스트가 통과했다고 나오는 것이다. 

 

즉, 테스트가 제대로 되지 않은 것이다. 

 

XCTestExpectation

이때 사용할 수 있는 것이 XCTestExpectation이다. 

https://developer.apple.com/documentation/xctest/xctestexpectation

 

XCTestExpectation | Apple Developer Documentation

An expected outcome in an asynchronous test.

developer.apple.com

이는 이런 식으로 활용할 수 있다. 

 

일단 XCTestExpectation 인스턴스를 선언한 후 테스트 검증 부분에 조건이 모두 충족한 경우 fulfill 함수가 호출되도록 한다. 

그리고 fulfill 함수가 호출될 때까지 wait 함수를 통해 해당 XCTestExpectation를 기다리게 하면 된다. 

 

이렇게 하면 비동기 함수도 테스트가 가능하다. 

 

하지만 이 방식은 외부 환경인 네트워크에 의존적인 테스트이며 테스트에 걸리는 시간도 엄청 길다는 단점이 있다. 

네트워크 요청의 경우 기본적인 동기 함수들에 비해 시간이 엄청 오래 걸리기 때문이다.

 

그래서 이런 방식은 가능은 하긴 하나 권장되는 방식은 아니다. 

 

다른 테스트에 영향을 받지 않도록 하는 것처럼 테스트는 무결성을 지녀야 하기 때문이다. 

 

그래서 사용할 수 있는 방법이 Test Double을 활용하는 것이다. 

 

Test Double

  • 테스트는 외부 환경에 의해 결과가 바뀌어선 안 된다
  • 빠르게 실행될 수 있어야 한다.

 

위 두 조건을 충족하는 테스트를 짜기 위해 Test Double을 활용할 수 있다. 

 

Moya와 Alamofire 같은 외부 라이브러리를 사용한다면 방법이 달라질 수도 있다. 

하지만 기본적인 개념 자체는 동일하며 이번 포스팅에선 URLSession을 기반으로 설명하고자 한다.

 

Mock과 Stub

  • Mock의 경우 행동 검증이다. ex) URLSession의 dataTask 함수 동작을 대체
  • Stub의 경우 상태 검증이다. ex) 통신을 통해 오는 데이터 자체를 대체

 

이 2가지 개념은 혼용되어서 사용하는 경우들이 종종 있고, 나 또한 그런 경험이 있어 잘 분류하고 사용하는 것이 중요하다. 

 

1. URLSession 추상화하기

일단 URLSession을 MockURLSession으로 대체해서 사용해야 하기 때문에 이를 한 번 추상화해놓는 것이 좋다. 

예제에선 URLSession의 dataTask 함수만 프로토콜에 정의해놓고 사용했는데 추가로 반드시 정의해야 하는 함수가 존재한다면 미리 정의를 해놓아도 무방하다. 

 

import Foundation

protocol URLSessionProtocol {
    func dataTask(
        with request: URLRequest,
        completionHandler: @escaping @Sendable (Data?, URLResponse?, Error?) -> Void
    ) -> URLSessionDataTask // 기존 dataTask 함수와 동일
}

extension URLSession: URLSessionProtocol { }

 

2. 이를 사용하는 객체 생성

추상화한 URLSession을 사용할 수 있는 객체를 생성한다. 

이를 초기화할 때 Mock으로 대체해서 사용하도록 구현한다.

 

예제를 위한 코드로 누락된 에러 처리나 좀 더 나은 에러 처리 방식이 존재할 수 있습니다. 

import Foundation

final class NetworkManager {
    private let urlSession: URLSessionProtocol
    
    init(urlSession: URLSessionProtocol = URLSession.shared) {
        self.urlSession = urlSession
    }

    func fetchData<T: Decodable>(
        url: URL,
        decodingType: T.Type,
        completionHandler: @escaping((Result<T, Error>) -> ())
    ) {
        let request = URLRequest(url: url)
        let task = urlSession.dataTask(with: request) { data, response, error in
            let successStatusCode = 200..<300
            guard
                let httpResponse = response as? HTTPURLResponse,
                successStatusCode.contains(httpResponse.statusCode)
            else {
                if let error {
                    completionHandler(.failure(error))
                } else {
                    let statusCodeError = NSError(
                        domain: "Network",
                        code: 0,
                        userInfo: [NSLocalizedDescriptionKey: "StatusCode Error"]
                    )
                    completionHandler(.failure(statusCodeError))
                }
                return
            }
            
            if let error {
                completionHandler(.failure(error))
            }
            
            if let data {
                let decodedData: T
                do {
                    decodedData = try JSONDecoder().decode(T.self, from: data)
                    
                    completionHandler(.success(decodedData))
                } catch {
                    let decodingError = NSError(
                        domain: "Decoding",
                        code: 0,
                        userInfo: [NSLocalizedDescriptionKey: "Decoding Error"]
                    )
                    completionHandler(.failure(decodingError))
                }
            }
        }
        
        task.resume()
    }
}

 

3. MockURLProtocolClass 만들기

import Foundation

final class MockURLProtocol: URLProtocol {
    static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data) )?

    override class func canInit(with request: URLRequest) -> Bool {
        return true // request를 처리할 수 있는지 확인하는 부분, Mock이므로 항상 가능하도록 선언
    }

    override class func canonicalRequest(for request: URLRequest) -> URLRequest {
        return request // 동일한 Request인지 확인하고 캐시 작업을 하는 부분인데 여기도 바로 request 전달
    }

    override func stopLoading() { } // request를 호출하는 작업이 중지되며 이후 동작을 정의

    override func startLoading() { // URLProtocolClient로 어떻게 request를 처리할 지 정의
        guard let handler = MockURLProtocol.requestHandler else {
            return
        }
        do {
            let (response, data)  = try handler(request)
            client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
            client?.urlProtocol(self, didLoad: data)
            client?.urlProtocolDidFinishLoading(self)
        } catch  {
            client?.urlProtocol(self, didFailWithError: error)
        }
    }
}

가장 초반에 선언된 requestHandler는 나중에 Request를 받아서 어떻게 처리할 지 정의해주면 된다. (Stub 데이터를 보내도록)

 

override한 4개 함수는 공식 문서 상에서 반드시 Subclass에서 정의해야 하는 메서드로 나와 있으나, 없어도 실행이 정상적으로 되긴 했다. 

 

일단 Mock 데이터를 간편하게 처리할 수 있도록 따로 캐시 작업 없이 항상 request를 처리하도록 구현해놓았다. 

 

4. 결과로 나올 Stub 파일 생성

이제 결과로 전달할 Stub 파일을 생성한다. 

이를 선언해두면 매번 PostMan 등을 통해 결과가 어떻게 전달되는지 확인할 수 있어 문서화의 역할도 할 수 있다. 

 

데이터는 JSON 형태로 오는 경우가 많기 때문에 JSON으로 설명을 하자면 이를 위해선 Strings File을 생성한다. 

 

이는 원래 Localize를 위해 존재하는 파일인데 해당 파일 선택 후 .json으로 확장자까지 작성해주면 JSON 파일을 생성할 수 있다.

(이후에 수정해줘도 변경이 되지 않기 때문에 반드시 생성 시 붙여주세요!!)

 

여기에 결과로 나올 JSON 형식의 데이터를 적어주면 된다. 

 

 

 

5. Network 처리 객체 선언

이제는 테스트를 위해 sut로 네트워크 요청을 처리할 객체를 선언해야 한다. 

이 작업은 setUpWithError에서 해주면 된다.

 

let configuration = URLSessionConfiguration.ephemeral
configuration.protocolClasses = [MockURLProtocol.self]
let urlSession = URLSession(configuration: configuration)

sut = NetworkProvider(session: urlSession)

여기서 URLSessionConfiguration.ephemeral은 캐시 / 인증 / 쿠키에 따로 저장소를 사용하지 않는 URLSession 구성이다. 우리는 Mock을 사용할 것이기 때문에 이걸 사용했다. 

 

그리고 protocolClasses로 선언한 MockURLProtocol의 타입 배열을 할당해주고 이를 통해 URLSession을 만들어주면 된다. 

 

6. 이를 활용한 테스트 코드 작성

    func testExample() throws {
        // Given
        let expectation = XCTestExpectation()
        
        guard
            let path = Bundle(for: type(of: self)).path(forResource: "StubRandomNumber", ofType: "json"),
            let jsonString = try? String(contentsOfFile: path)
        else {
            XCTFail()

            return
        }
        
        let data = jsonString.data(using: .utf8)
        let stubResponse = HTTPURLResponse(
            url: URL(string: "https://csrng.net/csrng/csrng.php?min=0&max=50")!,
            statusCode: 200,
            httpVersion: nil,
            headerFields: nil
        )
        
        MockURLProtocol.requestHandler = { request in
            return (stubResponse!, data!)
        }
        
        // When
        sut?.fetchData(url: URL(string: "https://csrng.net/csrng/csrng.php?min=0&max=100")!, decodingType: [RandomNumber].self, completionHandler: { result in
            switch result {
            case .success(let data):
                XCTAssertEqual(data.first?.min, 0)
                XCTAssertEqual(data.first?.max, 50)
                XCTAssertEqual(data.first?.status, "success")
                XCTAssertEqual(data.first?.random, 45)
                
                expectation.fulfill()
            case .failure(let error):
                XCTFail()
            }
        })
        
        wait(for: [expectation], timeout: 0.5)
    }

그리고 Stub 데이터를 불러와서 이를 utf8 형식의 데이터로 변환후 MockURLProtocol의 requestHandler에서 이 데이터를 항상 내보내도록 구현해주면 끝이다!

 

이를 활용해 테스트를 해보면 네트워크 요청을 직접 하지 않고 Stub으로 선언한 데이터가 항상 반환된다. 

 

여기서 wait 함수에 0.5초를 기다리도록 해놨지만 실제 걸리는 시간 자체는 0.004초로 이전 1초 이상 걸리던 것에 비해 비약적으로 빨라진 것을 확인할 수 있다. 

 


 

여기까지 테스트 전반에 대해 쭉 정리를 해보았다. 

 

회사에서도 요새 테스트 코드 도입 / 리팩토링을 하며 만질 일이 많았고, 해당 내용을 가지고 멋쟁이 사자처럼에서 발표까지 진행하면서 다시한 번 개념적인 부분을 정리할 수 있어서 좋았다. 

 

https://ho8487.tistory.com/75

 

[iOS] Networking Test (Stub) + RxSwift

기존 프로젝트를 다시 MVVM + RxSwift로 리팩토링하면서 다시 Networking 작업을 하고 있다. 저번에는 네트워크와 관련된 테스트 코드를 짜지 못했었는데 이번에는 Stub을 이용해 네트워크에 연결되지

ho8487.tistory.com

이전에 작성한 방식은 예전에 사용하던 방식이라 이렇게 작성하게 되면 코드에 warning이 뜨게 되어 개선된 버전으로 Test Double에 해당하는 방식도 변경해봤다. 

(이때는 Stub과 Mock도 혼재해서 사용했군...)

 

Swift로 테스트를 처음 접하시는 분들에게 많은 도움이 되었으면 좋겠다.

Comments