版本记录
版本号 | 时间 |
---|---|
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 Testing
和UI Testing
,感兴趣的给个赞或者关注~~~