iOS 유닛 테스트를 배워보자!(1)
시작에 앞서
개인적으로 학과 수업을 통해 소프트웨어 개발에서 테스트의 중요성은 알고 있었지만 실제 개발에 어떻게 적용해야 하는지 모르는 상태였다. 마침 2019 GDG 부산 행사에서 DevOps 와 Testing 에 관한 세션이 있었고 그때 구체적으로 TDD 가 무엇인지 알게 되었다.
Test Driven Developement란
말 그대로 테스트가 개발을 주도하는 것이다.
테스트를 먼저 만들고 그 테스트를 통과하기 위한 코드를 짜는 것을 뜻한다.
보통 개발이 끝난 후 테스트를 시행하지만 다음과 같은 장점들 때문에 그 순서를 바꾼 TDD를 적용한다.
- 처음 해보는 프로그램 주제
- 고객의 요구 조건이 바뀔 수 있는 프로젝트
- 개발하는 중에 코드를 많이 바꿔야 된다고 생각하는 겨우
- 내가 개발하고 나서 이 코드를 누가 유지보수할 지 모르는 경우
- 즉 불확실성이 높을 경우
Unit Test 란
유닛 테스트(unit test)는 컴퓨터 프로그래밍에서 소스 코드의 특정 모듈이 의도된 대로 정확히 작동하는지 검증하는 절차다. 즉, 모든 함수와 메소드에 대한 테스트 케이스(Test case)를 작성하는 절차를 말한다. (by wiki)
[유닛 테스트를 하는 장점]
- 문제점 발견이 쉽다
- 리팩토링이 쉽다.
- Integration이 간단하다
- 유지 보수하기 편하다
- 협업 능력이 향상된다
TDD 과 Unit test의 비교
Unit test 는 무엇을 테스팅 하느냐의 문제이고 그에 반해 TDD는 언제 테스팅을 하느냐의 문제이다.
Unit test의 경우 코드를 작성하는 때에 관계없이 할 수 있다.
그에 반해 TDD는 테스트가 개발을 주도하게 하는 것이다.
TDD를 실천하는 방법으로 Unit test, functional test, acceptance test 가 있는 것이다.
Test Driven Development 에서 제일 중요한 것은 Driven이다.
테스트가 뭘 할지 뭐가 될지 어떤 디자인이며 어떤 API 가 될지 판단하는 것이다.
(그렇다고 해서 테스트를 먼저 작성하고 개발한다고 TDD를 실천하는 것은 아니다. 테스트를 먼저 작성하는 건 TDD를 할 수 있는 준비만 한 셈이다.)
작은것 부터 천천히 해보자!
개발의 전반적인 흐름을 다루는 TDD를 배우기 전 Unit test 부터 따라해 보려고 한다.
튜토리얼의 목표는 다음과 같다.
- Xcode Test Navigator 를 사용해 app's model 과 asynchronous 메소드를 테스트 하는 방법
- stub와 mock를 이용해 library 나 system 객체와의 가짜 인터랙션을 만드는 방법
- UI와 performance를 테스트 하는 방법
- code coverage 도구를 사용하는 방법
뭘 테스트 해야 할까
- 핵심 : 모델 클래스와 함수 그리고 컨트롤러와의 상호작용
- UI 워크 플로우
- 경계값 조건
- 고쳐진 버그
테스팅을 위한 좋은 연습 - FIRST
- F : Fast - 테스트는 빨라야 한다
- I : Independent/Isolated - 테스트는 state를 공유하지 않아야 한다
- R :Repeatable - 동일한 테스트를 여러번 반복해도 같은 결과가 나와야 한다.
- S : Self Validating - 테스트는 자동화 되어야 한다.
결과값은 프로그래머가 log file 분석을 통해 결정하는게 아닌 "통과" 나 "실패"여야 한다. - T : Timely - 테스트는 production code를 작성하기 전에 명시되어야 한다.(TDD)
Xcode 에서의 Unit Testing
Test navigator은 테스트를 쉽게 만들어 주는 도구이다.
먼저 Test navigator 로 들어가 New Unit Test target을 만들어 준다.
만들게 되면 테스팅 프레임워크를 위한 템플릿이 만들어 진다.
import XCTest
class BullsEyeTests: XCTestCase {
override func setUp() {
// Put setup code here. This method is called before the invocation of each test method in the class.
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
func testExample() {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct results.
}
func testPerformanceExample() {
// This is an example of a performance test case.
measure {
// Put the code you want to measure the time of here.
}
}
}
여기서 3가지 방법으로 테스팅을 진행할 수 가 있다.
-
Product -> Test 이나 Command-U를 통해 모든 테스트 클래스를 실행 한다.
-
Test navigator 에 표시되는 재생 버튼을 클릭한다
-
gutter 영역에 있는 다이아몬드 아이콘을 클릭한다.
3가지 방법으로 테스트를 진행해 보면 아직 테스트를 작성하지 않았기 때문에 빠르게 실행되는 걸 볼 수 있다.
모든 테스트가 끝날 경우 testPerformanceExample() 함수에 있는 마지막 회색 다이아몬트 버튼을 통해 성능 결과를 확인해 볼 수 있다.
XCTAssert 사용하기
core functiond을 테스트 하기 위해 XCTAssert를 사용해 보겠다.
BullsEyeGame 객체가 점수에 대한 반올림 값을 정확하게 계산하고 있는지 테스트 해보자.
import 선언문 아래에 해당 코드를 추가한다.
@testable import BullsEye
이코드는 unit test가 내부의 타입과 function들에 대한 접근권한을 주는 역할이다.
BullsEyeTests 클래스의 상단에 해당 변수를 추가한다.
var sut: BullsEyeGame!
이 코드는 BullsEyeGame 객체의 테스트와 관련된 placeholder를 제공한다.
( 이 객체는 System Under Test 또는 이 테스트 케이스 클래스가로 불려진다)
다음으로 setup() 함수를 수정한다.
super.setUp()
sut = BullsEyeGame()
sut.startNewGame()
이 코드는 클래스 레벨에서 BullsEyeGame 객체를 생성한다.
이 테스트 클래스의 모든 테스트 들은 이 SUT 객체의 프로퍼티와 메소드를 통해 접근 할 수 있다.
또한 targetValue로 초기화 되는 startNewGame() 메소드도 호출 할 수 있는데 점수가 정확하게 계산되는지에 대한 대부분의 테스트 들은 이 변수를 통해서 테스트 될 것이다.
먼저 아래의 코드를 통해 SUT 객체를 tearDown()에 푼다.
sut = nil
super.tearDown()
팁) SUT를 setup 메소드에서 생성하고 tearDown 메소드에서 release 하는건 모든 테스트의 시작이 초기화된 state인지 보장해줄 수 있다.
세팅은 끝났다. 첫번째 테스트 코드를 작성해 보자.
다음 포스팅에ㅎㅎ