iOS - Unit Test
iOS 환경에서의 Unit Test를 공부해보자!
Figuring Out What to Test
일반적으로 테스트는 다음과 같은 것들을 다룬다:
- 핵심 기능: 모델 클래스, 메소드 그리고 이들과 controller와의 상호작용.
- 가장 흔한 UI 흐름(Workflows)
- 경계 조건
- 버그 수정
만약, 기존의 앱을 확장하는 것이 목표라면 변경할 모든 요소들에 대해서 테스트를 작성해야 한다.
Best Practices for Testing
두문자어 FIRST
로 효율적인 테스트를 위한 기준들을 설명할 수 있다.
Fast
: 테스트는 빠르게 진행되어야 한다.Independent/Isolated
: 테스트는 서로 상태를 공유해선 안된다.Repeatable
: 테스트는 수행할 때마다 항상 같은 결과를 내야한다. 외부 데이터 제공자와 동시성 문제는 일시적인 오류를 유발할 수 있다.Self-validating
: 테스트는 완전히 자동이어야 한다. 로그 파일에 대한 개발자의 해석에 의존하기 보단 결과는 항상 “성공” 혹은 “실패” 가 되어야 한다.Timely
: 이상적으로, 테스트는 프로덕션 코드를 작성하기 전에 작성되어야 한다. (Test-Driven Development)
FIRST
원칙을 따르면 깔끔하고 유용한 테스트를 유지할 수 있다.
Getting Started
BullsEye
프로젝트로 테스트를 진행할 것이다.
Unit Testing in Xcode
Test Navigator는 테스트를 위한 가장 쉬운 방법을 제공한다. 이를 이용해 테스트 target을 생성하고 테스트를 실행할 수 있다.
Creating a Unit Test Target
프로젝트를 열고 command+6
을 눌러 test navigator을 열어라. 왼쪽 아래의 +
버튼을 누른 후 New Unit Test Target…
를 선택한다.
프로젝트 이름+Tests
라는 default 이름이 생성된다. 테스트 번들을 열어보자. 자동으로 번들이 생성되지 않으면 다른 navigator에 갔다가 돌아오면된다.
기본 템플릿은 XCTest
framework를 import하고 XCTestCase
의 서브 클래스를 정의한 후, setUp(),tearDown()
과 함께 예시 테스트 메소드를 정의한다.
테스트를 실행하기 위한 방법은 3가지가 있다.
Product -> Test
혹은command+U
: 모든 테스트 클래스를 실행한다.- 테스트 네비게이터의 화살표 버튼을 클릭.
- gutter의 다이아몬드 버튼을 클릭.(gutter은 코드 라인 번호가 적인 부분이다)
또한, 다이아몬드 버튼을 눌러 테스트 메소드를 따로 실행할 수 있다. 3가지 방식으로 테스트를 실행시켜 보자.
모든 테스트가 성공적으로 끝나면 다이아몬드는 체크 마크와 함께 초록색이 채워진다. testPerformanceExample()
의 끝에 있는 회색 다이아몬드를 누르면 수행 결과를 볼 수 있다,
Using XCTAssert to Test Models
먼저 XCTestAssert
를 이용해서 프로젝트 모델의 핵심 기능을 테스트 할 것이다. BullsEyeGame
객체가 각 라운드의 점수를 정확하게 계산하는지 테스트 해보자.
BullsEyeTests.swift
의 import문 아래에 다음과 같은 코드를 추가하자.
@testable import BullsEye
이는 unit 테스트가 BullsEye
의 내부 타입과 함수에 접근할 수 있게 해준다.
BullsEyeTests
클래스 맨 위에 다음의 프로퍼티를 추가하자.
var sut: BullsEyeGame!
위 코드는 BullsEyeGame
을 위한 placeholder를 생성하고 이는 SUT(System Under Test)
, 즉 테스트와 연관된 테스트 케이스 클래스 객체이다.
이제 setUp()
에 다음 코드를 추가하자.
super.setUp()
sut = BullsEyeGame()
sut.startNewGame()
이는 클래스 수준의 BullsEyeGame
객체 생성한다. 이제 이 테스트 클래스의 모든 테스트는 SUT
객체의 프로퍼티와 메소드에 접근할 수 있다. 여기서는 startNewGame()
을 호출하여 targetValue
를 초기화 한다. 많은 테스트에서 게임이 점수를 정확히 계산하는지 테스트하기 위해 targetValue
를 사용할 것이다.
또한 잊지말고 SUT 객체를 release 해주자. tearDown()
에 다음을 추가하자:
sut = nil
super.tearDown()
Writing Your First Test
이제 본격적으로 테스트를 작성해보자! BullsEyeTests` 마지막에 다음 코드를 추가하자:
func testScoreIsComputed() {
// 1. given
let guess = sut.targetValue + 5
// 2. when
sut.check(guess: guess)
// 3. then
XCTAssertEqual(sut.scoreRound, 95, "Score computed from guess is wrong")
}
테스트 메소드의 이름은 항상 test로 시작해야하고, 무엇을 테스트 하는지에 대한 설명이 있어야 한다.
Given
, When
, Then
섹션으로 테스트 형식을 구성하는 것은 테스트를 구성하는데 좋은 연습이 될 것이다.
Given
: 여기선 필요한 값들을 지정할 것이다. 이 예제에선guess
값을 생성하여targetValue
와 얼만큼의 차이가 있는지 알 수 있다.When
: 여기선 테스트될 코드를 수행한다:Call(guess:)
를 호출한다.Then
: 이 섹션은 기대하는 결과와 테스트 실패시 출력된 메시지를 선언할 섹션이다. 예제의 경우sut.scoreRound
는 항상 95가 되어야 한다.
이 게임은 100에서 targetValue와 guess 값의 차만큼을 뺀 값이 한 라운드의 점수로 주어진다. ex) targetValue = 98, guess 90 → roundScore = 92
이제 다이아몬드 버튼을 눌러 테스트를 실행해보자. 빌드하고 앱을 실행시킬 것이며, 다이아몬드 아이콘은 초록색 체크로 변할 것이다.
Debugging a Test
BullsEyeGame
에는 사실 의도된 버그가 있고, 우리는 이 버그를 찾는 연습을 할 것이다. 먼저 버그를 보기 위해서 targetValue
에서 5를 빼는 테스트를 생성할 것이다.
다음 테스트를 추가하자:
func testScoreIsComputedWhenGuessLTTarget() {
// 1. given
let guess = sut.targetValue - 5
// 2. when
sut.check(guess: guess)
// 3. then
XCTAssertEqual(sut.scoreRound, 95, "Score computed from guess is wrong")
}
여기서도 guess
와 targetValue
는 5만큼 차이가 나고 따라서 점수는 95가 되어야 한다.
Breakpoint Navigator
에서 Test Failure Breakpoint
를 추가하자. 이제 테스트 메소드가 실패할 경우 테스트 실행이 중단된다.
테스트를 실행하면 XCAssertEqual
라인에서 멈출 것이다.
sut
과 guess
를 디버그 콘솔에서 확인해보자:
guess
는 targetValue-5
인데 scoreRound
는 95
가 아니라 105
다!
더 자세히 살펴보기 위해서, 일반적인 디버깅 프로세스를 사용하자: when
에 breakpoint를 설정하고 BullsEyeGame.swift
내부에 있는 check(guess:)
의 difference
를 생성하는 곳에도 breakpoint를 지정하자. 테스트를 다시 실행하고 difference
의 값을 살펴보기 위해 let difference
가 생성되는 과정을 단계별로 살펴보자:
문제는 difference
가 음수가 되어 점수가 100 - (-5)
로 계산되는 것이다. 이를 고치기 위해 difference
의 절댓값을 구하도록 해야한다. abs()
함수를 이용해 수정하자. breakpoint를 제거하고 테스트를 다시 실행하면 테스트가 성공적으로 진행되는 것을 확인할 수 있다.