테스트 주도 개발은(TestDrivenDevelopment)은 매우 짧은 개발 사이클을 반복하는 software 개발 프로세스 중 하나이다. 개발자는 새로운 함수를 정의하는 자동화된 TestCase를 먼저 작성한다. 이후에 TestCase를 통과하기 위한 최소한의 양의 코드를 생성한다. 그리고 마지막에 해당 코드들을 표준에 맞도록 refactoring 하는 것이 바로 TDD 개발 방법이다. - wiki백과
사전적인 설명만으로는 충분한 이해가 되지 않을 수 있다.
기존의 개발 프로세스와 TDD의 차이를 간단하게 아래의 그림으로 비교해보자.
기존의 개발의 프로세스들은 설계를 한 뒤 코드를 작성하고 이후에 테스트를 진행하고 나서 다시 설계를 수정하는 방식이었다면, TDD 프로세스의 경우에는 설계를 한 뒤 테스트 코드를 작성하고 테스트 하기 때문에 설계의 문제로 인한 오류 개선 속도가 기존 개발 프로세스보다 빠르다고 할 수 있다. 하지만 이것만으로는 아직까지 정확히 TDD가 무엇이고, 왜 사용하면 좋은지에 대해서는 잘 모르겠다. 앞으로의 포스팅에서 알아보도록 하자.
TDD는 아래의 그림과 같은 cycle을 따른다. 하나씩 알아보도록 하자.
TDD 개발에서의 첫 번째 단계는 실패이다. 즉, 실패하는 테스트 케이스를 작성하는 것이다. 이때 테스트 케이스는 프로젝트 전체의 모든 기능에 대해서 모든 케이스를 작성하는 것이 아니라, 먼저 구현할 기능에 대해서 테스트 케이스를 하나씩 작성해 나가는 것이다.
❖ 물론 상황에 따라서 한 번에 여러 개의 테스트 케이스를 작성하는 경우도 있다.
두 번째 단계는 성공이다. 실패 단계에서 작성한 테스트 케이스를 통과시키기 위한 최소한의 코드를 작성하는 단계이다.
세번째 단계는 리팩토링이다. 성공 단계에서 작성한 코드에 중복되는 코드나, 개선시킬 방법이 있다면 리팩토링을 진행해준다. 리팩토링을 진행하면서 매 단계마다 테스트 케이스를 통과하는지 확인해야한다. 이 단계까지 구현이 완료되었다면 다시 첫번째 단계로 새로운 실패하는 테스트 케이스를 작성하면 된다.
단위테스트(Unit Test)는 컴퓨터 프로그래밍에서 소스 코드의 특정 모듈이 의도된 대로 정확히 작동하는지 검증하는 절차이다. 즉, 모든 함수에 대한 테스트 케이스(TC; TestCase)를 작성하는 절차를 말한다. 각 테스트 케이스는 서로 분리 되어야 한다. 이를 통해서 언제라도 코드 변경으로 인해 문제가 발생할 경우, 단시간 내에 이를 파악하고 바로 잡을 수 있도록 해준다. -wiki백과
TDD를 설명하다가 갑자기 분위기 단위테스트라니 뜬금 없을 수도 있겠지만, 사실은 TDD의 첫 단계인 실패하는 테스트 코드를 작성하는 단계에서 기능 단위의 테스트 코드를 작성하는 것이 단위테스트라고 할 수 있다. 물론 단위테스트는 TDD처럼 꼭 테스트 코드를 먼저 작성하거나, 리팩토링을 하지는 않는다. 단지 순수하게 테스트 코드를 작성하는 것을 의미한다.
이러한 단위테스트를 작성하는 것에는 여러 장점이 있는데 간단히 알아보자.
단위테스트를 작성하는데에 있어서 FIRST 속성을 지킨다면 좋은 단위테스트를 작성할 수 있다.
FIRST : 테스트에 대한 모범 사례 (Best Practices for testing)
개발자들이 직접 작성한 코드라고 하더라도 단순히 메서드, 클래스의 코드들을 보면서 숨어있는 버그를 찾아내는 것은 거의 불가능하다. 물론 코드를 직접 읽어보면서 찾는 것도 좋은 방법이긴 하지만 효율적인 단위테스트를 통해서 버그를 찾아내는 것이 훨씬 효율적인 방법이 아닐까 싶다. 그렇다면 효율적인 단위테스트를 위해서는 어떠한 것을 고려해야할까?
바로 좋은 테스트 케이스들이 있다면 효율적으로 코드들을 테스트 할 수 있을 것이다.
아래의 Right - BICEP는 무엇을 테스트할지에 대해서 쉽게 선별할 수 있도록 도와준다.
Right - 결과가 올바른가?
Xcode에서 TDD 프로세스에 맞춰서 개발을 하고 싶은 경우에는 프로젝트를 생성할 때 부터 아래의 그림처럼 Include Tests에 체크를 하면 된다.
그럼 아래의 그림과 같이 Unit test class들이 생성된 채로 프로젝트가 생성되는 것을 확인할 수 있다.
만약 처음에는 테스트를 할 생각이 없어서 체크하지 않고 프로젝트를 만들었어도 나중에 Unit test class와 target을 추가할 수 있다. 아래의 그림에서 Xcode Test Navigator를 누르고 아래의 +
버튼을 눌러서 New Unit Test Target을 설정하면 된다.
이후에 기본적으로 생성되는 테스트 코드들과 함께 테스트 클래스가 생성된다.
XCTest
를 import하고 CalculatorTests
를 XCTestCase
의 하위 클래스로 정의하며, setUp()
, tearDown()
과 같은 테스트 메소드를 정의한다.
이때 테스트 클래스를 실행하는데에는 3가지 방법이 있다고 한다.
Product\Test
또는 Command + U
를 사용해서 모든 테스트 클래스를 실행한다.모든 테스트가 성공하면, 다이아몬드는 녹색으로 바뀌게 되고 체크 표시가 나타난다. testPerformanceExample()
의 끝에 있는 회색 다이몬드를 클릭하면 성능 결과를 확인해 볼 수 있다.
만약 성능 테스트가 필요한 것이 아니라면 testPerformanceExample()
은 삭제해도 된다.
프로젝트에서 Model을 테스트 하기 위해서는 XCTAssert의 여러 함수들을 사용하게 된다. 앞으로 포스팅에서 예시로 나오는 프로젝트는 계산기 프로젝트이다. 이를 염두하고 포스팅을 보면 될 것 같다.
먼저 Calculator
모델의 핵심 기능을 XCTAssert를 사용해서 테스트한다. CalculorTests.swift 파일에 import 구문 아래에 다음과 같은 줄을 추가해준다. 이를 통해서 Unit Test가 Calculator의 클래스와 메소드에 접근할 수 있게 해준다.
@testable import Calculator
다음은 CalculatorTests 클래스의 프로퍼티를 추가해 준다. 이때 객체를 만드는 부분은 setUp에서, 해제해주는 부분은 tearDown()에서 진행해주면 된다. 그 이유는 다음과 같다.
모든 테스트가 깨끗한 상태(state)로 시작하는지 확인하기 위해서
setUp()
에서 SUT(System Under Test) 객체를 생성하고,tearDown()
에서 해제하는 것이 가장 좋은 방법이기 때문이다.
import XCTest
@testable import Calculator
final class CalculatorTests: XCTestCase {
private var sut: BianryCalculator!
override func setUp() {
sut = BianryCalculator()
super.setUp()
}
override func tearDown() {
sut = nil
super.tearDown()
}
}
그렇다면 setUp()
과 tearDown()
은 무엇을 해주는 메소드 일까?
네이밍에서 유추할수 있듯이 setUp()
은 test case에 있는 각각의 test method를 실행하기 전에 모든 상태를 reset해주는 함수이다. tearDown()
은 test case에 있는 각각의 test method들이 끝나고 난 뒤에 cleanup을 수행해주는 함수이다.
이제는 테스트 코드를 작성해 볼 차례인데, 테스트 메소드의 이름은 항상 test
로 시작해야만하며, 뒤에 무엇을 테스트하는지 설명해준다. 또한 test 코드 안에는 given, when, then
섹션으로 테스트 형식을 지정하는 것이 좋다.
given
섹션에서는 필요한 모든 값을 설정한다.when
섹션에서는 테스트 중인 코드를 실행한다. then
섹션에서는 예상한 결과를 확인하며, 이 경우에는 계산 결과가 일치하는지 확인하여 실패하는 경우에 메세지를 출력한다.import XCTest
@testable import Calculator
final class CalculatorTests: XCTestCase {
private var sut: DecimalCalculator!
override func setUp() {
sut = DecimalCalculator()
super.setUp()
}
override func tearDown() {
sut = nil
super.tearDown()
}
func testAddition() throws {
//1. given (formula : 13 + 5)
try! sut.enterNumber("1")
try! sut.enterNumber("3")
try! sut.add()
try! sut.enterNumber("5")
//2. when
let result = try! sut.equal()
//3. then
XCTAssertEqual(result, "18", " ⚠️ add function is wrong❗️")
}
}
Given - When - Then
테스트의 구조는 클라이언트 친화적인 행동 주도적인 개발(BDD: Behavior Driven Development)에서 시작되었으며 은어 (low-jargon)의 명칭이다. 이를 대체할 시스템 이름은 Arrange-Act-Assert
와 Assemble-Activate-Assert
라고 할 수 있다.
XCTAssert
로 시작하는 함수들은 모두 테스트의 정의들이며 이러한 테스트 함수들을 종류가 매우 많다. 대부분 함수 이름을 보면 어떠한 용도인지 파악이 가능할 것이다.
XCTAssertNil(a1, format...)
, XCTAssertNotNil(a1, format...)
, XCTAssert(a1, format...)
, XCTAssertTrue(a1, format...)
, XCTAssertFalse(a1, format...)
, XCTAssertEqualObjects(a1, a2, format...)
, XCTAssertEqual(a1, a2, format...)
, XCTAssertEqualsWithAccuracy(a1, a2, format...)
, XCTAssertThrows(expression, format...)
, XCTAssertThrowsSpecific(expression, specificException, format...)
, XCTAssertThrowsSpecificNamed(expression, specificException, specificName, format...)
, XCTAssertNoThrow(expression, format...)
, XCTAssertNoThrowSpecific(expression, specificException, format...)
, XCTAssertNoThrowSpecificName(expression, specificException, specificName, format...)
Xcode 11.4 버젼부터 Unit test를 생성하면 기본적으로 setUpWithError()
, tearDownWithError()
로 생성이 되게된다. 물론 setUp()과 tearDown()도 override 해서 사용할 수 있다.
XCTest의 호출 순서 : setUpWithError() → setUp() → tearDown() → tearDownWithError()
그렇다면 setUp, tearDown 과 차이점은 무엇일까?
차이점은 바로 함수명에서 나와있듯이 바로 Error를 던질 수 있게 바뀌었다는 점만 빼면 나머지는 다 동일하다고 한다. 즉, 코드 중에서 throws를 할 수 있는 코드들을 do-catch문이나, try? 를 사용하지 않고 try를 사용하여 setUpWithError()에서도 error를 throw하게 해준다는 것이다.
setUpWithError()에서 error를 던지지 않으면 정상적으로 실행되겠지만, error가 존재하는 경우에는 테스트 자체가 skip되게 된다. 따라서 테스트는 전부 실패한 것으로 나오게 되지만 실제로는 실행이 아예 되지 못한 것이다. setUpWithError가 호출되고 에러 코드가 나오고 그 이후에 tearDownWithError도 호출이 되게 된다.
Xcode 11.4 버젼에 같이 새로 나온 친구들이 있는데 바로 XCTSkip()*
이다. 이 XCTSkip()
struct는 메소드로 XCTSkipIf()
와 XCTSkipUnless()
를 가지고 있는데 이 친구들은 runtime에 조건에 따라서 동적으로 테스트의 스킵을 처리하게 된다. 차례대로 알아보자면
XCTSKipIf(expression, ...)
macro는 expression의 값이 true
이면 해당 test method를 skip
XCTSkipUnless(expression, ...)
macro는 expression의 값이 false
이면 해당 test method를 skip
code coverage는 테스트의 가치를 측정하는 도구라고 할 수 있다. 이를 통해서 테스터가 의도한 대로 테스트가 잘 되었는지 판단할 수 있는 자료 중 하나다.
설정하는 방법 : Product → Scheme → Edit Scheme → Options 에서 Code Coverage를 체크해주고 target을 설정해주면 된다.
이후에 test를 진행하고 나서 Report navigator에서 가장 최신의 테스한 것으로 Coverage를 클릭하면 다음과 같이 test code의 coverage를 확인할 수 있다.
그럼 각 파일들에 대해서 얼마만큼의 코드들이 실행되었는지 확인할 수 있고 >
버튼을 눌러서 더 자세하게 확인할 수 있다.
이때 해당 변수나 메소드를 클릭하게 되면 파일로 이동하며 이동하면 오른쪽에 몇 번 호출이 되었는지 확인할 수 있다. 또한 호출이 한 번도 되지 않은, 즉 테스트가 되지 않은 코드들은 아래의 그림처럼 빨간색으로 표시가 된다.