-
IOS 단위 테스트 살짝 맛보기Apple🍎/Test 2023. 12. 13. 23:21
Unit test 예제
- Xcode 실행 -> File | New | Project.
- iOS | Application | App 에서 Next
- ProductName : FirstDemo / Interface : Storyboard / Language : Swift
- Include Tests 체크 Next
- FirstDemoTests 폴더에서
- FirstDemoTests.swift 선택해서 editor에 띄우기
- 가장 먼저 test framework와 test의 대상이 되는 target을 import 해주어야한다.
- 모든 test case는
XCTest
프레임 워크의 import가 필요하다.XCTest
는XCTestCase
클래스와 Assertion을 정의한다.
- Test의 대상이 되는 모듈인
FirstDemo
를 import 해주어야한다.- FirstDemo App에 대한 모든 코드 ( class, enu, method 등) FirstDemo 모듈에 들어있다.
- 현재 테스트하는 위치는 FirstDeomTests이기 때문에 FirstDemo의 코드를 테스트하기 위해서는 import 가 필요하다.
@testable
키워드를 사용해 모듈을 가져오면 테스트 케이스에서 import 하는 모듈 내부요소에 접근이 가능하다.
import XCTest @testable import FirstDemo
- 테스트 작성을 위한 클래스를 만드는데 이때
XCTestCase
를 상속받는다.
class FirstDemoTests: XCTestCase {
- 클래스를 열면 가장 먼저 다음 두 메서드 스니펫을 볼 수 있다.
setUpWithError( )
: 각 테스트 메서드가 실행되기 전에 호출되는 메서드이다. 따라서 각 테스트가 실행되기전에 먼저 실행되어야하는 코드들을 집어넣는다.tearDownWithError( )
: 각 테스트 메서드가 실행된 이후에 호출되는 메서드다. 따라서 각 테스트가 실행된 이후 실행되어야하는 코드들을 집어넣는다.
override func setUpWithError( ) throws { // Put set up code here ... } override func tearDownWithError( ) throws { // Put teardown code here... }
- 다음 두가지 메서드는 Apple에서 제공하는 테스트 템플릿이다.
func testExample() throws
: 첫번째 메서드는 일반적인 유닛 테스트이다. 기본적으로 테스트를 할 때 이러한 종류의 메서드를 많이 사용하게 된다.func testPerformanceExample() throws
: 두번째 메서드는 성능측정을 위한 성능 테스트이다. 걸리는 시간이 중요한 메서드나 함수를 테스트하는데 사용한다.self.measure
클로저에 넣은 코드를 10번 호출해서 걸린시간의 평균값을 구한다.- 성능 테스트는 복잡한 알고리즘을 구현 또는 개선한 이후 성능이 저하되지 않았는지 확인하는 데 유용할 수 있다.
func testExample() throws { // This is an example of a functional test case. // Use XCTAssert and related functions to ... } 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. } }
- 앞으로 작성할 모든 테스트 메서드 앞에는
test
를 접두사로 붙여야한다. 그렇지 않으면 테스트 실행시 해당 항목을 찾아서 실행할수가 없다. test
접두사 유무에 따라 test 실행여부가 달라지는 점을 이용하여 테스트가 더이상 필요없는 메서드의test
를 떼면 테스트 비활성화가 가능하다.
- 그럼 테스트를 하기 전에 테스트를 할 코드를 먼저 작성해 보자.
- FirstDemo | ViewController에 위치한 numberOfVowels는 주어진 단어의 모음 개수를 세서 반환하는 메서드다.
func numberOfVowels(in string: String) -> Int { let vowels: [Character] = ["a", "e", "i", "o", "u", "A", "E", "I", "O", "U"] var numberOfVowels = 0 for character in string { if vowels.contains(character) { numberOfVowels += 1 } } return numberOfVowels }
- String 문자열을 받아 Int 모음 개수를 반환하는 메서드
- 알파벳 대문자, 소문자 모음으로 이루어진 Character array vowels 만들기
- 모음 개수를 저장할 numberOfVowels 변수 만들기
- 반복문으로 입력 받은 문자열(String)을 character 변수로 받아 한글자씩 가져와서
- vowels array에 포함되어 있으면 모음 개수 카운팅 +1
- 반복문으로 모두 순회한 후에 모음 개수 반환
- 이제 위에 대한 테스트를 작성해보면 FirstDemoTests | FirstDemoTests class
- test(접두사) _ 테스트할 대상 _ 주어진 입력 _ 예상되는 출력
- test_ numberOfVowels 메서드에 _ ‘Dominik’이 입력으로 주어지면 _ 결과값이 3이나와야한다.
- numberOfVowels 메서드를 사용하기 위해서는
- ViewController 객체 만들어서 해당 객체에 붙어있는 메서드 사용
XCTAssertEqual(결과값, 예상값, 실패시출력값)
func test_numberOfVowels_whenGivenDominik_shouldReturn3( ) { let vc = ViewController( ) let result = vc.numberOfVowels( in: "Dominik") XCTAssertEqual(result, 3, "Expected 3 vowels in 'Dominik' but got \(result)" ) }
- Prouduct | Test 또는 Command + U 를 통해 프로젝트를 컴파일하고 테스트를 돌리면
- 왼쪽에 초록색 다이아몬드가 뜨면 테스트가 통과했다는 것을 나타낸다.
- 작성한 메서드가 테스트를 통과해 의도대로 동작하는 것을 확인 했으니 좀더 Swifty 한 방법으로 메서드를 변경해보자.
func numberOfVowels(in string: String) -> Int { let vowels: [Character] = ["a", "e", "i", "o", "u", "A", "E", "I", "O", "U"] return string.reduce(0) { $0 + (vowels.contains($1) ? 1 : 0) } }
- swift에 reduce 메서드는 주어진 컬렉션(요소의 집합)의 모든 항목을 단일 값으로 결합할 수 있게 해준다.
- 위의 경우에는 주어진 컬렉션이 문자열(String)이고 각 항목이 문자(Character)이다.
- reduce( ) 안에 들어있는 0은 문자열 개수를 세기 위한 초깃값이다.
- $0은 누적자로서 초깃값 0부터 시작해서 모음을 찾을 때마다 하나씩 값이 ‘누적’ 된다.
- $1은 Collection의 현재 요소 (String의 Character)를 나타낸다.
- vowels.contains($1) ? 1 : 0 현재 요소가 참이면 누적자에 1을 더하고 거짓이면 0을 더한다.
UI test는 일단 끄기
- 테스트를 돌리다보면 Xcode가 FirstDemoTests 뿐만아니라 FirstDeomUITests target 또한 실행하는 것을 볼 수 있다.
- UItest는 실행이 오래걸리기 때문에
command + U
단축키를 눌러 테스트를 실행할때마다 UItest를 UnitTest와 같이 실행하지 않도록하자.
- scheme selection에 들어가서 Edit Scheme 선택하기
- 왼쪽에서 Test 선택하고 FirstDemoUITests target 체크 표시 지우기
위의 과정을 거치면 이 scheme 에 대한 UITest가 비활성화되어 테스트의 실행속도가 빨라지는 것을 볼 수 있다.
XCTest 프레임워크에 있는 Assert 함수들
- 각 테스트 내에서는 동작이 예상대로 잘 이루어지고 있는지 확인하는 절차가 필요하다.
- XCTAssert 함수를 이용하면 Xcode에게 예상되는 동작이 무엇인지를 알려줄 수 있다.
- XCTAssert 함수를 포함하지 않는 테스트는 검증을 위한 비교 대상이 없기 때문에 항상 통과한다.
자주 사용되는 Assert func
- XCTAssertTrue(::file:line:): 이 표현식은 항상 참인지 확인.
- XCTAssert(::file:line:): XCTAssertTrue(::file:line:) 와 동일.
- XCTAssertFalse(::file:line:): 이 표현식은 항상 거짓인지 확인.
- XCTAssertEqual(::_:file:line:): 두 표현식은 동일한지 확인.
- XCTAssertEqual(::accuracy:_:file:line:): 정확도 매개변수에 넣은 정확도를 고려해서 두 표현식이 동일한지 확인
- XCTAssertNotEqual(::_:file:line:): 두 표현식이 다른지 확인 .
- XCTAssertNotEqual(::accuracy:_:file:line:): 정확도 매개변수에 넣은 정확도를 고려해서 두 표현식이 다른지를 확인 .
- XCTAssertNil(::file:line:): 표현식이 nil인지 확인.
- XCTAssertNotNil(::file:line:): 표현식이 nil이 아닌지를 확인.
- XCTFail(_:file:line:): 항상 실패하는지를 확인
커스텀 Assert func 만들기
- 기본으로 제공되는 Assert func이 충분하지 않은 경우 커스텀한 Assert 를 만들 수 있다.
- 예를 들어 두 dictionary가 주어졌을 때 동일한 contents를 가지고 있나를 확인하는 test가 있을 때 출력은 다음과 같다.
func test_dictsAreQual() { let dict1 = ["id": "2", "name": "foo"] let dict2 = ["id": "2", "name": "fo"] XCTAssertEqual(dict1, dict2) // Log output: // XCTAssertEqual failed: ("["name": "foo", "id": "2"]")... // ...is not equal to ("["name": "fo", "id": "2"]") }
- 위와 같이 dictionary에 요소들이 얼마 없을 때는 log 에서 뭐가 다른지를 쉽게 찾을 수 있지만 요소가 엄청 많아지게 되면 위와 같은 log에서는 어떤 요소들이 다른지를 찾기 어려울 것이다.
- 따라서 많은 요소들이 있을 때도 어떤 것들이 다른지를 찾아서 출력해주는 Assert 함수를 따로 정의해서 사용할 수 있다.
- 동일한 Test Target에 DDHAssertEqual func을 추가해보자.
func DDHAssertEqual<A: Equatable, B: Equatable> ( _ first: [A:B], _ second: [A:B]) { if first == second { return } for key in first.keys { if first[key] != second[key] { let value1 = String(describing: first[key]!) let value2 = String(describing: second[key]!) let keyValue1 = "\"\(key)\": \(value1)" let keyValue2 = "\"\(key)\": \(value2)" let message = "\(keyValue1) is not equal to \(keyValue2)" XCTFail(message) return } } }
- DDHAssertEqual 메서드는 파라미터로 딕셔너리 두개를 받아 둘이 같은지를 확인한다.
- 제네릭을 활용하여 딕셔너리의 key, value를 지정하여 다양한 key, value로 되어 있는 딕셔너리들에 대해서 사용할 수 있게 한다.
- <A: Equatable, B: Equatable > : generic 으로 파라미터를 받아도 key, value에 ==, != 같은 비교연산이 가능하기 위해서는 A, B가 String, Int 같은 기본 타입이거나 Equatable protocol을 채택하고 있어야한다.
- value 값이 generic이기 때문에 Optional이 들어올 수 있어 ! 언래핑
- if first == second : 두 딕셔너리가 동일할 경우 그냥 return 테스트 통과
- 다를 경우 루프를 돌리는데 첫번째 딕셔너리의 key들을 하나씩 순회한다.
- 해당 key 값을 각 딕셔너리에 꽂았을 때 나오는 value가 틀린 경우 즉시 순회를 중단하고 key와 각 value 값을 message로 만들어서
- XCTFail(message) : 테스트 실패와 함께 테스트 실패 이유가 담긴 message를 내보낸다.
- 커스텀으로 만든 DDHAssertEqual func을 테스트 메세드에서 사용하면 테스트 메서드내에서는 실패 경로만 표기하고 실제 실패지점은 Assert함수인 XCTFail에 찍히는 것을 볼 수 있다.
- 테스트는 테스트 메서드 내에서 성공, 실패 여부가 결정되어야한다. 따라서 실패 지점을 테스트 메서드 내로 옮겨 보자.
func DDHAssertEqual<A: Equatable, B: Equatable>( _ first: [A:B], _ second: [A:B], file: StaticString = #filePath, // << new line: UInt = #line) { // << new if first == second { return } for key in first.keys { if first[key] != second[key] { let value1 = String(describing: first[key]!) let value2 = String(describing: second[key]!) let keyValue1 = "\"\(key)\": \(value1)" let keyValue2 = "\"\(key)\": \(value2)" let message = "\(keyValue1) is not equal to \(keyValue2)" XCTFail(message, file: file, line: line) // << new return } } }
- file과 line 파라미터를 새로 추가하였으며 file의 기본값은 # filePath, # line 이다.
- 테스트 메서드 내에서 직접 만든 DDHAssertEqual을 호출했을 때 위에서 새로 추가한 filePath, line은 파일의 경로와 호출 지점으로 Assert func에 전달된다.
- 이제는 앞서 XCFail 지점에서 표시되던 실패가 테스트 매서드내의 XCHAssertEqual을 호출한 지점에서 표시되는 것을 볼 수 있다.
커스텀 Assert func을 만드는 것은 테스트 코드의 가독성을 향상시킬 수 있지만 따로 작성한 코드인 만큼 유지보수의 대상이 되는 것을 잊으면 안된다.
테스트의 다른 종류
단위 테스트 (Unit Test )
- 가장 기본이 되는 테스트이다.
- 유닛 테스트는 좁은 범위에 대해 특정 기능을 타겟팅해서 검증한다.
- 각 테스트의 목적이 확실함으로 이해가 쉽다.
통합 테스트 (Integration tests )
- 결국 어플리케이션은 개별 코드가 서로 상호작용하며 작동하기 때문에 이전에 격리해서 진행했던 단위 테스트를 모두 통과하였어도 각 서로다른 단위들이 모두 통합되었을 때도 의도하는 대로 동작하는지 확인하는 과정이 필요하다.
- 통합 테스트는 실제 데이터베이스 쿼리를 실행하고 서버에서 데이터를 가져오므로 단위 테스트보다 속도가 느릴 수 밖에 없다.
- 단위 테스트 처럼 자주 실행하지는 않는다.
- 테스트하는 범위가 넓기 때문에 Fail 에 대한 이해가 유닛 테스트보다 어렵다.
사용자 인터페이스 테스트 (UI Test)
- 앱의 UI에 대한 테스트이다.
- 사용자가 UI를 통해 원하는 동작을 실행하는 것처럼 프로그램인 test runner가 앱을 사용한다.
- 어떤 동작 수행시 화면에 정상적으로 출력이 나오는지를 확인하는 테스트이기 때문에 UI Test를 위해서는 테스트하려는 요구사항이 화면에 나와야한다.
- test runner가 애니메이션, 화면 업데이트를 기다려야하는 경우가 많아 테스트 시간이 오래 걸린다.
스냅샷 테스트 ( Snapshot Test)
- UI를 이전에 찍은 스냅샷과 비교하는 테스트이다.
- 설정한 픽셀 비율과 다른 경우 테스트는 실패한다.
- 하나의 앱 화면 UI가 완성되어 있는 상황에서 어떤 테스트 데이터를 주었을 때 UI가 변경되지 않아야되는 상황을 확인할 때 적절한 테스트이다.
직접 테스트 ( Manual Test)
- 아무리 많은 테스트를 거쳤어도 최종 단계에서는 사람이 직접 검수하는 과정이 필요하다.
- 앱의 베타 버전을 만들고 테스터들에게 피드백을 받는 과정이 필요하다.
GitHub - PacktPublishing/Test-Driven-iOS-Development-with-Swift-Fourth-Edition: Test-Driven iOS Development with Swift - Fourth
Test-Driven iOS Development with Swift - Fourth Edition, published by Packt - GitHub - PacktPublishing/Test-Driven-iOS-Development-with-Swift-Fourth-Edition: Test-Driven iOS Development with Swift ...
github.com
'Apple🍎 > Test' 카테고리의 다른 글
TDD가 뭔지 한번 해보기 (0) 2023.12.15 TDD를 포기하는 이유와 그럼에도 해야하는 이유 (0) 2023.12.13