[swift] Xcode에서 Unit Test 연습하기

[swift] Xcode에서 Unit Test 연습하기

진행하고 있는 프로젝트를 리팩토링 하면서, 내가 한 리팩토링이 잘 되었는지는 둘째치고 이전에 기능들은 잘 되고 있는지에 대한 불안감이 생겼다.

  1. 원래 되고있던 기능이 뭐지
  2. 제대로 되었다는것이 어떤 것을 의미하지

테스트 코드의 작성을 통해 위 두 문제를 해결할 수 있다고 해서 정리하기 시작했다.

리소스 다운
원문
번역본

모든 내용을 다 정리하지도 해 보지도 않았다 앞으로 필요한 테스트를 위한 기본 몸풀기 정도만 해 보았다. 추후에 해당 아티클들의 뒷 부분이 필요한 때에 다시 해 보고 정리하기로 한다.

테스트를 위한 네비게이션이 있었다. 뭐하는데 쓰는 것일까 궁금했더 곳이다. Command(⌘)+6으로 창을 열 수 있다. 왼쪽 아래의 + 버튼을 눌러 새로운 유닛테스트를 위해 New Unit Test Target…을 눌러 생성한다.


처음에 만들면 4개의 메소드가 있다.

  • setUpWithError() : 테스트를 위해 설정
  • tearDownWithError() : 테스트가 종료 된 후 삭제
  • testExample() -> 삭제
  • testPerformanceExample() -> 삭제

BullsEye

우리는 50으로 부터 랜덤으로 값을 생성하고, 슬라이더를 움직여 목표 값과 비슷한 값을 입력하는 게임을 테스트 하려한다.

프로젝트 설정

1 . @testable import BullsEye를 입력해 테스트 코드 파일의 최상단에 프로젝트를 임포트 해주자.
2 . var sut: BullsEyeGame!프로퍼티를 class BullsEyeGame 타입으로 만들어준다.

setUpWithError

super.setUp()
sut = BullsEyeGame()
sut.startNewGame()

인스턴스를 생성하고, startNewGame까지 실행한 상태로 설정한다.

tearDownWithError

sut = nil
super.tearDown()

테스트가 끝 났을 때 sut을 해제하고, tearDown 해준다.

testScoreIsComputed

점수를 계산하는 check() 메소드를 테스트 해보자. 테스트의 규칙은

  1. given - 입력값
  2. when - 테스트 대상의 메소드 실행
  3. then - 결과값 비교
    위와 같이 정한다.
func testScoreIsComputed() {
  // 1. given : targetValue에서 5 차이 난 값 입력 
  let guess = sut.targetValue + 5

  // 2. when : 변수가 입력된 check 메소드가 실행되었을 때 
  sut.check(guess: guess)

  // 3. then : 내가 기대한 값은 95와 scoreRound가 같은 값이어야한다.
  XCTAssertEqual(sut.scoreRound, 95, "Score computed from guess is wrong")
}

testScoreIsComputedWhenGuessLTTarget

그렇다면 목표 금액보다 5 작은 값이 들어 갔을 때에도 5점 작은 95점이라는 결과 값이어야한다.

func testScoreIsComputedWhenGuessLTTarget() {
    // 1. given
    let guess = sut.targetValue - 5
    
    // 2. when
    sut.check(guess: guess)
    
    // 3. then
    XCTAssertEqual(sut.scoreRound, 95, "Score computed from guess is wrong")
  }

실행하면 실패한다. 그 이유는 check()에 버그가 있그 때문이다. 입력받은 값을 빼 주기 때문에, 우리가 예상했던 96가 아닌 105라는 결과값을 반환했던 것이다.
디버그 창으로 가서 Test Failure Breakpoint를 추가하면, 실패한 테스트 케이스에서 디버깅이 시작된다.

잘못 작성된 check() 메소드의 let difference = guess - targetValuelet difference = abs(targetValue - guess)로 바꿔준다. 이제 무조건 빼는 게 아닌 절대값의 차이를 구하게 될 것이다. 테스트 성공.

HalfTunes

검색어를 입력하면 가수를 검색해 주는 반쪽짜리 아이튠즈를 테스트 하자.
요청을 날렸을 때, 응답에 따라 잘 동작하는지에 대한 테스트 이다.
해당 프로젝트를 열고 UnitTest를 추가해 주자.

setting

import XCTest
@testable import HalfTunes

class HalfTunesSlowTests: XCTestCase {
  
  var sut: URLSession!
  
  override func setUp() {
    super.setUp()
    sut = URLSession(configuration: .default)
  }
  
  override func tearDown() {
    sut = nil
    super.tearDown()
  }

URL요청을 날리기 위해 URLSession으로 만들어준다.

testValidCallToiTunesGetsHTTPStatusCode200

// Asynchronous test: success fast, failure slow
  func testValidCallToiTunesGetsHTTPStatusCode200() {
    // given : abba라고 사용자 입력하면 아래 url이 생성된다.
    let url =
      URL(string: "https://itunes.apple.com/search?media=music&entity=song&term=abba")
    // 1 : 비동기 테스트를 위한 expectation을 사용한다.
    let promise = expectation(description: "Status code: 200")
    
    // when : 요청을 해서 응답을 받았을 때
    let dataTask = sut.dataTask(with: url!) { data, 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 : 응답 코드가 200 이면
          promise.fulfill()
        } else {
          XCTFail("Status code: \(statusCode)")
        }
      }
    }
    dataTask.resume()
    // 3
    wait(for: [promise], timeout: 5)
  }

위 테스트에서는 요청에 바로 응답하는게 아닌 비동기로 짜여진 코드를 테스트 하기 위해 비동기 테스트를 진행한다. 응답 코드가 200이면 실행을 알리는 fulfill()을 실행한다.
하지만 이 테스트는 요청이 정상이 아닐 때에는 5초동안 기다렸다가 실패로 판단한다. 만약 200 말고도 다른 결과(404error)가 있다면 테스트 결과를 받기위해 5초(더 길게 설정한다면, 더 긴시간)를 기다려야 한다.
테스트를 개선해보자.

testCallToiTunesCompletes

func testCallToiTunesCompletes() {
    // given : 비동기 테스트를 위한 expectation을 사용한다.
    let url =
      URL(string: "https://itunes.apple.com/search?media=music&entity=song&term=abba")
    let promise = expectation(description: "Completion handler invoked")
    var statusCode: Int?
    var responseError: Error?
    
    // when : 요청을 해서 응답을 받았을 때 바로 실행된다.
    let dataTask = sut.dataTask(with: url!) { data, response, error in
      statusCode = (response as? HTTPURLResponse)?.statusCode
      responseError = error
      promise.fulfill()
    }
    dataTask.resume()
    wait(for: [promise], timeout: 5)
    
    // then : 200 이면 테스트 통과 아니면 실패
    XCTAssertNil(responseError)
    XCTAssertEqual(statusCode, 200)
  }

응답 결과를 가지고 통과 실패가 바로 나온다. 실제 타임아웃인 상황에 대해서만 타임아웃 테스트가 실패한다.

Result

TestCode에 대해서 다 알게 되지는 않았다. 하지만 더 공부 하는 것 보다는 내가 지금 짤 수 있는 테스트 케이스를 작성하면서 더 복잡한 테스트 코드를 짜는게 빠르게 공부할 수 있을 것 같아 이 뒷부분은 생략한다. 좀 더 테스트 코드에 익숙해지면 복잡한 부분을 진행해보자. 실제 테스트 코드를 짜면서 생긴 어려움을 추가로 정리해야겠다.

donaricano-btn

댓글

이 블로그의 인기 게시물

[IOS] AppDelegate는 뭐하는 녀석이지?

[git] Github 이슈 라벨(issue labels)

[git] git의 upstream과 origin 헷갈리는 사람 손!