Test라는 과정은 사실 공학 어디에서든 등장하는 개념이다. 작업에 의존도가 있고, 복잡한 과정으로 넘어갈 수록 이전 단계의 오류는 치명적이기 때문이다. 이를 관리하는 방법이 Test라 할 수 있다. 소프트웨어에서 역시 이러한 개념이 적용된다. 어떤 것인지, 왜 필요한지, 어떻게 할 수 있는지 알아보자.
일반적인 이러한 시나리오에서 Unit test가 가능한 모듈은 request를 준비하는 과정, 그리고 response를 파싱하는 과정이다.
Integration Test의 경우, 일련의 과정을 한꺼번에 확인하는 것을 말한다.
마지막으로 End-to-End Test(UI Test)는 실제 UI까지 전체적으로 보는 것을 맗한다.
사람은 실수하니까
처음 Target을 만들고 적용하면, 다음과 같은 클래스가 하나 만들어진다.
import XCTest
@testable import ExampleApp
class ExampleAppTests: XCTestCase {
override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
}
override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
func testExample() throws {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct results.
}
func testPerformanceExample() throws {
// This is an example of a performance test case.
self.measure {
// Put the code you want to measure the time of here.
}
}
}
setUpWithError()
, tearDownWithError()
: 각 test case실행 전 후 각각 실행된다.setUpWithError()
→ testA()
→ tearDownWithError()
setUpWithError()
와 tearDownWithError()
는 Xcode12에서 새로 소개됨. setUp()
, tearDown()
도 사용가능test*()
: test 로 시작하는 메소드가 test case가 된다.XCTAssert*
가 존재해야 한다.XCTAssert*
가 존재하지 않으면 해당 Test case는 성공으로 처리된다.@testable import
모든 Test case는 특정 조건(condition)으로 요약할 수 있다.
XCTAssert(result == expected, "결과와 예측이 같아야 함")
다양한 Assertion
함수가 존재한다. 실패시 어떤 이유로 실패했는지 좀더 명확하게 알 수 있다. 위 예시처럼 message를 사용하는 것도 방법이다.
category | function name |
---|---|
Equality | XCTAssertEqual , XCTAssertNotEqual |
Truthiness | XCTAssertTrue , XCTAssertFalse |
Nullability | XCTAssertNil , XCTAssertNotNil |
Comparison | XCTAssertLessThan , XCTAssertGreaterThan , XCTAssertLessThanOrEqual , XCTAssertGreaterThanOrEqual |
Erroring | XCTAssertThrowsError , XCTAssertNoThrow |
Speacial | XCTFail : 항상 실패XCTUnwrap : expression이 nil이면 실패, nil이 아니면 unwrapped value 반환 |
func testSplit_useDefaultSeparator_splitWords_Success() throws {
// Given
let text = "기본 파라미터 테스트"
// When (use default separator: space)
let result = try? split(text)
// Then
XCTAssertNotNil(result)
XCTAssertEqual(result, ["기본", "파라미터", "테스트"])
}
비동기 동작의 테스트 작성
func asyncFunction(with completion: @escaping (Int) -> Void) {
let someParallelTask = {
// write code
}
DispatchQueue.global().async {
let result = someParallelTask()
DispatchQueue.main.async {
completion(result)
}
}
}
특정 함수 실행시, global queue에서 동작 실행 후, main에서 화면을 업데이트한다고 가정한 completion code를 실행시킨다고 생각해보자. 어떻게 테스트할 수 있을까?
func test_asyncFunction() {
var result = 3.6
asyncFunction { result = $0 } // 계산 후 값을 반영
XCTAssertEqual(result, 3.6) // 결과와 예상 값 비교
}
이렇게 작성했다면, 실패한다. @escaping closure
의 실행 시점을 호출 시점에 알 수 없기 때문이다. completion이 호출되기 전 테스트가 종료된다.
func test_asyncFunction_with_expectation() {
var result = 3.6
let expectation = expectation(description: "asyncFunction")
asyncFunction { calculatedResult in
result = calculatedResult
expectation.fulfill() // 나 결과 받았어!!
}
wait(for: [expectation], timeout: 2) // 해당 스레드를 기다려줘! 2초 이후에는 time아웃!
XCTAssertEqual(result, 3.6)
}
XCTestExpectation
은 주의사하잉 있는데, 현재와 같은 흐름으로 짜는 것이 best practice이다. 즉, 비동기 task안에서 fullfill
호출하고, wait
로 기다려주고, XCTAssert
로 error, condition 확인하는 순서로 하는 것이 좋다.
또 수행시간 자체를 테스트하는 목적이라면 timeout을 test 실패 신호로 잡는 것은 괜찮지만, 이 자체를 해당 test case의 실패 신호로 잡는 것은 좋지 못하다. 해당 함수는 함수의 동작 결과를 비교하기 위한 것이기 때문이다.
XCTKVOExpectation
expectedValue
가 같아진 경우XCTNSPredicateExpectation
XCTNSNotificationExpectation
자동화된 테스트에서 Production 객체 대신 테스트를 위해 더 간단하게 동작하는 객체를 사용하는 경우
이런 방법을 사용하게 되면 복잡도를 줄이고, 독립적으로 코드 검증이 가능하게 한다.
Production 용은 아니나 동작이 구현된 객체
보통 Production 코드의 간략화된 버전으로 되어 있다.
미리 정의된 데이터를 들고 있다가 테스트 중 요청에 따라 그 값을 응답하는 객체
실제 DB나 네트워크에 접근하지 않고, 상황에 맞는 데이터를 전달한다.
수신된 요청을 기록하는 객체
Mock에 예상된 action이 수행되었는지 검증하는 방식으로 사용한다. 예를 들어 함수 호출시 count되는 flag를 달아서, 해당 flag값을 확인한 것.
실제 코드를 수행하기 싫거나 의도된 코드가 실제 수행되었는지 검증할 때 사용한다. 실제 이메일이 발송되었는지 확인하는 것은 어렵기 때문에, 해당 메소드가 호출되었는지 (count)확인하는 정도로 사용한다.
Logic과 Effect를 분리하여 구현해야 한다. (Bertrand Meyer "Object Oriented Software Construction")
func averageGrades(sudent: Student) -> Double
시스템의 상태를 변경하지 않고 결과를 리턴하는 method를 Query라 한다. 해당 메서드는 사이드 이펙트가 없이 값을 리턴한다. 이와 같은 Query 형태의 메서드를 Stub을 사용하여 Test Double할 수 있다. 즉, 값을 들고 있다가 응답하여 test할 수 있다.
func sendRemiderEmail(sudent: Student)
Command는 어떤 action을 수행하여 시스템의 상태를 바꾸면서, 값을 리턴하지 않는 경우를 말한다. 위의 예시의 경우 리턴하지 않고, 시스템의 상태를 변경하고 있다. 이런 경우는 Mock을 사용하여 테스트할 수 있다.
setUp()
, tearDown()
에서 초기화를 잘해주어야 한다.Test Target
⎣ Cases
⎣ Group 1
⎣ Tests 1
⎣ Tests 2
⎣ Group 2
⎣ Tests
⎣ Mocks
⎣ Helper Classes
⎣ Helper Extension