[iOS] Unit Test

leeyoungwoozz·2021년 5월 9일
0

iOS

목록 보기
26/46
post-thumbnail

Unit Test

  • Test!! Test!!

TDD 진짜 너무너무너무너무 어렵다...(다음 프로젝트 기회에ㅎㅎ)
Unit Test 하는 것도 어렵다.............

어려운게 아니라 익숙하지 않은 것이라고 생각하자!


이번 포스팅 주요 내용 = iOS Unit Testing and UI Testing Tutorial
iOS Unit Test와 UI Test의 Tutorial이 있는 곳을 찾았는데, 비동기 작업에 대한 내용도 있어서 이번 프로젝트에 써먹을 수 있을 듯? 하다ㅎ...

오늘은 해당 사이트 내용을 정리(번역)해보자!


목차

  • 테스트?
    • 테스트 대상 파악하기
    • 테스트 모범 사례 이해하기
  • Xcode의 테스트 네비게이터를 사용하여 앱의 모델 및 비동기 메서드 테스트
  • Stub 및 mocks을 사용하여 라이브러리 또는 시스템 객체와의 Fake interaction
  • UI, 성능 테스트
  • 코드 커버리지 도구 사용

테스트 대상 파악하기

우리는 어떤 테스트를 작성하든지, 먼저 우리가 테스트할 필요가 있는지 부터 봐야한다.

일반적으로 테스트는 아래를 포함해야 한다.

  • 핵심 기능 : Model Classes 및 Methods, 그리고 Controller와의 상호 작용
  • 가장 일반적인 UI workflows
  • Boundary conditions
  • Bug fixes

테스트 모범 사례 이해하기

F.I.R.S.T는 효과적인 단위 테스트를 위한 간결한 기준을 설명한다

  • Fast : 테스트가 빠르게 실행되어야 합니다
  • Independent / Isolated : 테스트가 서로 상태를 공유해서는 안된다. 독립적이여야 한다.
  • Repeatable : 테스트를 실행할때마다 동일한 결과를 얻어야 한다. 외부 데이터 공급자 또는 동시성 문제로 인해 간혈적인 오류가 발생할 수 있다.
  • Self-Validating : 테스트는 완전히 자동화되어야 한다. 출력은 프로그래머의 로그 파일 해석에 의존하는 대신 "통과" 또는 "실패" 여야 한다.
  • Timely : 이상적으로는 테스트의 대상이 될 코드를 작성하는 것보다 테스트를 먼저 작성해야 합니다. 이를 테스트 기반 개발이라고 한다.

Unit Testing in Xcode

테스트 네비게이터는 테스트 작업을 쉽게 하는 방법들을 제공한다. 이를 사용하여 테스트 대상을 만들고 앱에 대해 테스트를 실행한다.

Newe Unit test Target 을 통해서 Test를 생성하면 Default 로 생성되어 있는 setUpWithError(), tearDownWithError() 를 볼 수 있다.

테스트는 세 가지 방법을 통해서 실행할 수 있는데,

  1. Product > Test or Command-U

모든 test class들 실행

  1. Test Navigator에 있는 화살표 버튼을 통해 실행

  2. 코드 옆 다이아몬드 버튼을 클릭하여 실행

  • 해당 사이트에서는 testPerformanceExample()testExample() 에 대한 설명이 없어 보인다. 따로 찾아보자!

Using XCTAsssert to Test Models

먼저, 해당 사이트에서 제공해준 BullsEye 의 모델의 핵심 기능을 XCTAssert 함수를 통해서 테스트 해보자!

@testable import BullsEye

을 추가 함으로써 BullsEye 모듈 내의 타입과 함수들에 접근

var sut: BullsEyeGame!

테스트 클래스 내에 System under Test(SUT) 을 만들어 준다.

try super.setUpWithError()
sut = BullsEyeGame()

그리고 setUpWithError() 함수 내에 위 코드를 추가한다. 이렇게 하면 BullsEyeGame 클래스 수준에서 생성되므로(?) 해당 테스트 클래스 내의 테스트에서 sut 인스턴스의 프로퍼티와 메서드에 접근할 수 있게 된다.

→ setUpWithError 내의 코드는 매번 테스트마다 한 번씩 실행되는 것으로 알고 있다..ㅎ

sut = nil
try super.tearDownWithError()

그리고 잊기 전에 tearDownWithError() 메서드 내에 sut 개체에 nil 을 줌으로써 해제시키자.

이것은 모든 테스트가 깨끗한 상태로 시작되도록 SUT 객체 생성한 것을 이 곳에서 해제시켜준다.


Writing Your First Test

첫 번째 테스트 코드 작성!!

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

  // when
  sut.check(guess: guess)

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

테스트 메서드의 이름은 무조건 항상 test로 시작하고 테스트 대상에 대한 설명이 이후 따라야 한다. 테스트 목적과 내용을 명확하기 위해서 한글로도 많이 메서드명을 짓는 것 같다.

그리고 테스트를 알아보기 쉽게 section을 나누어서 작성한다.

  1. given : 여기에서 필요한 값을 설정한다. 이 예에서는 guess 값을 생성하여 targetValue와의 차이를 지정하고 있다.
  2. When : 여기에서는 테스트 받을 코드를 작성한다. check(guess:) 함수를 호출한다.
  3. then : 여기에서는 테스트가 실패할 경우 인쇄하는 메시지와 함께 예상한 결과를 주장하는 섹션이다. 예제에서는 sut.check(guess: guess) 를 통해서 sut.scoreRoun 의 값이 95이 되었다고 생각되어 해당 값들을 서로 비교해주고 있다. false일 경우에는 "Score computed from guess is wrong"이 출력될 것이다.

Apple Developer Documentation

애플 공식문서에 XCTestAssertions 리스트가 있으니 필요한 것을 가져다 쓰면 좋을 것 같다ㅎㅎ


Debugging a Test

BullsEyeGame 에 일부로 버그를 내장해뒀다고 하는데, 찾아보는 연습을 해보자!!

func  testScoreIsComputedWhenGuessIsLowerThanTarget () {
   // 주어진 
  let guess = sut.targetValue -  5

  // 언제
  sut.check (guess : guess)

  // then 
  XCTAssertEqual (sut.scoreRound, 95 , "추측에서 계산 된 점수가 잘못되었습니다." )
}

해당 코드를 테스트해보면 당연히 Fail 이 나온다. 왜냐고? 위에 코드랑 똑같은데 +5
→ -5로 했으면 당연히 반대로 결과가 나오겠지??ㅎㅎ

BreakPoint Navigator에서 Test Failure Breakpoint를 설정해보자!

그럼 Test Fail시에 Breakpoint가 걸리면서

해당 값에 대한 자세한 내용을 볼 수 있다.

더 상세하게 보기 위해서는 모델이 정의되어 있는 BullsEyeGame.swift 에 Breakpoint를 걸고 테스트를 실행해보면 된다.


XCTestExpectations을 사용하여 비동기 작업 테스트

이제 본격적으로 지금 BankManager 프로젝트에서 사용될만한 비동기 작업 테스트에 대해서 알아보자!!

예제 모델인 BullsEyeGame 에서는 URLSession 을 다음 게임의 목표로 난수를 얻는데 사용한다. URLSession 메서드는 비동기적이다. 즉시 반환되지만 나중에 실행이 완료되지 않는다. 비동기 메서드를 테스트하기 위해서는 XCTestExpectation 을 사용해서 비동기 함수가 완전히 끝날때까지 테스트가 리턴되지 않도록 해야된다.

일단 예제를 진행해보자!!

이 클래스의 모든 테스트는 URLSession 을 사용하여 요청을 전송하므로, SUT로 선언하고 객체를 생성하고 테스트가 끝나면 해제하도록 해주자!

var sut: URLSession!

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

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

그리고 테스트 함수를 추가해주자!!


// Asynchronous test: success fast, failure slow
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)
}

해당 테스트를 차근차근 알아가보자! 이 테스트는 유효한 요청을 보내면 statusCode 값으로 200을 보내는지 확인하는 테스트이다.

  1. expectation(descriptioin:) : XCTestsExpectation 을 반환한다. promise 상수에 저장되며, 예상되는 작업에 대해서 기술한다.
  2. promise.fulfill() : 비동기 작업의 completionHandler의 성공 조건에 부합하는 곳에 promise.fulfuill()을 호출하여서 expectation이 충족되었음을 알린다.
  3. wait(for: timeout: ): 모든 기대치가 충족되거나 timeout 간격이 끝날 때까지 테스트를 계속 실행한다.

해당 내용 BankManager 프로젝트에 적용

func test_Notification이_오는지_확인() {
    let expectation = XCTestExpectation(description: "성공")
    
    NotificationCenter.default.addObserver(self, selector: #selector(success),
      name: NSNotification.Name(rawValue: "completedCustomer"),
      object: nil)
        
    let customers: [Int: Customer] = [1:Customer(order: 1)]
    bankManager.setBankCounters(number: 1)
    bankManager.process(customers, completionHandler: {
      expectation.fulfill()
    })
    wait(for: [expectation], timeout: 3)
  }
  
  @objc func success() {
    XCTAssert(true)
  }


To be Continue....

참고 : 비동기 함수에 대한 test 작성에 대한 팁!! How to test asynchronous functions using expectation()
공식 문서 내에 테스트에 대한 내용 : Apple Devloper Document

profile
iOS Developer Student

0개의 댓글