一抹厄尔尼诺的灰色--Toptal人才应用的UI测试

128 阅读10分钟

技术

8分钟阅读

厄尔尼诺的一抹亮色--Toptal人才应用的UI测试

Ciprian Balea

Ciprian是一位经过认证的Scrum Master,在建立和发展CI基础设施和各种语言的测试自动化框架方面经验丰富。

SHARE

作为一个测试人员,你可以做的最重要的事情之一是使你的工作更加有效和快速,就是将你正在测试的应用程序自动化。仅仅依靠手动测试是不可行的,因为你需要每天运行全套的测试,有时一天要运行多次,测试推送到应用程序代码的每一个变化。

这篇文章将描述我们的团队在iOSToptal人才应用自动化的背景下,将谷歌的EarlGrey 1.0确定为最适合我们的工具的过程。我们使用EarlGrey的事实并不意味着EarlGrey是最适合所有人的测试工具--它只是恰好适合我们的需求。

为什么我们过渡到EarlGrey?

多年来,我们的团队在iOS和Android上都建立了不同的移动应用程序。一开始,我们考虑使用一个跨平台的UI测试工具,使我们能够编写一套测试并在不同的移动操作系统上执行。首先,我们选择了Appium,这是目前最流行的开源选项。

但随着时间的推移,Appium的限制变得越来越明显。在我们的案例中,Appium的两个主要缺点是。

  • 该框架的稳定性有问题,导致许多测试失败。
  • 相对较慢的更新过程阻碍了我们的工作。

为了缓解Appium的第一个缺点,我们写了各种代码调整和黑客攻击,使测试更加稳定。然而,对于第二个问题,我们却无能为力。每次iOS或Android的新版本发布,Appium都要花很长时间才能跟上。而且很多时候,由于有很多错误,最初的更新是无法使用的。因此,我们经常被迫在旧的平台版本上继续执行我们的测试,或者完全关闭测试,直到Appium的更新可用。

这种方法远非理想,由于这些问题,以及我们不会详细介绍的其他问题,我们决定寻找替代方案。一个新的测试工具的首要标准是提高稳定性更快的更新。经过一些调查,我们决定为每个平台使用本地测试工具。

因此,我们为Android项目过渡到Espresso,为iOS开发过渡到EarlGrey 1.0。事后看来,我们现在可以说这是一个好的决定。由于需要编写和维护两套不同的测试,每个平台各一套,所 "损失 "的时间被弥补了,因为我们不需要调查那么多不稳定的测试,也不需要在版本更新时有任何停机。

本地项目结构

你需要将框架包含在你正在开发的应用程序的同一个Xcode项目中。所以我们在根目录下创建了一个文件夹来承载UI测试。在安装测试框架时,创建EarlGrey.swift 文件是必须的,其内容是预定义的。

Toptal Talent App: Local Project Structure

EarlGreyBase 是所有测试类的父类。它包含一般的 和 方法,从 扩展而来。在 中,我们加载了大部分测试通常使用的存根(后面会有更多关于存根的内容),我们还设置了一些配置标志,我们注意到这些标志可以增加测试的稳定性。setUp tearDown XCTestCase setUp

// Turn off EarlGrey's network requests tracking since we don't use it and it can block tests execution

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

我们使用页面对象设计模式--应用程序中的每个屏幕都有一个相应的类,所有的UI元素和它们可能的交互都被定义。这个类被称为 "页面"。测试方法按驻留在单独的文件和类中的功能进行分组。

为了让你更好地了解一切是如何显示的,这是我们应用程序中的登录和忘记密码屏幕的样子,以及它们是如何由页面对象表示的。

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

在文章的后面,我们将介绍登录页面对象的代码内容。

自定义实用方法

EarlGrey将测试动作与应用程序同步的方式并不总是完美的。例如,它可能会尝试点击UI层次结构中尚未加载的按钮,导致测试失败。为了避免这个问题,我们创建了自定义的方法来等待元素出现在所需的状态,然后再与它们进行交互。

这里有几个例子。

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


static func waitElementVisibility(for element: GREYInteraction, timeout: Double = 15.0) -> Bool {
        GREYCondition(name: "Wait for element to appear", block: {
            var error: NSError?
            element.assert(grey_notNil(), error: &error)
            return error == nil
        }).wait(withTimeout: timeout, pollInterval: 0.5)
        if !elementVisible(element) {
            XCTFail("Element didn't appear")
        }
        return true
}

EarlGrey自己不做的另一件事是滚动屏幕,直到所需元素变得可见。下面是我们如何做到这一点的。

static func elementVisible(_ element: GREYInteraction) -> Bool {
	var error: NSError?
	element.assert(grey_notVisible(), error: &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 {
            if 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中缺少的其他实用方法是计算元素和读取文本值。这些实用程序的代码可以在GitHub上找到:这里这里

存根的API调用

为了确保我们避免后端服务器问题导致的错误测试结果,我们使用OHHTTPStubs库来模拟服务器调用。他们主页上的文档非常简单明了,但我们将介绍如何在我们的应用程序中存根响应,其中使用GraphQL API。

class StubsHelper {
	static let testURL = URL(string: "https://[our backend server]")!
	static func setupOHTTPStub(for request: StubbedRequest, delayed: Bool = false) {
		stub(condition: isHost(testURL.host!) && hasJsonBody(request.bodyDict())) { _ in
			let fix = appFixture(forRequest: request)
			if delayed {
				return fix.requestTime(0.1, responseTime: 7.0)
			} else {
				return fix
			}
		}
	}
	static let stubbedEmail = "fixture@email.com"
	static let stubbedPassword = "password"
	enum StubbedRequest {
		case login
		func bodyDict() -> [String: Any] {
			switch self {
				case .login:
					return EmailPasswordSignInMutation(
						email: stubbedTalentLogin, password: stubbedTalentPassword
						).makeBodyIdentifier()
			}
		}
		func statusCode() -> Int32 {
			return 200
		}
		func jsonFileName() -> String {
			let fileName: String
			switch self {
				case .login:
					fileName = "login"
			}
			return "\(fileName).json"
		}
	}
	private extension GraphQLOperation {
		func makeBodyIdentifier() -> [String: Any] {
			let body: GraphQLMap = [
				"query": queryDocument,
				"variables": variables,
				"operationName": operationName
			]
        // Normalize values like enums here, otherwise body comparison will fail
        guard let normalizedBody = body.jsonValue as? [String: Any] else {
        	fatalError()
        }
        return normalizedBody
    }
}

加载存根是通过调用setupOHTTPStub 方法进行的。

StubsHelper.setupOHTTPStub(for: .login)

把所有东西放在一起

本节将展示我们如何使用上面描述的所有原则来编写一个实际的端到端登录测试。

import EarlGrey

final class LoginPage {

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

    func fillLoginForm() {  
	ElementsHelper.waitElementVisibility(emailField()) 
    	emailField().perform(grey_replaceText(StubsHelper.stubbedTalentLogin))
        passwordField().perform(grey_tap())
        passwordField().perform(grey_replaceText(StubsHelper.stubbedTalentPassword))
    }

    func clearAllInputs() {
        if ElementsHelper.elementVisible(passwordField()) {
            passwordField().perform(grey_tap())
            passwordField().perform(grey_replaceText(""))
        }
        emailField().perform(grey_tap())
        emailField().perform(grey_replaceText(""))
    }
}

private extension LoginPage {
    func emailField(file: StaticString = #file, line: UInt = #line) -> GREYInteraction {
        return EarlGrey.selectElement(with: grey_accessibilityLabel("Email"), file: file, line: line)
    }

    func passwordField(file: StaticString = #file, line: UInt = #line) -> GREYInteraction {
        return EarlGrey.selectElement(
            with: grey_allOf([
                    grey_accessibilityLabel("Password"),
                    grey_sufficientlyVisible(),
                    grey_userInteractionEnabled()
                ]),
            file: file, line: line
        )
    }

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


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

在CI中运行测试

我们使用Jenkins作为我们的持续集成系统,我们为每个拉动请求中的每个提交运行UI测试。

我们使用 fastlane scan来执行CI中的测试并生成报告。对于失败的测试,在这些报告中附上屏幕截图是非常有用的。不幸的是,scan 并没有提供这个功能,所以我们不得不定制它。

tearDown() 功能中,我们检测测试是否失败,如果失败,则保存iOS模拟器的屏幕截图。

import EarlGrey
import XCTest
import UIScreenCapture

override func tearDown() {
        if testRun!.failureCount > 0 {
            // name is a property of the XCTest instance
            // https://developer.apple.com/documentation/xctest/xctest/1500990-name
            takeScreenshotAndSave(as: name)
        }
        super.tearDown()
}

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

        do {
            try imageData?.write(to: URL.init(fileURLWithPath: filePath))
        } catch {
            XCTFail("Screenshot not written.")
        }
}

这些截图保存在模拟器文件夹中,你需要从那里获取它们,以便将它们作为构建工件附在上面。我们使用Rake 来管理我们的CI脚本。这就是我们收集测试工件的方式。

def gather_test_artifacts(booted_sim_id, destination_folder)
  app_container_on_sim = `xcrun simctl get_app_container #{booted_sim_id} [your bundle id] data`.strip
  FileUtils.cp_r "#{app_container_on_sim}/Documents", destination_folder
end

主要收获

如果你正在寻找一种快速而可靠的方式来自动化你的iOS测试,那就去找EarlGrey吧。它是由谷歌开发和维护的(还需要我说更多吗?),在许多方面,它比现在的其他工具要好。

你需要对该框架进行一些修补,以准备实用的方法来促进测试的稳定性。要做到这一点,你可以从我们的自定义实用方法的例子开始。

我们建议在存根数据上进行测试,确保你的测试不会因为后端服务器没有你期望的所有测试数据而失败。使用OHHTTPStubs 或类似的本地网络服务器来完成这项工作。

当在CI中运行你的测试时,确保为失败的案例提供屏幕截图,使调试更容易。

你可能想知道为什么我们还没有迁移到EarlGrey 2.0,这里有一个简单的解释。新版本是去年发布的,它承诺比V1.0有一些增强。不幸的是,当我们采用EarlGrey时,v2.0并不是特别稳定。因此我们还没有过渡到v2.0。然而,我们的团队正急切地等待着新版本的错误修复,这样我们就可以在未来迁移我们的基础设施。

在线资源

如果你正在考虑为你的项目使用测试框架,GitHub主页上的EarlGrey入门指南你想要开始的地方。在那里,你会发现一个易于使用的安装指南,该工具的API文档,以及一个方便的小抄,列出了该框架的所有方法,在编写测试时可以直接使用。

关于为iOS编写自动化测试的其他信息,你也可以查看我们之前的一篇博文

了解基础知识

UI测试是功能测试吗?

功能测试是验证一个系统的功能的过程。因此,UI测试是通过用户界面的功能测试。

为什么UI测试很重要?

UI测试验证了应用程序的多个层次是否能按照它们的要求一起工作。较低层次的测试,如单元测试或API测试,不能像UI测试那样广泛地寻找错误。

哪些测试自动化工具可用于UI测试自动化?

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

标签

谷歌EarlGreyEarlGreyEarlGreyUI

自由职业者? 寻找你的下一份工作。

iOS开发工作

冯晓明(Ciprian Balea

高级QA自动化工程师

关于作者

Ciprian是一名经过认证的Scrum Master,在建立和发展CI基础设施和各种语言的测试自动化框架方面经验丰富。

评论

请启用JavaScript以查看由Disqus提供的评论。评论由Disqus提供

世界级的文章,每周交付。

获得优秀的内容

订阅意味着同意我们的隐私政策

谢谢您!
请查看您的收件箱以确认您的邀请。

热门文章

工程图示Chevron后端

使用Express.js路由进行基于承诺的错误处理

工程图示 ChevronWeb前端

企业应用的最佳React状态管理工具

工程图示 ChevronBack-end

使用AWS SSM进行SSH日志和会话管理

工程图ChevronTechnology

明尼苏达大学的Linux禁令引发了对开源的质疑

查看我们的相关人才

觉效果

自由职业者? 寻找你的下一份工作。

iOS开发工作

关于作者

Ciprian Balea

高级QA自动化工程师

阅读 下一页

工程图示Chevron后端

使用Express.js路由进行基于承诺的错误处理