作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
Ciprian Balea

Ciprian Balea

高级QA自动化工程师

Ciprian是一名经过认证的scrum管理员,在以各种语言建立和开发CI基础架构和测试自动化框架方面经验丰富.

Expertise

Share

作为一名测试人员,你可以做的最重要的事情之一就是让你正在测试的应用程序自动化,从而提高你的工作效率和速度. 完全依赖手动测试是不可行的,因为您需要每天运行全套测试, 有时一天好几次, 测试推送到应用代码中的每一个更改.

This article will describe our team’s journey to identifying 谷歌的EarlGrey 1.0 as the tool that worked best for us in the context of automating the iOS Toptal Talent app. 事实上,我们正在使用EarlGrey并不意味着它是每个人最好的测试工具——它只是碰巧适合我们的需要.

为什么我们要过渡到EarlGrey

Over the years, our team has built different mobile apps on both iOS and Android. In the beginning, 我们考虑使用跨平台UI测试工具,这样我们就可以编写一组测试并在不同的移动操作系统上执行. 首先,我们用 Appium, the most popular open-source option available.

但随着时间的流逝, Appium limitations 变得越来越明显. 在我们的案例中,Appium的两个主要缺点是:

  • The framework’s questionable stability caused many test flakes.
  • The comparatively slow update process hampered our work.

以减轻第一个Appium的缺点, we wrote all sorts of code tweaks and hacks to make the tests more stable. However, there was nothing we could do to address the second. 每次iOS或Android发布新版本时,Appium都要花很长时间才能赶上. And very often, because of having many bugs, the initial update was unusable. As a result, 我们经常被迫在较旧的平台版本上继续执行我们的测试,或者完全关闭它们,直到可用的Appium更新.

这种做法远非理想, 因为这些问题, along with additional ones that we won’t cover in detail, 我们决定寻找替代方案. 一个新的测试工具的最高标准是 增加了稳定性 and faster updates. After some investigation, we decided to use native testing tools for each platform.

所以,我们过渡到 Espresso 用于Android项目和EarlGrey 1.0 for iOS development. In hindsight, we can now say that this was a good decision. The time “lost” due to the need to write and maintain two different sets of tests, 每个平台一个, 不需要调查这么多不可靠的测试,也不需要在版本更新上有任何停机时间,这不仅仅是弥补吗.

本地项目结构

你需要将框架包含在与你正在开发的应用程序相同的Xcode项目中. So we created a folder in the root directory to host the UI tests. Creating the EarlGrey.swift 文件在安装测试框架时是必需的,其内容是预定义的.

Toptal人才应用程序:本地项目结构

EarlGreyBase 父类是所有测试类的吗. 它包含了 setUp and tearDown 方法,从 XCTestCase. In setUp, 我们加载了大多数测试通常会使用的存根(稍后会详细介绍存根),我们还设置了一些配置标志,我们注意到这些标志可以提高测试的稳定性:

//关闭EarlGrey的网络请求跟踪,因为我们不使用它,它会阻塞测试执行

GREYConfiguration.sharedInstance().setValue([".*"], forConfigKey: kGREYConfigKeyURLBlacklistRegex)
GREYConfiguration.sharedInstance().setValue(false, forConfigKey: kGREYConfigKeyAnalyticsEnabled)

我们使用Page Object设计模式——应用程序中的每个屏幕都有一个对应的类,其中定义了所有UI元素及其可能的交互. 这个类被称为“页面”.测试方法是根据存在于单独的文件和类中的特性进行分组的.

To give you a better idea of how everything is displayed, 这就是我们的应用程序中的登录和忘记密码屏幕的样子,以及它们是如何由页面对象表示的.

This is the appearance of Login and Forgot Password screens in our app.

Later in the article, we will present the code contents of the Login page object.

自定义实用程序方法

The way EarlGrey synchronizes the test actions with the app is not always perfect. For example, it might try to click on a button that is not yet loaded in the UI hierarchy, 导致测试失败. 为了避免这个问题, 我们创建了自定义方法,以便在与元素交互之前等待元素以所需的状态出现.

下面是一些例子:

static func asyncWaitForVisibility(on element: GREYInteraction) {
     // By default, EarlGrey blocks test execution while
     // the app is animating or doing anything in the background.           
     //http://github.com/google/EarlGrey/blob/master/docs/api.md#synchronization
     GREYConfiguration.sharedInstance().setValue(false, forConfigKey: kGREYConfigKeySynchronizationEnabled)
     element.断言(grey_sufficientlyVisible ())
     GREYConfiguration.sharedInstance().setValue(true, forConfigKey: kGREYConfigKeySynchronizationEnabled)
}


等待元素:GREYInteraction, timeout: Double = 15.0) -> Bool {
        GREYCondition(name: "Wait for element to appear", block: {
            var error: NSError?
            element.断言(grey_notNil()错误: &error)
            返回错误== nil
        }).wait(withTimeout: timeout, pollInterval: 0).5)
        if !elementVisible(元素){
            XCTFail(“元素没有出现”)
        }
        return true
}

EarlGrey本身没有做的另一件事是滚动屏幕,直到所需的元素变得可见. 我们可以这样做:

static func elementVisible(_ element: GREYInteraction) -> Bool {
	var error: NSError?
	element.断言(grey_notVisible()错误: &error)
	if error != nil {
		return true
	} else {
		return false
	}
}

static func scrollUntilElementVisible(_ scrollDirection: GREYDirection, _ speed: String, _ searchedElement: GREYInteraction, _ actionElement: GREYInteraction) -> Bool {
        var swipes = 0
        while !elementVisible (searchedElement) && swipes < 10 {
            如果speed == "slow" { 	
            actionElement.perform(grey_swipeSlowInDirection(scrollDirection))
            } else {             
            actionElement.perform(grey_swipeFastInDirection(scrollDirection))
            }
            swipes += 1
        }
        if swipes >= 10 {
            return false
        } else {
            return true
        }
}

我们发现EarlGrey的API中缺少的其他实用程序方法是计数元素和读取文本值. The code for these utilities is available on GitHub: here and here.

Stubbing API Calls

为确保避免后端服务器问题导致的错误测试结果,我们使用 OHHTTPStubs library 模拟服务器调用. The documentation on their homepage is pretty straightforward, but we will present how we stub responses in our app, 它使用GraphQL API.

类StubsHelper {
	static let testURL = URL(string: "http://[our backend server]")!
	static func setupOHTTPStub(for request: StubbedRequest, delayed: Bool = false) {
		存根(条件:isHost (testURL.host!) && hasJsonBody(请求.bodyDict()) {_ in
			let fix = appFixture(forRequest: request)
			if delayed {
				return fix.requestTime(0.1, responseTime: 7.0)
			} else {
				return fix
			}
		}
	}
	让stubbedEmail = "fixture@email.com"
	让stubbedPassword = "password"
	enum stubberequest {
		case login
		func bodyDict() -> [String: Any] {
			switch self {
				case .login:
					返回EmailPasswordSignInMutation (
						email: stubbedTalentLogin, password: stubbedTalentPassword
						).makeBodyIdentifier ()
			}
		}
		func statusCode() -> Int32 {
			return 200
		}
		func jsonFileName() -> String {
			让文件名:字符串
			switch self {
				case .login:
					fileName = "login"
			}
			返回“\(文件名).json"
		}
	}
	私有扩展graphqoperation {
		func makeBodyIdentifier () -> [String: Any] {
			let body: GraphQLMap = [
				“查询”:queryDocument,
				“变量”:变量,
				“operationName”:operationName
			]
        // Normalize values like enums here, otherwise body comparison will fail
        让normalizedBody = body.jsonValue as? [String: Any] else {
        	fatalError()
        }
        返回normalizedBody
    }
}

加载存根是通过调用 setupOHTTPStub method:

StubsHelper.setupOHTTPStub (: .login)

把所有东西放在一起

本节将演示如何使用上面描述的所有原则来编写实际的端到端登录测试.

import EarlGrey

最终类LoginPage {

    func login() -> HomePage {
        fillLoginForm()
        loginButton().执行(grey_tap ())
        return HomePage()
    }

    函数fillLoginForm() {  
	ElementsHelper.waitElementVisibility (emailField ()) 
    	emailField().执行(grey_replaceText (StubsHelper.stubbedTalentLogin))
        passwordField().执行(grey_tap ())
        passwordField().执行(grey_replaceText (StubsHelper.stubbedTalentPassword))
    }

    函数clearAllInputs() {
        if ElementsHelper.elementVisible (passwordField ()) {
            passwordField().执行(grey_tap ())
            passwordField().执行(grey_replaceText (" "))
        }
        emailField().执行(grey_tap ())
        emailField().执行(grey_replaceText (" "))
    }
}

私有扩展LoginPage {
    func emailField(file: StaticString = #file, line: UInt = #line) -> GREYInteraction {
        return EarlGrey.selectElement(with: grey_accessibilityLabel("Email"), File:文件,line:行)
    }

    func passwordField(file: StaticString = #file, line: UInt = #line) -> GREYInteraction {
        return EarlGrey.selectElement(
            with: grey_allOf([
                    grey_accessibilityLabel(“密码”),
                    grey_sufficientlyVisible (),
                    grey_userInteractionEnabled ()
                ]),
            File:文件,line:行
        )
    }

    func loginButton(file: StaticString = #file, line: UInt = #line) -> GREYInteraction {
        return EarlGrey.selectElement(with: grey_accessibilityID("login_button"), File:文件,line:行)
    }
}


类BBucketTests: EarlGreyBase {
    func testLogin() {
        StubsHelper.setupOHTTPStub (: .login)
        LoginPage().clearAllInputs()
        let主页= LoginPage().login()
        GREYAssertTrue(
            homePage.assertVisible(),
            reason: "Home screen not displayed after successful login"
        )
    }
}

在CI中运行测试

We use Jenkins as our continuous integration system, and we run the UI tests for each commit in every pull request.

We use fastlane scan 在CI中执行测试并生成报告. It’s useful to have screenshots attached to these reports for failed tests. Unfortunately, scan doesn’t provide this functionality, so we had to custom-make it.

In the tearDown() 函数,我们检测测试是否失败,并保存iOS模拟器的屏幕截图.

import EarlGrey
import XCTest
进口UIScreenCapture

重载函数tearDown() {
        if testRun!.failureCount > 0 {
            // name是XCTest实例的属性
            / / http://developer.apple.com/documentation/xctest/xctest/1500990-name
            takeScreenshotAndSave(名称):
        }
        super.tearDown()
}

func takeScreenshotAndSave(as testCaseName: String) {
        let imageData = UIScreenCapture.takeSnapshotGetJPEG ()
        let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask,真的)
        let filePath = "\(paths[0])/\(testCaseName) ".jpg"

        do {
            try imageData?.write(to: URL.init (fileURLWithPath: filePath))
        } catch {
            截图未写.")
        }
}

The screenshots are saved in the Simulator folder, 您需要从那里获取它们,以便将它们附加为构建工件. We use Rake 来管理我们的CI脚本. 这就是我们收集测试工件的方式:

def gather_test_artifacts(booted_sim_id, destination_folder)
  App_container_on_sim = ' xrun simctl get_app_container #{booted_sim_id}[你的bundle id] data '.strip
  FileUtils.cp_r "#{app_container_on_sim}/Documents", destination_folder
end

Key Takeaways

If you are looking for a fast and reliable way to automate your iOS 测试,去找格雷伯爵吧. It is developed and maintained by Google (need I say more?), and in many respects, it is superior to other tools available today.

您将需要对框架进行一些修改,以准备实用方法来提高测试稳定性. To do this, you can start with our examples of custom utility methods.

我们建议对存根数据进行测试,以确保您的测试不会因为后端服务器没有您期望它拥有的所有测试数据而失败. Use OHHTTPStubs or a similar local web server to get the job done.

在CI中运行测试时, make sure to provide screenshots for the failed cases to make debugging easier.

You may be wondering why we did not migrate to EarlGrey 2.还没有,这里有一个简短的解释. The new version was released last year and it promises some enhancements over v1.0. 不幸的是,当我们采用EarlGrey时,v2.0不是特别稳定. 所以我们没有变换到v2.0 just yet. However, 我们的团队急切地等待着新版本的bug修复,这样我们就可以在未来迁移我们的基础设施.

Online Resources

EarlGrey的入门指南 GitHub homepage is the 如果您正在为您的项目考虑测试框架,您希望从哪里开始. There, you will find an easy-to-use installation guide, 该工具的API文档, 还有一个方便的备忘单,以一种在编写测试时直接使用的方式列出了框架的所有方法.

有关为iOS编写自动化测试的更多信息,您也可以查看 我们之前的博客文章.

了解基本知识

  • UI测试是功能测试吗?

    Functional testing is the process of verifying the functionalities of a system. Thus, UI testing is functional testing through the user interface.

  • 为什么UI测试很重要?

    UI测试验证应用程序的多个层是否应该一起工作. 低级别测试, 例如单元测试或API测试, cannot cast such a wide net to look for bugs as UI testing can.

  • What test automation tools can be used for UI test automation?

    根据需要测试的系统的性质,您可以使用特定的工具. 一些著名的UI自动化测试工具是Selenium、Appium、Ranorex或AutoIt.

聘请Toptal这方面的专家.
Hire Now

世界级的文章,每周发一次.

输入您的电子邮件,即表示您同意我们的 privacy policy.

世界级的文章,每周发一次.

输入您的电子邮件,即表示您同意我们的 privacy policy.

Toptal Developers

Join the Toptal® community.