raywenderlich - Test Driven Development Tutorial for iOS
목차 (눌러서 이동)
TDD 의 장점
Test code 는 Production code 의 작성과 함께 계속 수정되므로 개발 내용을 계속해서 따라간다. 그래서 작성한 Test 자체가 앱의 동작 방식에 대해 기술한 개발 문서가 된다.
TDD 방식으로 개발할 때 Test code 의 Code coverage 가 당연히 더 좋고, 추후에 중대한 update 를 하게 될 경우 훨씬 쉽다.
한 명이 Test code 를 작성, 다른 한 명이 Production code 를 작성하며 Pair-programming 을 적용하기 쉽고, 이로 인해 개발 속도는 빠르지만 튼튼한 앱을 만들게 된다.
Red - Green - Refactor cycle
TDD 개발은 보통 다음과 같은 흐름으로 개발한다.
가장 먼저 Red 단계로 Failing Test 를 작성한다. 원하는 동작이 구현됐을 때 통과해야할 테스트를 작성하는 것이다. 한 번에 하나의 동작만 테스트하는 것이 좋다.
Green 단계에서는 작성된 Failing Test 를 통과할 수 있도록 Production Code 를 작성한다. Test 되지 않는 코드를 최소화해야 하므로 Test 를 겨우 통과할 수 있을 정도로만 작성해야 한다.
Test 를 통과하여 원하는 동작이 구현되면 Refactoring 과정을 통해 여러 Test 들에서 중복되는 코드를 함수로 따로 정리하는 등 최적화 과정을 거친다.
모든 use case 를 처리할 수 있을 때 까지 위의 과정을 반복한다.
TDD 시작하기
숫자 변경에 대한 코드(Converter)를 작성하기 전에 실패할 Test code 를 먼저 작성한다. 새로운 Unit test class 를 선언하고 다음과 같이 작성한다.
import XCTest
@testable import Numero
class ConverterTests: XCTestCase {
let converter = Converter()
override func setUpWithError() throws {}
override func tearDownWithError() throws {}
func testConversionForOne() {
let result = converter.convert(1)
XCTAssertEqual(result, "I", "Conversion for 1 is incorrect")
}
}
1 을 로마 숫자로 변환하면 "I" 가 나와야 한다.그러므로, 이에 해당하는 Assert 구문을 작성한다.
이는 convert() 메서드를 아직 작성하지 않은 현재 상황에서실패하는 Test code 이고, 앞으로 실제 convert() 메서드가 어떻게 작성되어야 하는지에 대한 Documentation 이 된다.
따라서, 지금 작성한 "실패하는 Test" 를 만족시키는 방향으로 실제 Code 를 작성해 나가면 된다. 위의 Assert 구문을 겨우 통과할 정도의 Code 만 작성하자.
convert() 함수가 "I" 을 반환하도록 한 뒤 Test 를 실행하면 모두 통과된다.
이후에도 각 숫자들에 대해서 Failing test 를 작성한 뒤, 이를 통과시키기 위한 convert() 메서드를 작성해 나간다.
2 에 대한 converter test
func testConversionForTwo() {
let result = converter.convert(2)
XCTAssertEqual(result, "II", "Conversion for 2 is incorrect")
}
숫자만큼 "I" 를 반복하도록 작성, 3 까지 이런 방식으로 변환 가능하다.
func convert(_ number: Int) -> String {
return String(repeating: "I", count: number)
}
5에 대한 converter test (4는 특별한 처리가 필요해서 일단 건너 뛴다.)
func testConversionForFive() {
let result = converter.convert(5)
XCTAssertEqual(result, "V", "Conversion for 5 is incorrect")
}
마찬가지로 이번에 작성한 Test에 대해서만 통과할 수 있을만큼 작성한다.
func convert(_ number: Int) -> String {
if number == 5 {
return "V"
} else {
return String(repeating: "I", count: number)
}
}
6에 대한 converter test
func testConversionForSix() {
let result = converter.convert(6)
XCTAssertEqual(result, "VI", "Conversion for 6 is incorrect")
}
5를 넘는 수에 대해 "V"를 먼저 붙인 뒤 나머지에 대해 "I" 반복.
func convert(_ number: Int) -> String {
var result = ""
var localNumber = number
if localNumber >= 5 {
result += "V"
localNumber = localNumber - 5
}
result += String(repeating: "I", count: localNumber)
return result
}
10에 대한 converter test
func testConversionForTen() {
let result = converter.convert(10)
XCTAssertEqual(result, "X", "Conversion for 10 is incorrect")
}
5 에 대한 변환과 비슷하게 10 을 변환할 수 있다.
func convert(_ number: Int) -> String {
var result = ""
var localNumber = number
if localNumber >= 10 {
result += "X"
localNumber = localNumber - 10
}
if localNumber >= 5 {
result += "V"
localNumber = localNumber - 5
}
result += String(repeating: "I", count: localNumber)
return result
}
20 에 대한 converter test
func testConversionForTwenty() {
let result = converter.convert(20)
XCTAssertEqual(result, "XX", "Conversion for 20 is incorrect")
}
약간의 패턴이 드러난다. 10 을 처리하는 구문에서 if 를 while 로 바꾸어 10마다 반복하게 한다.
func convert(_ number: Int) -> String {
var result = ""
var localNumber = number
while localNumber >= 10 {
result += "X"
localNumber = localNumber - 10
}
if localNumber >= 5 {
result += "V"
localNumber = localNumber - 5
}
result += String(repeating: "I", count: localNumber)
return result
}
4에 대한 converter test
func testConversionForFour() {
let result = converter.convert(4)
XCTAssertEqual(result, "IV", "Conversion for 4 is incorrect")
}
func convert(_ number: Int) -> String {
var result = ""
var localNumber = number
while localNumber >= 10 {
result += "X"
localNumber = localNumber - 10
}
if localNumber >= 5 {
result += "V"
localNumber = localNumber - 5
}
if localNumber >= 4 {
result += "IV"
localNumber = localNumber - 4
}
result += String(repeating: "I", count: localNumber)
return result
}
9 에 대한 converter test
func testConversionForNine() {
let result = converter.convert(9)
XCTAssertEqual(result, "IX", "Conversion for 9 is incorrect")
}
func convert(_ number: Int) -> String {
var result = ""
var localNumber = number
while localNumber >= 10 {
result += "X"
localNumber = localNumber - 10
}
if localNumber >= 9 {
result += "IX"
localNumber = localNumber - 9
}
if localNumber >= 5 {
result += "V"
localNumber = localNumber - 5
}
if localNumber >= 4 {
result += "IV"
localNumber = localNumber - 4
}
result += String(repeating: "I", count: localNumber)
return result
}
Refactoring
Red, Green 과정을 거친 후 생산된 code 에서 중복을 제거하고 정리하기 위한 단계로 TDD cycle 에서 필수적인 단계이다.
Green 과정으로 생산된 convert() 메서드를 살펴보자. 중복된 형태가 보이는데, if 문들을 while 로 바꾸면 완전히 같은 형태의 while 문들이 눈에 띈다.
while 로 바꾼 뒤 한 번 더 Test 를 실행해 해당 변화가 문제를 일으키지 않는지 확인하자. 이렇게 변화에 대해 문제가 없음을 확신할 수 있는 것도 TDD 의 장점이라 할 수 있다.
"I" 에 대한 String(reapeating:,cout:) 구문도 while 문의 형태로 바꾼 뒤 Test 를 진행하자.
func convert(_ number: Int) -> String {
var result = ""
var localNumber = number
while localNumber >= 10 {
result += "X"
localNumber = localNumber - 10
}
while localNumber >= 9 {
result += "IX"
localNumber = localNumber - 9
}
while localNumber >= 5 {
result += "V"
localNumber = localNumber - 5
}
while localNumber >= 4 {
result += "IV"
localNumber = localNumber - 4
}
while localNumber >= 1 {
result += "I"
localNumber = localNumber - 1
}
return result
}
숫자에 대한 로마 숫자를 Tuple 로 저장한 뒤이를 순회하며 각 숫자에 대해 while 문의 형태를 만들자.
func convert(_ number: Int) -> String {
var localNumber = number
var result = ""
let numberSymbols: [(number: Int, symbol: String)] =
[(10, "X"),
(9, "IX"),
(5, "V"),
(4, "IV"),
(1, "I")]
for item in numberSymbols {
while localNumber >= item.number {
result += item.symbol
localNumber = localNumber - item.number
}
}
return result
}
Edge case 들에 대해 Test 를 작성하는 것 역시 중요하다. 0 은 로마 숫자로 나타낼 수 없으므로 빈 문자열을 기대하는 Test 를 작성한다. 이는 현재 작성된 convert() 메서드로 통과할 수 있다.
func testConverstionForZero() {
let result = converter.convert(0)
XCTAssertEqual(result, "", "Conversion for 0 is incorrect")
}
큰 수에 대한 Test 를 작성해보자.
func testConverstionFor3999() {
let result = converter.convert(3999)
XCTAssertEqual(result, "MMMCMXCIX", "Conversion for 3999 is incorrect")
}
천 단위까지의 로마 숫자를 convert() 메서드의 Tuple 에 mapping 시킨다.
let numberSymbols: [(number: Int, symbol: String)] =
[(1000, "M"),
(900, "CM"),
(500, "D"),
(400, "CD"),
(100, "C"),
(90, "XC"),
(50, "L"),
(40, "XL"),
(10, "X"),
(9, "IX"),
(5, "V"),
(4, "IV"),
(1, "I")]
그런데, 사실 이번 Test 에서는 TDD 의 규칙에 잘 맞지 않는 방식으로 convert() 메서드를 작성 (TDD flow 의 Green 단계) 했다.
Green 단계에서는 Red 에서 작성된 실패한 Test 를 겨우 통과할 만큼만 작성해야 한다.
즉, 3999 에 대한 Test 를 작성하면서 10 부터 1000 까지 여러 로마 숫자를 mapping 했는데, 40, 50, 100, 400, 500 에 대한 정보는 Test 에서 사용되지 않는다. (실제로 해당 원소를 주석 처리하고 Test 해도 통과된다.) TDD 원칙대로 작성한다면 1000, 900, 90 에 대한 mapping 만 추가하면 된다.
현재 작성된 테스트만 통과할 수 있도록 코드를 쓴다면 어떤 부분이 Test 되지 않았는지 고민할 필요도 없고, 단일한 Test 에 대해 코드를 작성하므로 단기적인 개발 목표가 명확해진다.
이런 이유로 TDD 개발이 명료한 개발 문서의 이점과 좋은 Code coverage를 보여주는 장점을 가진다는 것 이다.
주석 처리된 원소들을 Cover 하는 Test 들을 작성하자.
func testConverstionFor40() {
let result = converter.convert(40)
XCTAssertEqual(result, "XL", "Conversion for 40 is incorrect")
}
func testConverstionFor50() {
let result = converter.convert(50)
XCTAssertEqual(result, "L", "Conversion for 50 is incorrect")
}
func testConverstionFor100() {
let result = converter.convert(100)
XCTAssertEqual(result, "C", "Conversion for 100 is incorrect")
}
func testConverstionFor400() {
let result = converter.convert(400)
XCTAssertEqual(result, "CD", "Conversion for 400 is incorrect")
}
func testConverstionFor500() {
let result = converter.convert(500)
XCTAssertEqual(result, "D", "Conversion for 500 is incorrect")
}
'iOS 개발 > App 개발 관련' 카테고리의 다른 글
[iOS] Stanford iOS Lecture - MVVM (cs193p) (0) | 2021.08.31 |
---|---|
[iOS] 모의 URLSession 으로 네트워킹 Test 하기 (0) | 2021.07.30 |
[iOS] WWDC 2019 Testing in Xcode (0) | 2021.07.12 |
[iOS] Unit & UI Testing (0) | 2021.07.10 |
[iOS] MVC 구조의 앱을 MVVM 으로 바꾸기 (0) | 2021.06.25 |