[번역]iOS Unit Testing, UI Testing 튜토리얼

okstring·2021년 7월 20일
1

이 글은 raywenderlich.com의 iOS Unit Testing and UI Testing Tutorial을 읽고 공부를 위해 번역했습니다.


iOS Unit 테스트는 매력적이지 않지만, 테스트로 인해 흥미로운 앱이 버그가 많은 잡동사니가 되는 것을 막을 수 있기 때문에 필요합니다. 이 튜토리얼을 읽고 있다면 코드와 UI에 대한 테스트를 해야한다는 것을 이미 알고 있지만 방법을 모를 수도 있습니다.

작업 중 앱이 있을 수 있지만, 앱 확장을 위해 변경 사항을 테스트하려고 합니다. 아마 여러분은 이미 시험을 썼을 수도 있지만, 시험이 옳은지 확실하지 않을 수도 있습니다. 또는 새 앱에서 작업을 시작했는데 진행하면서 테스트하려고 합니다.

이 튜토리얼에서는 다음 방법을 보여 줍니다.

  • Xcode의 Test 네비게이터를 사용하여 앱의 모델과 비동기 메소드을 테스트합니다.
  • stubs(Method stub), mocks를 사용하여 라이브러리 또는 시스템 객체와의 상호 작용을 위조합니다.
  • UI 및 성능을 테스트합니다.
  • 코드 적용 도구를 사용합니다.

도중에, 여러분은 닌자를 테스트하는 데 사용되는 어휘 중 일부를 배울 것입니다.

시작

이 튜토리얼의 상단 또는 하단에 있는 Download Materials 버튼을 사용하여 프로젝트 자료를 다운로드하십시오. 이 프로그램에는 UIKit Apprentice의 샘플 앱을 기반으로 하는 프로젝트 BullsEye가 포함되어 있습니다. 이것은 우연과 행운의 단순한 게임입니다. 게임 로직은 이 튜토리얼에서 테스트할 'Bulls EyeGame' 클래스에 있습니다.

Test 대상 파악

어떤 테스트를 쓰기 전에, 기본을 아는 것이 중요합니다. 무엇을 테스트해야 합니까?

기존 앱을 확장하는 것이 목표인 경우 먼저 변경할 구성 요소에 대한 테스트를 작성해야 합니다.

일반적으로 테스트는 다음을 포함해야 합니다.

- 핵심 기능: 클래스 및 메서드 및 컨트롤러와의 상호 작용을 모델링합니다.
- 가장 일반적인 UI 워크플로우입니다.
- 경계 조건
- 버그 수정

Test에 대한 모범 사례를 이해

FIRST라는 약자는 효과적인 단위 테스트를 위한 일련의 기준을 설명합니다. 그 기준은 다음과 같습니다.

  • Fast(빠른): 테스트는 빨리 실행되어야 합니다.
  • Independent/Isolated(독립, 격리): 테스트는 서로 상태를 공유해서는 안 됩니다.
  • Repeatable(반복 가능한): 테스트를 실행할 때마다 동일한 결과를 얻어야 합니다. 외부 데이터 공급자 또는 동시성 문제로 인해 간헐적으로 장애가 발생할 수 있습니다.
  • Self-validating(자체 검증): 테스트는 완전히 자동화되어야 합니다. 출력은 로그 파일에 대한 프로그래머의 해석에 의존하지 말고 "통과" 또는 "실패"여야 합니다.
  • Timely(시기): 이상적으로는 테스트하는 생산 코드를 작성하기 전에 테스트를 작성해야 합니다. 이를 테스트 주도 개발이라고 합니다.

FIRST 원칙을 따르면 앱의 장애물이 되는 대신 테스트가 명확하고 유용하게 유지됩니다.

img

Xcode Unit Test

Test navigator를 사용하면 Test 작업을 가장 쉽게 수행할 수 있습니다. 이 프로그램을 사용하여 테스트 대상을 만들고 앱에 대한 테스트를 실행할 수 있습니다.

단위 테스트 대상을 만듭니다.

BullsEye 프로젝트를 열고 Command-6를 눌러 테스트 탐색기를 엽니다.

왼쪽 아래 모서리에서 +를 클릭한 다음 메뉴에서 새 장치 테스트 대상...을 선택합니다.

iOS Unit Testing: Test navigator

기본 이름인 BullsEyeTests를 사용하고 Organization Identifiercom.raywenderlich를 입력합니다. 테스트 탐색기에 테스트 번들이 나타나면 확장 버튼(disclosure triangle)을 클릭하여 확장하고 BullsEyeTests를 클릭하여 편집기에서 엽니다.

iOS Unit Testing: Template

기본 템플릿은 테스트 프레임워크인 XCTest를 가져오고 SetUpWithError()``tearDownWithError() 및 예제 테스트 방법을 사용하여 XCTestCaseBullsEyeTests 하위 클래스를 정의합니다.

다음 세 가지 방법으로 테스트를 실행할 수 있습니다.

  1. 제품 test 테스트 또는 Command-U입니다. 이 두 가지 모두 모든 테스트 클래스를 실행합니다.
  2. 테스트 탐색기에서 화살표 단추를 클릭합니다.
  3. gutter에 있는 다이아몬드 버튼을 클릭합니다.

iOS Unit Testing: Running Tests

테스트 네비게이터 또는 거더에서 다이아몬드를 눌러 개별 테스트 방법을 실행할 수도 있습니다.

얼마나 오래 걸리고 어떻게 생겼는지 느끼기 위해 테스트를 실행하는 여러 가지 방법을 시도해 보세요. 샘플 테스트는 아직 아무 것도 하지 않아서, 그들은 매우 빨리 달립니다.

모든 테스트가 성공하면, 다이아몬드는 녹색으로 변하고 체크 표시를 보일 것입니다. 'test Performance 예제()' 끝의 회색 다이아몬드를 클릭하여 성능 결과를 엽니다.

iOS Unit Testing: Performance Results

이 튜토리얼에는 test Performance Example() 또는 testExample()이 필요하지 않으므로 삭제하십시오.

XCTA를 사용하여 모델을 테스트합니다.

먼저 'XCTAssert' 기능을 사용하여 BullsEye 모델의 핵심 기능을 테스트합니다. 불스 아이게임이 라운드 스코어를 정확하게 계산합니까?

BullsEyeTests.swift에서 "import XCTest" 아래에 이 줄을 추가합니다.

@testable import BullsEye

그러면 장치 테스트에서 BullsEye의 내부 유형 및 기능에 액세스할 수 있습니다.

'BullsEyeTests'의 맨 위에 다음 속성을 추가하십시오.

var sut: BullsEyeGame!

이렇게 하면 SUT(System Under Test) 또는 이 테스트 사례 클래스가 테스트와 관련된 개체인 'BullsEyeGame'의 자리 표시자가 생성됩니다.

그런 다음 'SetUpWithError()'의 내용을 다음과 같이 바꿉니다.

try super.setUpWithError()
sut = BullsEyeGame()

이렇게 하면 클래스 레벨에서 "BullsEyeGame"이 생성되므로 이 테스트 클래스의 모든 테스트가 SUT 개체의 속성과 메서드에 액세스할 수 있습니다.

잊기 전에 tearDownWithError()에서 SUT 개체를 release하십시오. 내용을 다음으로 바꿉니다.

sut = nil
try super.tearDownWithError()

note: SetUpWithError()에서 SUT를 만들어 TearDownWithError()에 풀어 모든 테스트가 깨끗한 슬레이트로 시작되도록 하는 것이 좋습니다. 자세한 내용은 해당 주제에 대한 Jon Reid's Post)를 참조하십시오.

첫 번째 테스트를 작성합니다.

이제 첫 번째 테스트를 작성할 준비가 되었습니다!

BullsEyeTests의 끝에 다음 코드를 추가하여 추측에 대한 예상 점수를 계산하는지 테스트합니다.

func testScoreIsComputedWhenGuessIsHigherThanTarget() {
  // given
  let guess = sut.targetValue + 5

  // when
  sut.check(guess: guess)

  // then
  XCTAssertEqual(sut.scoreRound, 95, "Score computed from guess is wrong")
}

테스트 방법의 이름은 항상 테스트로 시작하고 테스트 내용에 대한 설명이 이어집니다.

테스트를 given, whenthen 섹션으로 포맷하는 것이 좋습니다.

  1. Given: 여기서는 필요한 값을 설정합니다. 이 예에서는 'guess' 값을 만들어 'targetValue'와 얼마나 다른지 지정할 수 있습니다.
  2. When: 이 섹션에서는 테스트할 코드를 실행합니다. check(guess:)를 호출합니다.
  3. Then: 이 섹션은 테스트 실패 시 프린트되는 메시지와 함께 예상되는 결과를 주장하는 섹션입니다. 이 경우 sut.scoreRound는 100-5이므로 95가 됩니다.

gutter 또는 테스트 탐색기에서 다이아몬드 아이콘을 클릭하여 테스트를 실행합니다. 이렇게 하면 앱이 만들어지고 실행되며, 다이아몬드 아이콘이 녹색 체크 표시로 바뀝니다! X 코드 위에 다음과 같이 보이는 성공 여부를 나타내는 순간적인 팝업도 표시됩니다.

iOS Unit Testing: Test Succeeded

참고: XCTestAssurations의 전체 목록을 보려면 Application's Acceptions by Category)로 이동하십시오.

테스트를 디버깅합니다.

일부러 불스 아이게임에 버그가 내장되어 있는데, 지금 바로 찾기 연습을 해보세요. 버그가 작동하는 것을 보기 위해 given 섹션의 "targetValue"에서 *5를 빼서 다른 모든 것을 동일하게 하는 테스트를 만듭니다.

다음 테스트를 추가합니다.

func testScoreIsComputedWhenGuessIsLowerThanTarget() {
  // given
  let guess = sut.targetValue - 5

  // when
  sut.check(guess: guess)

  // then
  XCTAssertEqual(sut.scoreRound, 95, "Score computed from guess is wrong")
}

추측과 목표값의 차이는 여전히 5이므로 점수는 95점이어야 합니다.

breakpoint navigator에서 Test Failure Breakpoint를 추가합니다. 이렇게 하면 테스트 방법이 failure assertion을 게시할 때 테스트 실행이 중지됩니다.

iOS Unit Testing: Adding a Test Failure Breakpoint

테스트를 실행하면 테스트 실패와 함께 'XCTAssert Equal' 라인에서 중지됩니다.

디버그 콘솔에서 sutguess를 검사합니다.

iOS Unit Testing: Inspecting a Test Failure

guess는 목표값 - 5인데 점수 라운드는 95가 아니라 105입니다!

자세히 조사하려면 nomar debugging process를 사용하십시오. when 명령문에 중단점을 설정하고 BullsEyeGame.swift에서 difference가 발생하는 "check(guess: )"에 중단점을 설정합니다. 그런 다음 테스트를 다시 실행하고 difference 코드로 이동하여 앱의 difference 값을 검사합니다.

iOS Unit Testing: Debug Console

문제는 difference가 음수여서 점수가 100 - (-5)라는 점입니다. 이 문제를 해결하려면 difference절대값을 사용해야 합니다. check(guess:)에서 올바른 줄을 제거하고 잘못된 줄을 삭제합니다.

두 중단점을 제거하고 테스트를 다시 실행하여 이제 성공했는지 확인합니다.

XCTestExpectation을 사용하여 비동기 작업을 테스트합니다.

이제 모델을 테스트하고 테스트 실패를 디버그하는 방법을 배웠으니 이제 비동기 코드를 테스트하는 단계로 넘어갈 때입니다.

BullsEyeGame은 URL세션(URLSession)을 활용해 다음 게임 대상으로 임의번호를 부여합니다. URLSession 방법은 비동기적 입니다. 그들은 바로 돌아오지만 나중에야 달리기를 끝냅니다. 비동기 메서드를 테스트하려면 'XCTestExpectation'을 사용하여 비동기 작업이 완료될 때까지 테스트를 기다리도록 합니다.

비동기 테스트는 일반적으로 느리므로 더 빠른 단위 테스트와 별도로 유지해야 합니다.

BullsEyeSlowTests라는 이름의 새 장치 테스트 대상을 만듭니다. 새로운 테스트 클래스 'BullsEyeSlowTests'를 열고 기존 '가져오기' 명령문 바로 아래에 있는 BullsEye 앱 모듈을 가져옵니다.

@testable import BullsEye

이 클래스의 모든 테스트는 기본 URLSession을 사용하여 요청을 전송하므로 sut를 선언하고 SetupWithError()에서 생성한 후 TearDownWithError()로 해제합니다. 이렇게 하려면 BullsEyeSlowTests의 내용을 다음으로 바꾸십시오.

var sut: URLSession!

override func setUpWithError() throws {
  try super.setUpWithError()
  sut = URLSession(configuration: .default)
}

override func tearDownWithError() throws {
  sut = nil
  try super.tearDownWithError()
}

다음에 비동기 테스트를 추가합니다:

// success가 빠릅니다. failure가 느립니다.
func testValidApiCallGetsHTTPStatusCode200() throws {
  // given
  let urlString = 
    "http://www.randomnumberapi.com/api/v1.0/random?min=0&max=100&count=1"
  let url = URL(string: urlString)!
  // 1
  let promise = expectation(description: "Status code: 200")

  // when
  let dataTask = sut.dataTask(with: url) { _, response, error in
    // then
    if let error = error {
      XCTFail("Error: \(error.localizedDescription)")
      return
    } else if let statusCode = (response as? HTTPURLResponse)?.statusCode {
      if statusCode == 200 {
        // 2
        promise.fulfill()
      } else {
        XCTFail("Status code: \(statusCode)")
      }
    }
  }
  dataTask.resume()
  // 3
  wait(for: [promise], timeout: 5)
}

이 테스트는 유효한 요청을 전송하면 200 상태 코드가 반환되는지 확인합니다. 대부분의 코드는 앱에서 작성한 것과 동일하며 다음과 같은 추가 줄이 있습니다.

  1. expectation(description:): 'promise'에 저장된 'XCTestExpectation'을(를) "description"은 예상한 결과를 나타냅니다.
  2. promise.fulfill(): 비동기 메서드의 완료 핸들러의 성공 조건 종료 시 이를 호출하여 예상이 충족되었음을 플래그로 지정합니다.
  3. wait(for:timeout:): 모든 예상이 충족될 때까지 또는 timeout 간격이 종료될 때까지 먼저 테스트를 실행합니다

테스트를 실행합니다. 인터넷에 연결되어 있는 경우, 앱이 시뮬레이터에 로드된 후 테스트가 성공하는 데 약 1초 정도 걸립니다.

빠르게 실패합니다.

실패는 아프지만, 오래 걸릴 필요는 없어요.

실패를 경험하려면testValidApiCallGetsHTTPStatusCode200()을(를) 잘못된 값으로 지정하기만 하면 됩니다.

let url = URL(string: "http://www.randomnumberapi.com/test")!

테스트를 실행합니다. 실패하지만 전체 시간이 초과되서 걸립니다! 왜냐하면 당신은 요청이 항상 성공할 것이라고 생각했기 때문이고, 거기서 당신은 '약속'을 불렀습니다.액면 그대로 이행합니다. 요청이 실패했으므로 시간 초과가 만료되었을 때만 완료되었습니다.

가정을 변경하여 이를 개선하고 검정을 더 빨리 실패할 수 있습니다. 요청이 성공할 때까지 기다리지 말고 비동기 메서드의 완료 처리기가 호출될 때까지 기다리십시오. 이 문제는 앱이 서버로부터 OK 또는 error 응답을 수신하는 즉시 발생하며, 이는 기대치를 충족시킵니다. 그런 다음 테스트에서 요청이 성공했는지 여부를 확인할 수 있습니다.

이 방법을 확인하려면 새 테스트를 만드십시오.

그러나 먼저 url의 변경 사항을 실행 취소하여 이전 테스트를 수정합니다.

그런 다음 다음 클래스에 다음 테스트를 추가합니다.

func testApiCallCompletes() throws {
  // given
  let urlString = "http://www.randomnumberapi.com/test"
  let url = URL(string: urlString)!
  let promise = expectation(description: "Completion handler invoked")
  var statusCode: Int?
  var responseError: Error?

  // when
  let dataTask = sut.dataTask(with: url) { _, response, error in
    statusCode = (response as? HTTPURLResponse)?.statusCode
    responseError = error
    promise.fulfill()
  }
  dataTask.resume()
  wait(for: [promise], timeout: 5)

  // then
  XCTAssertNil(responseError)
  XCTAssertEqual(statusCode, 200)
}

주요 차이점은 완료 핸들러를 입력하는 것만으로도 예상을 충족하며,이 작업이 발생하는 데 1 초 밖에 걸리지 않는다는 것입니다. 요청이 실패하면 'then' 선언문은 실패합니다.

테스트를 실행하십시오. 이제 실패하는 데 약 1초가 걸립니다. 테스트 실행이 timeout를 초과했기 때문에 요청이 실패했기 때문에 실패합니다.

url을 수정 한 다음 테스트를 다시 실행하여 이제 성공하는지 확인합니다.

조건부 실패입니다.

어떤 상황에서는 테스트를 실행하는 것이 말이 안 되는 경우도 있습니다. 예를 들어, testValidApiCallGetsHTTPStatusCode200() 를 실행 할 경우 어떻게 해야 합니까? 네트워크 연결 없이 실행됩니까? 물론, 200 상태 코드를 받을 수 없기 때문에 통과해서는 안 됩니다. 하지만 그것은 또한 실패해서는 안 됩니다. 왜냐하면 그것은 아무 것도 테스트하지 않았으니까요.

다행히 애플은 XCTS킵을 도입해 전제조건이 실패할 경우 테스트를 생략했습니다. 'sut' 선언 아래에 다음 줄을 추가합니다.

let networkMonitor = NetworkMonitor.shared

NetworkMonitorNWpathMonitor를 감싸고 있어 네트워크 연결을 편리하게 확인할 수 있습니다.

testValidApiCallGetsHTTPStatusCode200()에서 테스트 시작 부분에XCTSkipUnless를 추가합니다.

try XCTSkipUnless(
  networkMonitor.isReachable, 
  "Network connectivity needed for this test.")

네트워크에 연결할 수 없는 경우 XCTSkipInvertion(_:_:)이(가) 테스트를 건너뜁니다. 네트워크 연결을 비활성화하고 테스트를 실행하여 이 옵션을 선택합니다. 테스트 옆의 거터에 새 아이콘이 표시되어 테스트에 합격하거나 불합격하지 않았음을 나타냅니다.

iOS Unit Testing: Test Skipped

네트워크 연결을 다시 활성화하고 테스트를 다시 실행하여 정상 상태에서도 성공하는지 확인합니다. 동일한 코드를 testApiCallCompletes()의 시작 부분에 추가합니다.

Objects과 상호작용을 위조합니다.

비동기 테스트는 코드가 비동기 API에 대한 올바른 입력을 생성한다는 확신을 줍니다. 또한 코드가 URLSession에서 입력을 수신할 때 코드가 올바르게 작동하는지 테스트하거나 UserDefaults 데이터베이스 또는 iCloud 컨테이너를 올바르게 업데이트하는지 테스트할 수 있습니다.

대부분의 앱은 사용자가 제어할 수 없는 시스템 또는 라이브러리 개체와 상호 작용합니다. 이러한 개체와 상호 작용하는 테스트는 속도가 느리고 반복할 수 없어 FIRST 원칙 중 두 가지를 위반할 수 있습니다. 대신 stubs에서 입력을 받거나 mock 개체를 업데이트하여 상호 작용을 *가짜로 만들 수 있습니다.

코드가 시스템 또는 라이브러리 개체에 대한 의존성이 있는 경우 Fakery를 사용합니다. 이 작업은 가짜 object를 만들어 해당 부분을 재생하고 이 가짜를 코드에 주입하는 방식으로 수행합니다. [Dependency Injection](Jon Reid의 https://www.objc.io/issues/15-testing/dependency-injection/)에서는 이러한 작업을 수행하는 몇 가지 방법을 설명합니다.

stub에서 입력을 위조합니다.

이제 세션에서 다운로드한 데이터를 앱의 get RandomNumber(complete:)가 올바르게 구문 분석하는지 확인합니다. 여러분은 BullsEyeGame의 세션을 뭉툭한 데이터로 위조하게 될 것입니다.

Test navigator로 이동하여 +를 클릭하고 New Unit TEst Class...를 선택합니다. BullsEyeFakeTests라는 이름을 지정하고 BullsEyeTests 디렉토리에 저장한 후 대상을 BullsEyeTests로 설정합니다.

iOS Unit Testing: New Unit Test Class

import 문 바로 아래에 있는 BullsEye app module을 가져옵니다.

@testable import BullsEye

이제 BullsEyeFakeTests의 내용을 다음과 같이 바꾸십시오.

var sut: BullsEyeGame!

override func setUpWithError() throws {
  try super.setUpWithError()
  sut = BullsEyeGame()
}

override func tearDownWithError() throws {
  sut = nil
  try super.tearDownWithError()
}

BullsEyeGame인 SUT를 SetUpWithError()에서 생성하고 TearDownWithError()로 해제합니다.

BullsEye 프로젝트에는 URLSessionStub.swift 지원 파일이 포함되어 있습니다. 이것은 URL로 데이터 작업을 생성하는 방법을 사용하여 URLSession Protocol 또한 이 프로토콜을 준수하는 URLSessionStub를 정의합니다. 초기화를 통해 데이터 작업에서 반환해야 하는 데이터, 응답 및 오류를 정의할 수 있습니다.

가짜를 설정하려면 BullsEyeFakeTests.swift로 이동하여 새 테스트를 추가합니다.

func testStartNewRoundUsesRandomValueFromApiRequest() {
  // given
  // 1
  let stubbedData = "[1]".data(using: .utf8)
  let urlString = 
    "http://www.randomnumberapi.com/api/v1.0/random?min=0&max=100&count=1"
  let url = URL(string: urlString)!
  let stubbedResponse = HTTPURLResponse(
    url: url, 
    statusCode: 200, 
    httpVersion: nil, 
    headerFields: nil)
  let urlSessionStub = URLSessionStub(
    data: stubbedData,
    response: stubbedResponse, 
    error: nil)
  sut.urlSession = urlSessionStub
  let promise = expectation(description: "Value Received")

  // when
  sut.startNewRound {
    // then
    // 2
    XCTAssertEqual(self.sut.targetValue, 1)
    promise.fulfill()
  }
  wait(for: [promise], timeout: 5)
}

이 테스트는 두 가지 작업을 수행합니다.

  1. 가짜 데이터와 응답을 설정하고 가짜 세션 개체를 만듭니다. 마지막으로 sut의 속성으로 가짜 세션을 앱에 주입합니다.
  2. 스텁이 비동기 메서드로 가장하기 때문에 여전히 이 테스트를 비동기 테스트로 작성해야 합니다. startNewRound(complete:)를 호출하면 targetValue와 stubbed fake number를 비교하여 가짜 데이터를 구문 분석하는지 확인합니다.

테스트를 실행합니다. 실제 네트워크 연결이 없기 때문에 상당히 빠르게 성공할 수 있습니다!

Mock object의 업데이트를 위조합니다.

이전 테스트에서는 가짜 object로부터 입력을 제공하기 위해 stub를 사용했습니다. 그런 다음 mock 객체를 사용하여 코드가 UserDefaults를 올바르게 업데이트하는지 테스트합니다.

이 앱은 두 가지 게임 스타일이 있습니다. 사용자는 다음 중 하나를 수행할 수 있습니다.

  1. 슬라이더를 움직여 목표값에 맞춥니다.
  2. 슬라이더 위치에서 목표값을 맞춥니다.

오른쪽 아래 모서리에 있는 세그먼트 컨트롤은 게임 스타일을 전환하여 UserDefaults에 저장합니다.

다음 테스트는 앱이 gameStyle 속성을 올바르게 저장하는지 확인합니다.

새 테스트 클래스를 대상 BullsEyeTests에 추가하고 이름을 BullsEyeMockTests로 지정합니다. import 문 아래에 다음을 추가합니다.

@testable import BullsEye

class MockUserDefaults: UserDefaults {
  var gameStyleChanged = 0
  override func set(_ value: Int, forKey defaultName: String) {
    if defaultName == "gameStyle" {
      gameStyleChanged += 1
    }
  }
}

MockUserDefaultsset(_:forKey:)gameStyleChanged로 재정의합니다. 유사한 테스트에서 Boole 변수를 설정하는 경우가 많지만, Int를 늘리면 유연성이 향상됩니다. 예를 들어, 테스트에서는 앱이 메소드를 한 번만 호출하는지 확인할 수 있습니다.

다음으로 BullsEyeMockTests에서 SUT와 모의 개체를 선언합니다.

var sut: ViewController!
var mockUserDefaults: MockUserDefaults!

SetUpWithError()TearDownWithError()를 다음으로 바꿉니다.

override func setUpWithError() throws {
  try super.setUpWithError()
  sut = UIStoryboard(name: "Main", bundle: nil)
    .instantiateInitialViewController() as? ViewController
  mockUserDefaults = MockUserDefaults(suiteName: "testing")
  sut.defaults = mockUserDefaults
}

override func tearDownWithError() throws {
  sut = nil
  mockUserDefaults = nil
  try super.tearDownWithError()
}

그러면 SUT와 모의 개체가 생성되고 SUT의 속성으로 모의 개체가 주입됩니다.

이제 템플릿의 두 가지 기본 테스트 방법을 다음으로 바꾸십시오.

func testGameStyleCanBeChanged() {
  // given
  let segmentedControl = UISegmentedControl()

  // when
  XCTAssertEqual(
    mockUserDefaults.gameStyleChanged, 
    0, 
    "gameStyleChanged should be 0 before sendActions")
  segmentedControl.addTarget(
    sut,
    action: #selector(ViewController.chooseGameStyle(_:)),
    for: .valueChanged)
  segmentedControl.sendActions(for: .valueChanged)

  // then
  XCTAssertEqual(
    mockUserDefaults.gameStyleChanged, 
    1, 
    "gameStyle user default wasn't changed")
}

when assertion은 test methon가 세그먼트 컨트롤을 변경하기 전에 gameStyleChanged 플래그가 0이라는 것입니다. 따라서 then assertion도 참이면 set(_:forKey:)이 정확히 한 번 호출되었다는 의미입니다.

테스트를 실행합니다. 성공해야 합니다.

Xcode의 UI 테스트

UI 테스트를 통해 사용자 인터페이스와의 상호 작용을 테스트할 수 있습니다. UI 테스트는 쿼리로 앱의 UI object를 찾고 이벤트를 합성한 다음 해당 object로 이벤트를 보내는 방식으로 작동합니다. API를 사용하면 UI object의 property과 state를 검사하여 예상 상태와 비교할 수 있습니다.

Test navigator에서 새 UI Test Target을 추가합니다. Test Target이 BullsEye인지 확인한 다음 기본 이름 BullsEye를 사용합니다

iOS Unit Testing: New UI Test Target

BullsEye를 엽니다.UITests.swift를 테스트하고 BullsEye 상단에 이 속성을 추가합니다.UITests 클래스는 다음과 같습니다.

var app: XCUIApplication!

TearDownWithError()를 제거하고 SetUpWithError()의 내용을 다음과 같이 바꿉니다.

try super.setUpWithError()
continueAfterFailure = false
app = XCUIApplication()
app.launch()

기존 테스트 두 개를 제거하고 testGameStyleSwitch()라는 새 테스트를 추가합니다.

func testGameStyleSwitch() {    
}

testGameStyleSwitch()에서 새 줄을 열고 편집기 창 하단에 있는 빨간색 Record 버튼을 클릭합니다.

iOS Unit Testing: Recording a UI Test

이렇게 하면 상호 작용을 테스트 명령으로 기록하는 모드에서 앱이 시뮬레이터에서 열립니다. 앱이 로드되면 게임 스타일 스위치의 Slide 세그먼트와 top label을 누릅니다. Xcode Record 버튼을 다시 클릭하여 기록을 중지합니다.

이제 testGameStyleSwitch()에 다음 세 줄이 있습니다.

let app = XCUIApplication()
app.buttons["Slide"].tap()
app.staticTexts["Get as close as you can to: "].tap()

Recoder에서 사용자가 앱에서 테스트한 것과 동일한 작업을 테스트하기 위한 코드를 만들었습니다. 게임 스타일 세그먼트 컨트롤 및 상단 레이블에 탭을 보냅니다. 이를 기반으로 고유한 UI 테스트를 만듭니다. 다른 문장이 보이면 삭제하세요.

첫 번째 행은 SetUpWithError()에서 생성한 속성을 복제하므로 해당 행을 삭제합니다. 아직 아무 것도 누르지 않아도 되므로 2행과 3행의 끝에 있는 .tap()도 삭제하십시오. 이제 ["Slide"] 옆에 있는 작은 메뉴를 열고 segmentedControls.buttons["Slide"] 를 선택합니다.

iOS Unit Testing: Changing Recording

다음 사항을 고려해야 합니다.

app.segmentedControls.buttons["Slide"]
app.staticTexts["Get as close as you can to: "]

Recoder가 테스트에서 액세스할 수 있는 코드를 찾는 데 도움이 되도록 다른 object를 누릅니다. 이제 이러한 라인을 이 코드로 교체하여 주어진 섹션을 생성합니다.

// given
let slideButton = app.segmentedControls.buttons["Slide"]
let typeButton = app.segmentedControls.buttons["Type"]
let slideLabel = app.staticTexts["Get as close as you can to: "]
let typeLabel = app.staticTexts["Guess where the slider is: "]

이제 segmented control에 있는 두 버튼과 가능한 두 상단 레이블에 대한 이름이 지정되었으므로 아래에 다음 코드를 추가하십시오.

// then
if slideButton.isSelected {
  XCTAssertTrue(slideLabel.exists)
  XCTAssertFalse(typeLabel.exists)

  typeButton.tap()
  XCTAssertTrue(typeLabel.exists)
  XCTAssertFalse(slideLabel.exists)
} else if typeButton.isSelected {
  XCTAssertTrue(typeLabel.exists)
  XCTAssertFalse(slideLabel.exists)

  slideButton.tap()
  XCTAssertTrue(slideLabel.exists)
  XCTAssertFalse(typeLabel.exists)
}

segmented control의 각 버튼을 tap() 할 때 올바른 라벨이 존재하는지 확인합니다. 테스트를 실행합니다. 모든 주장이 성공해야 합니다.

테스트 성능

[Apple's documentation:](https://developer.apple.com/library/prerelease/content/documentation/Developer를 참조하십시오.Tools/Conceptional/testing_with_xcode/chapters/04-writing_tests.html#//apple_ref/doc/uid/TP40014132-CH4-SW8):

성능검사는 평가하고자 하는 코드 블록을 취하여 10회 실행하며, 평균 실행 시간과 실행 표준 편차를 수집합니다. 이러한 개별 측정의 평균은 테스트 실행에 대한 값을 형성하며, 이 값을 기준선과 비교하여 성공 또는 실패를 평가할 수 있습니다.

성능 테스트 작성은 간단합니다. 측정하려는 코드를 measure()의 closure에 넣기만 하면 됩니다. 또한 측정할 metrics을 여러 개 지정할 수 있습니다.

다음 테스트를 BullsEyeTests에 추가합니다.

func testScoreIsComputedPerformance() {
  measure(
    metrics: [
      XCTClockMetric(), 
      XCTCPUMetric(),
      XCTStorageMetric(), 
      XCTMemoryMetric()
    ]
  ) {
    sut.check(guess: 100)
  }
}

이 테스트는 여러 메트릭을 측정합니다.

  • XCTClockMetric은 경과 시간을 측정합니다.
  • XCTCPUMetric은 CPU 시간, 주기, 명령 수 등 CPU의 활동을 추적합니다.
  • XCTStorageMetric은 테스트된 코드가 스토리지에 얼마나 많은 데이터를 쓰는지 알려줍니다.
  • XCTMemoryMetric은 사용된 물리적 메모리의 양을 추적합니다.

테스트를 실행한 다음 measure() trailing closure 시작 옆에 나타나는 아이콘을 클릭하여 통계를 확인합니다. Metric 옆에서 선택한 Metric을 변경할 수 있습니다.

iOS Unit Testing: Viewing Performance Result

Set Baseline을 클릭하여 참조 시간을 설정합니다. 성능 테스트를 다시 실행하여 결과를 확인하십시오. 기준보다 더 좋거나 더 나쁠 수 있습니다. Edit 버튼을 사용하여 기준을 이 새 결과로 재설정할 수 있습니다.

Baselines은 디바이스 구성별로 저장되므로 여러 디바이스에서 동일한 테스트를 실행할 수 있습니다. 각 구성에서는 특정 구성의 프로세서 속도, 메모리 등에 따라 서로 다른 baseline을 유지할 수 있습니다.

테스트 중인 method의 성능에 영향을 미칠 수 있는 앱을 변경할 때마다 성능 테스트를 다시 실행하여 해당 방법이 baseline과 어떻게 비교되는지 확인합니다.

Code Coverage를 활성화

code coverage tool는 테스트가 실제로 실행 중인 앱 코드를 알려주므로, 아직 테스트되지 않은 앱 부분이 무엇인지 알 수 있습니다.

code coverage를 활성화하려면 스키마의 테스트 작업을 편집하고 옵션 탭 아래의 적용 범위 수집 확인란을 선택합니다.

iOS Unit Testing: Enabling Coverage

Command-U를 사용하여 모든 테스트를 실행한 후 Command-9를 사용하여 report navigator를 엽니다. 목록의 맨 위 항목 아래에서 커버리지를 선택합니다.

iOS Unit Testing: Code Coverage Report

삼각형을 클릭하여 BullsEyeGame.swift의 기능 및 폐쇄 목록을 확인합니다.

iOS Unit Testing: Code Coverage Report Details

getRandomNumber(completion:)로 스크롤하여 적용 범위가 95.0%인지 확인합니다.

이 함수의 화살표 단추를 클릭하여 함수의 소스 파일을 엽니다. 오른쪽 사이드바의 커버리지 주석을 마우스로 가리키면 코드의 섹션이 녹색 또는 빨간색으로 강조 표시됩니다.

iOS Unit Testing: Coverage Highlights in Code Editor

적용 범위 주석은 테스트가 각 코드 섹션에 도달하는 횟수를 나타냅니다. 호출되지 않은 섹션은 빨간색으로 강조 표시됩니다.

100% 커버리지를 달성하시겠습니까?

100% 코드 적용을 위해 얼마나 노력해야 합니까? Google "100% unit test coverage"만 보면, "100% coverage"의 정의에 대한 토론과 함께 이에 대한 찬반 주장을 찾을 수 있습니다. 이에 반대하는 주장들은 마지막 10%–15%가 노력할 가치가 없다고 말합니다. 이에 대한 주장은 마지막 10%~15%가 가장 중요하다고 말합니다. 왜냐하면 테스트하기가 너무 어렵기 때문입니다. Google은 검증 불가능한 코드는 더 깊은 설계 문제의 표시라는 설득력 있는 주장을 찾기 위해 "잘못된 디자인을 결합하기 어렵다"고 말합니다.

여기서 어디로 갈까요?

본 튜토리얼의 상단 또는 하단에 있는 Download Materials 버튼을 사용하여 프로젝트의 완료된 버전을 다운로드할 수 있습니다. 자신의 테스트를 추가하여 스킬을 계속 개발하십시오.

이제 프로젝트에 대한 쓰기 테스트에서 사용할 수 있는 몇 가지 유용한 도구가 있습니다. 이 iOS 유닛 테스트 및 UI 테스트 튜토리얼을 통해 모든 것을 테스트할 수 있는 자신감을 얻으셨기를 바랍니다!

다음은 추가 연구를 위한 몇 가지 리소스입니다.

profile
step by step

0개의 댓글