ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • IOS 단위 테스트 살짝 맛보기
    Apple🍎/Test 2023. 12. 13. 23:21

     

    Unit test 예제

    1. Xcode 실행 -> File | New | Project.
    2. iOS | Application | App 에서 Next
    3. ProductName : FirstDemo / Interface : Storyboard / Language : Swift
    4. Include Tests 체크 Next

    1. FirstDemoTests 폴더에서
    2. FirstDemoTests.swift 선택해서 editor에 띄우기

    • 가장 먼저 test framework와 test의 대상이 되는 target을 import 해주어야한다.
    • 모든 test case는 XCTest 프레임 워크의 import가 필요하다.
      • XCTestXCTestCase 클래스와 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
    }
    1. String 문자열을 받아 Int 모음 개수를 반환하는 메서드
    2. 알파벳 대문자, 소문자 모음으로 이루어진 Character array vowels 만들기
    3. 모음 개수를 저장할 numberOfVowels 변수 만들기
    4. 반복문으로 입력 받은 문자열(String)을 character 변수로 받아 한글자씩 가져와서
    5. vowels array에 포함되어 있으면 모음 개수 카운팅 +1
    6. 반복문으로 모두 순회한 후에 모음 개수 반환
    • 이제 위에 대한 테스트를 작성해보면 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와 같이 실행하지 않도록하자.
    1. scheme selection에 들어가서 Edit Scheme 선택하기
       
    2. 왼쪽에서 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

    댓글

Designed by Tistory.