美文网首页工作方法iOS精品
自动化Test使用详细解析(七) —— 关于Unit Testi

自动化Test使用详细解析(七) —— 关于Unit Testi

作者: 刀客传奇 | 来源:发表于2021-05-20 09:58 被阅读0次

    版本记录

    版本号 时间
    V1.0 2021.05.20 星期四

    前言

    自动化Test可以通过编写代码、或者是记录开发者的操作过程并代码化,来实现自动化测试等功能。接下来几篇我们就说一下该技术的使用。感兴趣的可以看下面几篇。
    1. 自动化Test使用详细解析(一) —— 基本使用(一)
    2. 自动化Test使用详细解析(二) —— 单元测试和UI Test使用简单示例(一)
    3. 自动化Test使用详细解析(三) —— 单元测试和UI Test使用简单示例(二)
    4. 自动化Test使用详细解析(四) —— 单元测试和UI Test(一)
    5. 自动化Test使用详细解析(五) —— 单元测试和UI Test(二)
    6. 自动化Test使用详细解析(六) —— 关于Unit Testing 和 UI Testing(一)

    源码

    1. Swift

    首先看下工程组织结构

    下面就是源码了

    1. AppDelegate.swift
    
    import UIKit
    
    @UIApplicationMain
    class AppDelegate: UIResponder, UIApplicationDelegate {
      var window: UIWindow?
    }`
    
    2. BullsEyeGame.swift
    
    import Foundation
    
    class BullsEyeGame {
      var round = 0
      let startValue = 50
      var targetValue = 50
      var scoreRound = 0
      var scoreTotal = 0
    
      var urlSession: URLSessionProtocol = URLSession.shared
    
      init() {
        startNewGame()
      }
    
      func startNewGame() {
        round = 1
        scoreTotal = 0
      }
    
      func startNewRound(completion: @escaping () -> Void) {
        round += 1
        scoreRound = 0
        getRandomNumber { newTarget in
          self.targetValue = newTarget
          DispatchQueue.main.async {
            completion()
          }
        }
      }
    
      @discardableResult
      func check(guess: Int) -> Int {
        let difference = abs(targetValue - guess)
        scoreRound = 100 - difference
        scoreTotal += scoreRound
        return difference
      }
    
      func getRandomNumber(completion: @escaping (Int) -> Void) {
        guard let url = URL(string: "http://www.randomnumberapi.com/api/v1.0/random?min=0&max=100&count=1") else {
          return
        }
        let task = urlSession.dataTask(with: url) { data, _, error in
          do {
            guard
              let data = data,
              error == nil,
              let newTarget = try JSONDecoder().decode([Int].self, from: data).first
            else {
              return
            }
            completion(newTarget)
          } catch {
            print("Decoding of random numbers failed.")
          }
        }
        task.resume()
      }
    }
    
    3. ViewController.swift
    
    import UIKit
    
    class ViewController: UIViewController {
      var defaults = UserDefaults.standard
    
      @IBOutlet weak var targetGuessLabel: UILabel!
      @IBOutlet weak var targetGuessField: UITextField!
      @IBOutlet weak var roundLabel: UILabel!
      @IBOutlet weak var scoreLabel: UILabel!
      @IBOutlet weak var slider: UISlider!
      @IBOutlet weak var segmentedControl: UISegmentedControl!
    
      let game = BullsEyeGame()
      enum GameStyle: Int { case moveSlider, guessPosition }
      let gameStyleRange = 0..<2
      var gameStyle = GameStyle.guessPosition
    
      override func viewDidLoad() {
        super.viewDidLoad()
    
        let defaultGameStyle = defaults.integer(forKey: "gameStyle")
        print(defaultGameStyle)
        if gameStyleRange.contains(defaultGameStyle) {
          gameStyle = GameStyle(rawValue: defaultGameStyle) ?? .moveSlider
          segmentedControl.selectedSegmentIndex = defaultGameStyle
        } else {
          gameStyle = .moveSlider
          defaults.set(0, forKey: "gameStyle")
        }
        updateView()
      }
    
      @IBAction func chooseGameStyle(_ sender: UISegmentedControl) {
        if gameStyleRange.contains(sender.selectedSegmentIndex) {
          gameStyle = GameStyle(rawValue: sender.selectedSegmentIndex) ?? .moveSlider
          updateView()
        }
        defaults.set(sender.selectedSegmentIndex, forKey: "gameStyle")
      }
    
      func updateView() {
        switch gameStyle {
        case .moveSlider:
          targetGuessLabel.text = "Get as close as you can to: "
          targetGuessField.text = "\(game.targetValue)"
          targetGuessField.isEnabled = false
          slider.value = Float(game.startValue)
          slider.isEnabled = true
        case .guessPosition:
          targetGuessLabel.text = "Guess where the slider is: "
          targetGuessField.text = ""
          targetGuessField.placeholder = "1-100"
          targetGuessField.isEnabled = true
          slider.value = Float(game.targetValue)
          slider.isEnabled = false
        }
        roundLabel.text = "Round: \(game.round)"
        scoreLabel.text = "Score: \(game.scoreTotal)"
      }
    
      @IBAction func checkGuess(_ sender: Any) {
        var guess: Int?
        switch gameStyle {
        case .moveSlider:
          guess = Int(lroundf(slider.value))
        case .guessPosition:
          targetGuessField.resignFirstResponder()
          guess = Int(targetGuessField.text ?? "")
        }
        if let guess = guess {
          showScoreAlert(difference: game.check(guess: guess))
        } else {
          showNaNAlert()
        }
      }
    
      func showScoreAlert(difference: Int) {
        let title = "you scored \(game.scoreRound) points"
        let message = "target value \(game.targetValue)"
        let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
        let action = UIAlertAction(title: "OK", style: .default) { _ in
          self.game.startNewRound {
            alert.dismiss(animated: true, completion: nil)
            self.updateView()
          }
        }
        alert.addAction(action)
        present(alert, animated: true, completion: nil)
      }
    
      func showNaNAlert() {
        let alert = UIAlertController(
          title: "Not A Number",
          message: "Please enter a positive number",
          preferredStyle: .alert
        )
        let action = UIAlertAction(title: "OK", style: .default, handler: nil)
        alert.addAction(action)
        present(alert, animated: true, completion: nil)
      }
    
      @IBAction func startOver(_ sender: Any) {
        game.startNewGame()
        updateView()
      }
    }
    
    4. URLSessionStub.swift
    
    import Foundation
    
    typealias DataTaskCompletionHandler = (Data?, URLResponse?, Error?) -> Void
    protocol URLSessionProtocol {
      func dataTask(
        with url: URL,
        completionHandler: @escaping DataTaskCompletionHandler
      ) -> URLSessionDataTask
    }
    
    extension URLSession: URLSessionProtocol { }
    
    class URLSessionStub: URLSessionProtocol {
      private let stubbedData: Data?
      private let stubbedResponse: URLResponse?
      private let stubbedError: Error?
    
      public init(data: Data? = nil, response: URLResponse? = nil, error: Error? = nil) {
        self.stubbedData = data
        self.stubbedResponse = response
        self.stubbedError = error
      }
    
      public func dataTask(
        with url: URL,
        completionHandler: @escaping DataTaskCompletionHandler
      ) -> URLSessionDataTask {
        URLSessionDataTaskStub(
          stubbedData: stubbedData,
          stubbedResponse: stubbedResponse,
          stubbedError: stubbedError,
          completionHandler: completionHandler
        )
      }
    }
    
    class URLSessionDataTaskStub: URLSessionDataTask {
      private let stubbedData: Data?
      private let stubbedResponse: URLResponse?
      private let stubbedError: Error?
      private let completionHandler: DataTaskCompletionHandler?
    
      init(
        stubbedData: Data? = nil,
        stubbedResponse: URLResponse? = nil,
        stubbedError: Error? = nil,
        completionHandler: DataTaskCompletionHandler? = nil
      ) {
        self.stubbedData = stubbedData
        self.stubbedResponse = stubbedResponse
        self.stubbedError = stubbedError
        self.completionHandler = completionHandler
      }
    
      override func resume() {
        completionHandler?(stubbedData, stubbedResponse, stubbedError)
      }
    }
    
    5. NetworkMonitor.swift
    
    import Network
    
    class NetworkMonitor {
      static let shared = NetworkMonitor()
      var isReachable: Bool { status == .satisfied }
    
      private let monitor = NWPathMonitor()
      private var status = NWPath.Status.requiresConnection
    
      private init() {
        startMonitoring()
      }
    
      func startMonitoring() {
        monitor.pathUpdateHandler = { [weak self] path in
          self?.status = path.status
        }
        let queue = DispatchQueue(label: "NetworkMonitor")
        monitor.start(queue: queue)
      }
    
      func stopMonitoring() {
        monitor.cancel()
      }
    }
    
    6. BullsEyeTests.swift
    
    import XCTest
    @testable import BullsEye
    
    // swiftlint:disable implicitly_unwrapped_optional
    class BullsEyeTests: XCTestCase {
      var sut: BullsEyeGame!
    
      override func setUpWithError() throws {
        try super.setUpWithError()
        sut = BullsEyeGame()
      }
    
      override func tearDownWithError() throws {
        sut = nil
        try super.tearDownWithError()
      }
    
      func testScoreIsComputedWhenGuessIsHigherThanTarget() {
        // 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")
      }
    
      func testScoreIsComputedWhenGuessIsLowerThanTarget() {
        // 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")
      }
    
      func testScoreIsComputedPerformance() {
        measure(metrics: [XCTClockMetric(), XCTCPUMetric(), XCTStorageMetric(), XCTMemoryMetric()]) {
          sut.check(guess: 100)
        }
      }
    }
    
    7. BullsEyeFakeTests.swift
    
    import XCTest
    @testable import BullsEye
    
    // swiftlint:disable implicitly_unwrapped_optional
    // swiftlint:disable force_unwrapping
    class BullsEyeFakeTests: XCTestCase {
      var sut: BullsEyeGame!
    
      override func setUpWithError() throws {
        try super.setUpWithError()
        sut = BullsEyeGame()
      }
    
      override func tearDownWithError() throws {
        sut = nil
        try super.tearDownWithError()
      }
    
      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: "Completion handler invoked")
    
        // when
        sut.startNewRound {
          // then
          // 2
          XCTAssertEqual(self.sut.targetValue, 1)
          promise.fulfill()
        }
        wait(for: [promise], timeout: 5)
      }
    }`
    
    8. BullsEyeMockTests.swift
    
    import XCTest
    @testable import BullsEye
    
    class MockUserDefaults: UserDefaults {
      var gameStyleChanged = 0
      override func set(_ value: Int, forKey defaultName: String) {
        if defaultName == "gameStyle" {
          gameStyleChanged += 1
        }
      }
    }
    
    // swiftlint:disable implicitly_unwrapped_optional
    class BullsEyeMockTests: XCTestCase {
      var sut: ViewController!
      var mockUserDefaults: MockUserDefaults!
    
      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()
      }
    
      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")
      }
    }
    
    9. BullsEyeSlowTests.swift
    
    import XCTest
    @testable import BullsEye
    
    // swiftlint:disable implicitly_unwrapped_optional
    // swiftlint:disable force_unwrapping
    class BullsEyeSlowTests: 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 = "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)
      }
    
      // 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)
      }
    }
    
    10. BullsEyeUITests.swift
    
    import XCTest
    
    // swiftlint:disable implicitly_unwrapped_optional
    class BullsEyeUITests: XCTestCase {
      var app: XCUIApplication!
    
      override func setUpWithError() throws {
        try super.setUpWithError()
        continueAfterFailure = false
    
        app = XCUIApplication()
        app.launch()
      }
    
      func testGameStyleSwitch() throws {
        // 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: "]
    
        // 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)
        }
      }
    }
    

    后记

    本篇主要讲述了关于Unit TestingUI Testing,感兴趣的给个赞或者关注~~~

    相关文章

      网友评论

        本文标题:自动化Test使用详细解析(七) —— 关于Unit Testi

        本文链接:https://www.haomeiwen.com/subject/webjjltx.html