美文网首页iOS
如何编写iOS的自动测试

如何编写iOS的自动测试

作者: chdo002 | 来源:发表于2017-03-18 22:13 被阅读39次
    原文地址

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

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

    自动测试,包括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或者其他的模式,你的代码会更加健壮,易于测试。

    相关文章

      网友评论

        本文标题:如何编写iOS的自动测试

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