Swift

13. Unit Test(3)

Daesiker 2022. 12. 23. 13:48
반응형

[iOS] Unit Test(1)
[iOS] Unit Test(2)

저번 포스팅에서는 Unit Test 프로젝트 생성 및 기초 설명 및 동기함수 테스트를 진행해 보았는데, 이번에는 비동기 통신 함수를 테스트하는 과정을 포스팅할 예정이다.

전체 코드

import XCTest
@testable import UnitTestExample

class UnitTextExampleSlow: XCTestCase {

    var sut: URLSession!
    let networkMonitor = NetworkMonitor.shared
    
    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 {
      try XCTSkipUnless(
        networkMonitor.isReachable,
        "Network connectivity needed for this test.")

      // given
      let urlString = "<https://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)
        
    }

    // Asynchronous test: faster fail
    func testApiCallCompletes() throws {
        
      try XCTSkipUnless(
        networkMonitor.isReachable,
        "Network connectivity needed for this test."
      )

      // given
      let urlString = "<http://www.randomnumberapi.com/api/v1.0/random?min=0&max=100&count=1>"
      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)
    }

}

이전에는 mock 객체인 sut을 BullsEyeGame 클래스로 선언했었는데, 이번에는 URLSession 클래스로 선언하였다. 또 NetworkMonitor 클래스의 static 변수를 선언했는데, 네트워크 변경 사항을 모니터링하고 대응하는 데 사용하는 관찰자로써 사용하였다.

testValidApiCallGetsHTTPStatusCode200()

func testValidApiCallGetsHTTPStatusCode200() throws {
      try XCTSkipUnless(
        networkMonitor.isReachable,
        "Network connectivity needed for this test.")

      // given
      let urlString = "<https://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)
        
    }
  • XCTSkipUnless( () → Bool, () → String) : 첫번째 파라미터의 값이 false이면 테스트를 중단하고 2번째 파라미터의 메세지가 나온다.
  • expectation(description: String) : 비동기 테스트에서 기대되는 결과값으로 description에 문자열은 실패를 진단하는 데 도움이 되도록 이 예상에 대한 테스트 로그에 표시할 문자열을 입력한다. 기대되는 결과값이 나오면 fulfill() 함수를 통해 알려준다.
  • wait(for: [XCTestExpectation], timeout: TimeInterval) : for 안에 있는 테스트가 fulfill()을 배출할 때까지 timeout시간동안 기다린다.

진행과정

  1. XCTSkipUnless를 통해 원할한 네트워크가 가능한지 테스트를 한다.
  2. 테스트하고 싶은 url과 테스트 결과를 담을 XCTestExpecation 객체를 선언한다.
  3. 데이터 통신을 할 때 테스트가 실패할 경우와 성공할 경우에 대한 dataTask를 작성한다.
  4. dataTask를 실행하고, 최대 5초 동안 성공 유무를 기다린다.

결론

XCTSkipUnless가 정상적으로 진행이 된다면, XCTestExpectation에서 fulfill이 발생하는 경우에는 timeout의 시간 즉, 5초와 상관없이 바로 테스트가 종료되지만, Fail이 발생하는 경우에는 시간에 상관없이 5초후에 결과를 알 수 있다. 그래서 성공할 때는 결과를 빨리 알 수 있지만, 실패할 때는 결과가 보여지는 시간이 길다.

testApiCallCompletes()

// Asynchronous test: faster fail
    func testApiCallCompletes() throws {
        
      try XCTSkipUnless(
        networkMonitor.isReachable,
        "Network connectivity needed for this test."
      )

      // given
      let urlString = "<http://www.randomnumberapi.com/api/v1.0/random?min=0&max=100&count=>"
      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)
    }

앞의 코드의 문제점인 실패를 했을 때 timeout 시간동안 기다려야하는 문제를 해결하기 위해 dataTask를 조금 수정하였다. 이전 코드는 statusCode가 200일 경우에만 promise를 fulfill() 하였지만 이번 코드를 보면 상태코드와 에러를 담는 변수를 옵셔널로 선언하여 nil이던 아니던 해당 변수에 담고 promise를 fulfill하였다. 이렇게 되면 해당 코드가 에러가 있던 없던 바로 promise가 종료되기 떄문에 성공 유무에 시간 차이가 존재하지 않는다.

반응형

'Swift' 카테고리의 다른 글

<Value Type> Set  (1) 2024.03.03
14. UI Test  (0) 2022.12.31
12. Unit Test(2)  (0) 2022.12.16
11. Unit Test(1)  (0) 2022.12.09
10. 멀티 스레딩과 GCD  (0) 2022.12.04