如何编写iOS的自动测试

原文地址

作为一个好的开发者,你会尽全力测试全部的功能和你写的代码逻辑及其结果。但是很少会把所有的逻辑和结果都测试到。

随着应用体积增大和复杂度增加,十有八九手动的测试会让你忽视到越来越多东西。

自动测试,包括UI测试和后端APIs,会让你的工作更加自信,并且减少开发,重构,添加新功能,或者是更改已有功能的压力。

有了自动测试,你可以:

  • 减少bug: 没有办法可以完全去除代码中的bugs,但是自动测试可以极大减少bug的数量。
  • 改动更加有信心:添加新功能时避免出现bug,这意味着你可以更快的做出代码的调整,又不会痛苦。
  • 代码的文档化: 作为一个开发者,你有时候可能会害怕重构,尤其是重构一大堆代码的时候,单元测试(Unit tests)可以保证重构的代码可以和你预期的一样正常工作。

这篇文章教你如何构建并执行iOS平台上的自动测试。

Unit Tests vs. UI Tests

区分单元测试和UI测试很重要。

单元测试是在一个特定的上下文中对某个功能的测试。单元测试负责验证被测试的那部分代码(通常是一个单一的方法)能够按照目的正常工作,有大量关于单元测试的文章和博客,所以这里我们并不覆盖这个部分。

UI测试是用来测试交互界面的,例如,界面是否按预期刷新,或者在用户操控界面元素时候其指定方法是否被调用。

每个UI测试只测试一个单独明确的用户操作,自动测试能够,也应该,在单元测试和UI测试的层面上进行。

构建自动测试

由于Xcode支持现成的单元测试和UI测试,添加到你的工程中就很简单了,当创建新的工程时,只要勾选“Include Unit Tests” 和“Include UI Tests.”

当项目构建成功时,两个targets会加到你的项目目录中,名称分别是"XXX Tests" 或者 "XXX UITests"。

就这样,你就可以开始写你项目的自动测试了。

如果想给现已有的项目添加UI和单元测试的话,你就要多做几个步骤,但是仍然很简单。

打开File -> New -> Target 然后选 iOS Unit Testing Bundle 或者 iOS UI Testing Bundle,按下一步,选择被测试的目标就可以了。

编写单元测试

在我们写单元测试前,我们必须理解他的结构。
当你将单元测试包含到你的项目中时,会创建一个示例的测试类,像这样:

import XCTest
@testable import TestingIOS

class TestingIOSTests: XCTestCase {
    
    override func setUp() {
        super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.

    }
    
    override func tearDown() {
        // Put teardown code here. This method is called after the invocation of each test method in the class.
        super.tearDown()
    }
    

    func testExample() {
        // This is an example of a functional test case.
        // Use XCTAssert and related functions to verify your tests produce the correct results.
    }
    

    func testPerformanceExample() {
        // This is an example of a performance test case.
        self.measure {
            // Put the code you want to measure the time of here.
        }
    }
    
}

最重要需要理解的方法是 setUp 和 tearDown。
setUp方法是在每个测试方法前被用的,tearDown相反。
如果我们运行这个示例中代码,他会按如下顺序执行:

setUp → testExample → tearDown setUp → testPerformanceExample → tearDown

Tips: 按cmd + U 运行测试

如果你只想运行一个指定的测试方法,点击方法左边的小方块,如图:

现在,如果你准备好了,你就可以写测试代码了。

添加一个负责用户注册的界面,用户会添加邮箱,密码,和确认密码,我们的示例类会负责检查输入的合法性,并尝试注册。
注意: 此例使用MVVM结构,用MVVM是用为它能使应用结构更清晰,更易于测试。

有了MVVVM,我们就更容易区分业务逻辑和展示逻辑,从而避免大体量视图控制器的问题。

MVVM的详细内容并不在此文范围,你可以从这里获取更多信息。

我们创建一个view-model类来负责用户的注册..

class RegisterationViewModel {
        var emailAddress: String? {
            didSet {
                enableRegistrationAttempt()
            }
        }
        var password: String? {
            didSet {
                enableRegistrationAttempt()
            }
        }
        var passwordConfirmation: String? {
            didSet {
                enableRegistrationAttempt()
            }
        }
        var registrationEnabled = Dynamic(false)
        var errorMessage = Dynamic("")
        var loginSuccessful = Dynamic(false    
        var networkService: NetworkService
        init(networkService: NetworkService) {
            self.networkService = networkService
        }
}

首先,我们添加了写属性,动态属性,和一个初始化方法。

不必担心Dynamic,他是MVVM中的一部分。

当一个Dyanmic<Bool>的值设为true时,被这个viewmodel绑定视图控制器会激活注册按钮,当loginSuccessful设为true时,他相连的视图也会被更新。
现在添加一些方法来检查密码和邮箱的合法性。

func enableRegistrationAttempt() {
    registrationEnabled.value = emailValid() && passwordValid()
}
func emailValid() -> Bool {
    let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}"
    let emailTest = NSPredicate(format:"SELF MATCHES %@", emailRegEx)
    return emailTest.evaluate(with: emailAddress)
} 
func passwordValid() -> Bool {
    guard let password = password,
    let passwordConfirmation = passwordConfirmation else {
    return false
}
let isValid = (password == passwordConfirmation) &&
    password.characters.count >= 6
    return isValid
}

每次用户在邮箱或者密码输入框键入些什么时,enableRegistrationAttempt方法会被激发来检查其是否是正确的格式,或者通过registrationEnabled这个动态属性决定注册按钮是否可用。

为了保证本例简单,添加了两个方法-- 一个是检测邮箱是否可用,一个是尝试使用 用户填写的用户名和密码注册。

func checkEmailAvailability(email: String, withCallback callback: @escaping (Bool?)->(Void)) {
        networkService.checkEmailAvailability(email: email) { (available, error) in
            if let _ = error {
                self.errorMessage.value = "Our custom error message"
            } else if !available {
                self.errorMessage.value = "Sorry, provided email address is already taken"
                self.registrationEnabled.value = false
                callback(available)
            }
        }
}
func attemptUserRegistration() {
        guard registrationEnabled.value == true else { return }
        // 还是为了简单,此处密码未哈希
        guard let emailAddress = emailAddress,
        let passwordHash = password else { return }
        networkService.attemptRegistration(forUserEmail: emailAddress, withPasswordHash: passwordHash) {
            (success, error) in 
            if let _ = error {
                self.errorMessage.value = "Our custom error message"
            } else {
                self.loginSuccessful.value = true
            }
        }   
}   

API处理方法简便起见就写了个假的,NetworkService是一个协议,通过NetworkServiceImpl实现。

typealias RegistrationAttemptCallback = (_ success: Bool, _ error: NSError?) -> Void
typealias EmailAvailabilityCallback = (_ available: Bool, _ error: NSError?) -> Void

protocol NetworkService {
    func attemptRegistration(forUserEmail email: String, withPasswordHash passwordHash: String,
                             andCallback callback: @escaping RegistrationAttemptCallback)    
    func checkEmailAvailability(email: String, withCallback callback: @escaping EmailAvailabilityCallback)
}
class NetworkServiceImpl: NetworkService {
    func attemptRegistration(forUserEmail email: String, withPasswordHash passwordHash: String, andCallback callback: @escaping RegistrationAttemptCallback) 
{
        // Make it look like method needs some time to communicate with the server
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(1), execute: {
            callback(true, nil)
        })
    }
func checkEmailAvailability(email: String, withCallback callback: @escaping EmailAvailabilityCallback) {
        // Make it look like method needs some time to communicate with the server
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(1), execute: {
            callback(true, nil)
        })
    }
}

现在,一个完整的例子已经写好,我们可以写一个覆盖到这些类的单元测试。

1,为我们的viewmodel新建一个测试类,在TestingIOSTests文件夹中右击,然后 NewFile-> Unit Test Case Class,命名为RegistrationViewModelTests

2,把testExampletestPerformanceExample删了,因为不需要他们。

3,由于Swift使用的modules 和我们使用的不同,我们需要在import声明和类定义之间添加一个@testable, 不然,我们无法应用我们的方法或则类。

4,添加registrationViewModel变量。

整个应该看起来是这样:

import XCTest
@testable import TestingIOS
class RegistrationViewModelTests: XCTestCase{
    var registrationViewModel: RegisterationViewModel?
    override func setUp() {
        super.setUp()
    }
    override func tearDown() {
        super.tearDown()
    }
}

让我们试试写一个测试emailVaild的方法,取名testEmailValid。 在方法的前面添加一个test关键词很重要,否则,这个方法不会被识别为测试方法。

我们的测试方法就像这样:

func testEmailValid() {
    let registrationVM = RegisterationViewModel(networkService: NetworkServiceImpl())
    registrationVM.emailAddress = "email.test.com"
    XCTAssertFalse(registrationVM.emailValid(), "\(registrationVM.emailAddress) shouldn't be correct")   
    registrationVM.emailAddress = "email@test"
    XCTAssertFalse(registrationVM.emailValid(), "\(registrationVM.emailAddress) shouldn't be correct")
    registrationVM.emailAddress = nil
    XCTAssertFalse(registrationVM.emailValid(), "\(registrationVM.emailAddress) shouldn't be correct")
    registrationVM.emailAddress = "email@test.com"
    XCTAssert(registrationVM.emailValid(), "\(registrationVM.emailAddress) should be correct")
}

我们的使用断言方法,用来检查每个情况的true或者false。
若是false,assert会报错(和整个方法一起),并且输出信息。
其他可以使用的断言方式还有: XCTAssertEqualObjects, XCTAssertGreaterThan, XCTAssertNil, XCTAssertTrue 或者 XCTAssertThrows。
如果你现在运行测试,方法会通过。你已经成功的创建了你的第一个测试方法,但是实际上还没有真正的准备好。这个方法由三个问题存在(一个大的,两个小的)

问题1:你直接使用了 NetworkService协议的实现方法

单元测试的主要准则之一就是,每一个测试都应该独立于外部变量或者是依赖,单元测试应该是自动的。

如果你在测试一个方法,比如测试一个依赖于后台的API方法,那么这个测试就会关联到你的网络代码和后台的实际情况,若后台在测试没有运行,你的测试就会失败。

在这种情况下,你测试RegistrationViewModel的方法,RegistrationViewModel依赖于NetworkServiceImpl类,即使我们要测试的方法emailValid并不是直接依赖于 NetworkServiceImpl
写单元测试的时候,所有的外部依赖需要被移除,但是关键是怎样移除NetworkService的依赖同事又不更改RegistrationViewModel的实现呢?

有个简单的解决方案,叫做 Object Mocking.
若果你仔细看RegistrationViewModel的时候,你会发现他遵守NetworkService协议,当RegistrationViewModel初始化时,NetworkService的实现就会被给到或者是注入到RegistrationViewModel对象中。

这个原则称为dependency injection via constructor,(还有其他更多种类的依赖注入)。

网上有很多的关于依赖注入的文章,看这里这里

RegistrationViewModel实例化时,他会注入一个NetworkService 协议的实现
let registrationVM = RegisterationViewModel(networkService: NetworkServiceImpl())

由于我们的viewmodel依赖于这个协议,所以可以创建一个自定义的(或者mocked(虚拟的))NetworkService实现类并且把这个mocked类注入到viewmodel对象中。

让我们开始创建虚拟的NetworkService协议实现类。

TestingIOSTests下新建一个叫NetworkServiceMock的swift文件。

里面写:

import Foundation
@testable import TestingIOS
class NetworkServiceMock: NetworkService {
    func attemptRegistration(forUserEmail email: String,
                             withPasswordHash passwordHash: String,
                             andCallback callback: @escaping RegistrationAttemptCallback) {
        // Make it look like method needs some time to communicate with the server
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(1), execute: {
            callback(true, nil)
        })
    }
    func checkEmailAvailability(email: String, withCallback callback: @escaping EmailAvailabilityCallback) {
        // Make it look like method needs some time to communicate with the server
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(1), execute: {
            callback(false, nil)
        })
    }
}

看起来和NetworkServiceImpl没什么区别,但是在实际的生产环境中,NetworkServiceImpl还会包含网络代码,网络返回的处理,类似的代码。

而这个虚拟的类什么也不做,这也是他的意义,因为他什么都不做的就可以在测试中排除他了。

所以解决这第一个问题,我们的测试方法应该这么写:

let registrationVM = RegisterationViewModel(networkService: NetworkServiceMock())

<font color=#41639B>问题2:viewmodel直接在测试方法体中被实例化</font>

setUp``tearDown的存在是有意义的。

这些方法是用来初始化或者配置在测试中需要的对象用的,你应该使用这些方法避免代码在各个方法里写好多次,当不使用这些方法也不是什么大问题,尤其是当你需要一些特殊的配置时。

RegistrationViewModel的初始化比较简单,就直接在setUp和tearDown中重构了。

class RegistrationViewModelTests: XCTestCase {
    var registrationVM: RegisterationViewModel!
    override func setUp() {
        super.setUp()
        registrationVM = RegisterationViewModel(networkService: NetworkServiceMock())
    }
    override func tearDown() {
        registrationVM = nil
        super.tearDown()
    }
    func testEmailValid() {
        registrationVM.emailAddress = "email.test.com"
        XCTAssertFalse(registrationVM.emailValid(), "\(registrationVM.emailAddress) shouldn't be correct")
        ...
    }
}

<font color=#41639B>问题3在一个测试方法中存在多个断言:</font>

虽然这不是什么大问题,但是有些主张每个测试方法里只有一个断言。

这样做的主要原因是为了侦测错误。

当一个测试方法含有多个测试断言时,一个失败,整个方法就会被标记为错误,其他的断言就没有测试到。

这样的话你一次这能测一个错误,你也不能知道其他的断言有没有失败。

想我们这种情况,这测试邮件的合法性,所以代码可以保持原样。(.....)

<font color=#41639B>通过异步调用来测试方法</font>

无论应用有多简单,某个方法都有可能性在一个异步的线程中被调用,尤其是你一贯的在他自己的线程中进行UI。

单元测试中异步调用的主要的问题是需要时间来结束,但是单元测试不会等到他结束,就是异步的block还没执行,单元测试已经结束了,导致我们的测试结果都是一个样(无论你在block中写什么)。
下面是一个示例,

func testCheckEmailAvailability() {
        registrationVM.registrationEnabled.value = true
        registrationVM.checkEmailAvailability(email: "email@test.com") {
            available in
            XCTAssert(self.registrationVM.registrationEnabled.value == false, "Email address is not available, registration should be disabled")
        }
    }

这里你想要测试在 我们的方法告诉你某个邮箱已经被占用时,registrationEnabled 是否会变成false。
结果肯定是通过测试,

但是如果改成这样:
XCTAssert(self.registrationVM.registrationEnabled.value == true, "Email address is not available, registration should be disabled")

测试的结果依然是通过测试。
原因就是上述的,幸运的是,Xcode6 中添加了XCTestExpectation类,其工作流程是:

1, 在测试的开始部分设置你的测试预期(expectation)--- 就是一句话描述你想要在测试中测试什么。
2, 在一个异步的block中,你实现这个预期(expectation)。
3, 在测试的结尾你需要设置waitForExpectationWithTimerblock,这个代码块会在预期(expectation)完成时或者是计时器跑完
4, 现在,单元测试除非在预期完成或者计时器到时,否则不会结束。

像下面这样写:

func testCheckEmailAvailability() {
        // 1. Setting the expectation
        let exp = expectation(description: "Check email availability")
        registrationVM.registrationEnabled.value = true
        registrationVM.checkEmailAvailability(email: "email@test.com") {
            available in
            XCTAssert(self.registrationVM.registrationEnabled.value == true, "Email address is not available, registration should be disabled")
            // 2. Fulfilling the expectation
            exp.fulfill()
        }
        // 3. Waiting for expectation to fulfill
        waitForExpectations(timeout: 3.0) {
            error in
            if let _ = error {
                XCTAssert(false, "Timeout while checking email availability")
            }
        }
    }

现在再跑一遍测试,就会发现和我们预期的一样没有通过测试。

用不带回调的闭包测试方法

我们的示例工程中的方法attemptUserRegistration使用的NetworkService.attemptRegistration方法中包含了异步的代码: 尝试用后台的API注册一个用户。

在例子里,这个方法等待1秒来模拟网络请求,并完成注册过程。若注册成功,则loginSuccessful被该成true,让我们做一下这个过程的测试。

func testAttemptRegistration() {
        registrationVM.emailAddress = "email@test.com"
        registrationVM.password = "123456"
        registrationVM.attemptUserRegistration()
        XCTAssert(registrationVM.loginSuccessful.value, "Login must be successful")
}

测试结果是失败的,因为networkService.attemptRegistration没有执行, loginSuccessful也就没有被设为true。

我们在NetworkServiceImpl写过一个attemptRegistration方法,他是等了1秒再返回成功的回调,你可以在这里使用GCD,利用asyncAfter方法来1秒后执行断言方法,代码像这样:

func testAttemptRegistration() {
        registrationVM.emailAddress = "email@test.com"
        registrationVM.password = "123456"
        registrationVM.passwordConfirmation = "123456"
        registrationVM.attemptUserRegistration()   
        DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) {
            XCTAssert(self.registrationVM.loginSuccessful.value, "Login must be successful")
        }
    }

然并软,这个还是不行,失败的原因和我们上面说的一样,所以,我们就使用XCTestException类:

 func testAttemptRegistration() {
        let exp = expectation(description: "Check registration attempt")
        registrationVM.emailAddress = "email@test.com"
        registrationVM.password = "123456"
        registrationVM.passwordConfirmation = "123456"
        registrationVM.attemptUserRegistration()
        DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) {
            XCTAssert(self.registrationVM.loginSuccessful.value, "Login must be successful")
            exp.fulfill()
        }
        waitForExpectations(timeout: 4.0) {
            error in
            if let _ = error {
                XCTAssert(false, "Timeout while attempting a registration")
            }
        }
    }

现在由于单元测试已经覆盖了整个RegistrationViewModel, 我们就可以放心的对这个类进行修改。

重要提示:
如果方法已经改变,但是单元测试没有及时更新,单元测试就没有意义了。

不要把单元测试推迟到最后写,开发的同时就应该开始写了。这样你才能对哪里需要测试和哪里是边际条件有更好的理解。

编写UI测试

当所有的单元测试都搞定了,那么就是开始写一体化测试的时候了,而UI测试是必要的一部分。

开始UI测试前,需准备些UI元素和交互,让我们创建一个视图控制器。

  1. main.storyboard中拉一个viewcontroller,长成下面这样。

邮箱的textfield设tag100, 密码textfield为101,密码确认102.

  1. 新建RegistrationViewController.swift关联到上面的vc,
import UIKit
class RegistrationViewController: UIViewController, UITextFieldDelegate {
    @IBOutlet weak var emailTextField: UITextField!
    @IBOutlet weak var passwordTextField: UITextField!
    @IBOutlet weak var passwordConfirmationTextField: UITextField!
    @IBOutlet weak var registerButton: UIButton! 
    private struct TextFieldTags {
        static let emailTextField = 100
        static let passwordTextField = 101
        static let confirmPasswordTextField = 102
    }
    var viewModel: RegisterationViewModel?   
    override func viewDidLoad() {
        super.viewDidLoad()
        emailTextField.delegate = self
        passwordTextField.delegate = self
        passwordConfirmationTextField.delegate = self
        bindViewModel()
    }
}

将ViewModel中的动态属性‘绑定’到视图控制器中的输入框上,你可以这样写:

    fileprivate func bindViewModel() {
        viewModel?.registrationEnabled.bindAndFire {
            self.registerButton.isEnabled = $0
        }
    }

给输入框写代理:

 func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
        guard let viewModel = viewModel else {
            return true
        }
        let newString = (textField.text! as NSString).replacingCharacters(in: range, with: string)
        switch textField.tag {
        case TextFieldTags.emailTextField: viewModel.emailAddress = newString
        case TextFieldTags.passwordTextField: viewModel.password = newString
        case TextFieldTags.confirmPasswordTextField: viewModel.passwordConfirmation = newString
        default:
            break
        }
        return true
    }

将viewmodel绑定到控制器中:

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        initializeStartingView()
        return true
}
fileprivate func initializeStartingView() {
        if let rootViewController = window?.rootViewController as? RegistrationViewController {
            let networkService = NetworkServiceImpl()
            let viewModel = RegisterationViewModel(networkService: networkService)
            rootViewController.viewModel = viewModel
        }
}

storyboard和RegistrationViewController虽然简单,但是已经足够用来展示自动UI测试是怎么工作的了。

如果一起设置妥当,注册按钮在app启动时,应该是不能用的状态,仅在所有信息都填好的时候。

我们的UI测试是检测在邮箱,密码,确认密码都填好时,注册按钮有没有变成可用,步骤如下:

  1. 打开 TestingIOSUITests.swif,删除testExample() 方法,并添加testRegistrationButtonEnabled()
  2. 把光标放在testRegistrationButtonEnabled()方法中,点击红色的录制测试按钮,(在屏幕下方)
  1. 之后应用将会启动,然后在邮箱输入框中输入邮箱,你会发现,代码自动就出现在方法体中了。

这是个简单的在输入框中输入的指引。

let emailTextField = XCUIApplication().otherElements.containing(.staticText, identifier:"Email Address").children(matching: .textField).element
        emailTextField.tap()
        emailTextField.typeText("email@test.com")
  1. 在交互已经录完后,点击停止按钮。
  2. 现在可以在出现的代码中来进行测试。

录制的指引可能并不是总是好读,可能还让人难于理解。幸运的是,你可以手动的输入UI说明。

手动添加一下UI说明:

  1. 用户点击了密码输入框
  2. 用户输入了密码

在storyboard的输入框中,给ui元素添加识别id(在属性检查器的accessibility下面的identifier),密码的识别id是passwordTextField
所以他的指引可以这么写:

let passwordTextField = XCUIApplication().secureTextFields["passwordTextField"]
        passwordTextField.tap()
        passwordTextField.typeText("password")

还有一个用户输入确认密码的ui交互没有写,一样:

let confirmPasswordTextField = XCUIApplication().secureTextFields["Confirm Password"]
        confirmPasswordTextField.tap()
        confirmPasswordTextField.typeText("password")

之后就是写断言了,和单元测试中一样,检查注册按的isEnabled是否改变:

let registerButton = XCUIApplication().buttons["REGISTER"]
XCTAssert(registerButton.isEnabled == true, "Registration button should be enabled")

这个就像这样:

func testRegistrationButtonEnabled() {
        // Recorded by Xcode
        let emailTextField = XCUIApplication().otherElements.containing(.staticText, identifier:"Email Address").children(matching: .textField).element
        emailTextField.tap()
        emailTextField.typeText("email@test.com")
        // Queried by accessibility identifier
        let passwordTextField = XCUIApplication().secureTextFields["passwordTextField"]
        passwordTextField.tap()
        passwordTextField.typeText("password")
        // Queried by placeholder text
        let confirmPasswordTextField = XCUIApplication().secureTextFields["Confirm Password"]
        confirmPasswordTextField.tap()
        confirmPasswordTextField.typeText("password")
        let registerButton = XCUIApplication().buttons["REGISTER"]
        XCTAssert(registerButton.isEnabled == true, "Registration button should be enabled")
    }

运行测试,看断言是否起作用。

为了改进测试,可以测试注册按钮的isEnabled是否改为false:

func testRegistrationButtonEnabled() {
        let registerButton = XCUIApplication().buttons["REGISTER"]
        XCTAssert(registerButton.isEnabled == false, "Registration button should be disabled")
        // Recorded by Xcode
        let emailTextField = XCUIApplication().otherElements.containing(.staticText, identifier:"Email Address").children(matching: .textField).element
        emailTextField.tap()
        emailTextField.typeText("email@test.com")
        XCTAssert(registerButton.isEnabled == false, "Registration button should be disabled")
        // Queried by accessibility identifier
        let passwordTextField = XCUIApplication().secureTextFields["passwordTextField"]
        passwordTextField.tap()
        passwordTextField.typeText("password")
        XCTAssert(registerButton.isEnabled == false, "Registration button should be disabled")
        // Queried by placeholder text
        let confirmPasswordTextField = XCUIApplication().secureTextFields["Confirm Password"]
        confirmPasswordTextField.tap()
        confirmPasswordTextField.typeText("pass")
        XCTAssert(registerButton.isEnabled == false, "Registration button should be disabled")
        confirmPasswordTextField.typeText("word") // the whole confirm password word will now be "password"
        XCTAssert(registerButton.isEnabled == true, "Registration button should be enabled")
    }

通过写良好的测试称为一个更好开发者

从我的经验看,尝试些测试会让你成为一个更好的开发者。你会尝试更好的组织你的代码。

有条理的,模块化的代码是一个成功的,抗压的测试的前提。

当想到应用的结构时,你会发现,使用MVVM,MVP,VIPER或者其他的模式,你的代码会更加健壮,易于测试。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,099评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,473评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,229评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,570评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,427评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,335评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,737评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,392评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,693评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,730评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,512评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,349评论 3 314
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,750评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,017评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,290评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,706评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,904评论 2 335

推荐阅读更多精彩内容