今天 QQ 上一位群友询问我怎么做关键字驱动的框架。鉴于我最近这 1 年左右的时间都是在公司的一个自动化测试框架项目,也还没分享过这方面的思路和感受,所以在这里分享一下。
工作经验有限,有些地方说的可能并不对,大家找到有问题的地方欢迎提出,我会立即更正。

现在有许多的自动化测试框架可以使用,如 appium,xUnit,Cucumber 等,但很多时候单纯使用其中一个框架并不是十分好用,而且很多的框架名词,如 BDD,关键字驱动等也会让一些想接触这方面的人感到有点 Hold 不住。其实一个完整、实用的框架是有规律可循的,而且也并不是特别困难。

下面的大部分思路来自于 芈峮 大神的《iOS 测试指南》中 8.6 自动化测试框架剖析 。看完这里后我觉得我终于把心中的混乱理清了。因此我也在此介绍给大家,理清一些混乱的地方。

测试框架分层

一个完整的测试框架是怎样的?

例如我们写 appium 的测试用例,如果只是做 Demo 的话我们会直接使用 WebDriver API 来写。这时候一个用例长这样:

import os
from time import sleep
from appium import webdriver

if __name__ == '__main__':
    # Returns abs path relative to this file and not cwd
    PATH = lambda p: os.path.abspath(
        os.path.join(os.path.dirname(__file__), p)
    )

    # init driver, open session
    desired_caps = {}
    desired_caps['platformName'] = 'Android'
    desired_caps['platformVersion'] = '4.2'
    desired_caps['deviceName'] = 'Android Emulator'
    desired_caps['app'] = PATH(
        '../../../sample-code/apps/ApiDemos/bin/ApiDemos-debug.apk'
    )

    driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps)

    # execute some action
    els = driver.find_elements_by_android_uiautomator("new UiSelector().clickable(true)")

    # check if result is as expected
    if len(els) != 12:
        print "Case Failed. There should be 12 elements but only {} elements exist".format(len(els))
    else:
        print "Case Passed."
     # end the session
    driver.quit()

然后用例数目比较多了,每个 case 里面都要启动 appium 太麻烦了,而且测试结果也不够好看,所以我们开始使用一些单元测试框架,使用它们的 setUp,tearDown 或者测试报告。此时用例会长这样:

import os
from time import sleep

import unittest

from appium import webdriver

# Returns abs path relative to this file and not cwd
PATH = lambda p: os.path.abspath(
    os.path.join(os.path.dirname(__file__), p)
)

class SimpleAndroidTests(unittest.TestCase):
    def setUp(self):
        desired_caps = {}
        desired_caps['platformName'] = 'Android'
        desired_caps['platformVersion'] = '4.2'
        desired_caps['deviceName'] = 'Android Emulator'
        desired_caps['app'] = PATH(
            '../../../sample-code/apps/ApiDemos/bin/ApiDemos-debug.apk'
        )

        self.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps)

    def tearDown(self):
        # end the session
        self.driver.quit()

    def test_check_clickable_element_count(self):

        els = self.driver.find_elements_by_android_uiautomator("new UiSelector().clickable(true)")
        self.assertEqual(12, len(els))

if __name__ == '__main__':
    suite = unittest.TestLoader().loadTestsFromTestCase(SimpleAndroidTests)
    unittest.TextTestRunner(verbosity=2).run(suite)

再后面我们要写更多的用例,单靠做自动化测试的几个人 hold 不住了,需要让一些技术方面不那么强的人来做。为了让他们容易上手而又不至于搞乱我们的框架,我们会把用例再封装,做成表格或者 BDD 这种不会代码的人也能比较容易写用例和读用例的形式。此时用例会长这样 (Cucumber):

Feature: Simple android test
  Do some sample operations with android application

  Scenario: Check clickable element count
    When I opened the application
    Then 12 clickable buttons should occur

这三步其实对应着测试框架的三个层级:工具层(如 appium),核心层(如单元测试框架),适配层(如 BDD 框架)

工具层

工具层主要负责相应侧面的测试执行的动作触发。(书上原文)

首先,我们要想通过程序控制一些被测程序,那么就必须有相应的工具。这些工具让原本难以做到、甚至无法做到的控制方式变成了可能,同时也让它们更容易被做到。例如 iOS 的 UIAutomation 。如果没有它的存在,我们就必须在应用里加入一些 agent(如 MonkeyTalk ),或者直接在应用里添加测试代码(如 KIF ),甚至自己做一套能通过接口控制程序的测试工具。这些工具能让我们节省很多的时间,并且更简单地做到测试部分和功能部分的分离。

工具层的框架很多,移动测试的有 Appium,robotium,espresso 等,web 测试基本是 selenium 的天下。

核心层

核心层负责测试执行的驱动和结果监控并且反馈。(书上原文)

自动化测试程序其实都有一些共通之处,例如它们都需要丰富的断言功能支持,否则就只能去写各种 If..else..,然后校验和执行步骤就耦合到一起了。核心层的框架做的就是这个事情,把自动化测试程序这种程序的最共通之处抽取出来(例如需要有专门的异常对应 fail,需要有各种灵活简便的断言,需要有创造前置条件的专用函数),然后以一种较为固定的写法把它们封装起来。

例如大部分用例都会有前置条件(如 web 的用例会要求浏览器必须处于打开状态),而且会高度重复,并具有 block 的特性(浏览器没打开,后面的步骤就不用执行了)。那么核心层的框架就会提供一个特殊的方法(一般方法名为 setUp),让这个方法在执行用例的内容前执行以创造合适的前置条件,并在这个方法出错或 fail 时自动把整个用例标记为 fail 或 block 。

核心层的框架也不少,例如各种单元测试框架(JUnit,OCUnit 等),Testng 等。一般来说由于工具层使用的框架选定后所使用的语言也就确定了,所以一般会先确定工具层,然后再选择核心层使用的框架。如果语言不一致则需要适当进行二次开发来方便调用。当然,像 appium/selenium 这种支持多个语言的工具层框架能更灵活地选择核心层框架,这也是它们受欢迎的重要原因。

适配层

适配层负责重复使用的测试方法封装适配。(书上原文)

当用例达到一定的数量级(数十或者过百)时,如果没有很好地把重复部分提取出来或者以更好的形式编写,将会出现一定数量的冗余代码。此时适配层的作用就是把这些重复的方法进行更好的封装,让写用例的人能更快速地编写/阅读用例。

我们平时听到的 数据驱动、关键字驱动、行为驱动(BDD)大多属于这一层。因为无论使用什么驱动,它都和具体的测试程序/测试领域无关。你不会说 BDD 只能拿来测 Android 应用,也不会说 数据驱动 只能拿 Java 来写。这些 xx 驱动 主要是一种思想,一种实践中好用、实用、且有不少工具可供选择的用例编写思想。当然由于具体使用的工具不同,所使用的语言也会有一定的限制,但思想是相通的。

适配层也有各种框架,如 BDD 的 Cucumber ,关键字驱动的 robot framework(同时它也带有核心层和工具层,是一个完整的且实用的测试框架,它的工具层以扩展库的形式提供,十分易于扩展以适应不同软件领域,很值得借鉴)。由于适配层的编写方式很可能已经不属于某种具体的编程语言了(如 关键字驱动 使用表格的形式,脱离了语言),因此适配层的框架大多都会有对应的核心层/工具层框架提供一体化的支持。

一个简单的例子

robot framework 相信不少人有用过。它是一个完整的带有这三层结构的测试框架:

适配层:用例使用 tsv 格式编写,采用的主要是关键字驱动的形式。
核心层:提供了 setUp,tearDown 方法,并有合适并足够的断言功能和异常捕获功能。
工具层:具有众多的 Library 可供选择,可以结合不同的工具测试各种领域的软件。

另一个简单的例子

另一个例子是一种关键字驱动框架的实现形式:

适配层:以表格形式展现的用例,以及把用例转换成可执行代码的转换器:

表格:

action params
openBrowser browserName="Chrome"

转换后代码:

class TestCase(ActionBase, unittest.TestCase):

    # a test case
    def open_browser(self):
        # step of test case
        self.action_call("openBrowser", {"browserName":"Chrome"})

然后这个 action_call 的实现里调用对应的 action 方法来执行实际动作:

class ActionBase:
    ...
    def action_call(action_name, params):

        # get action function in this class
        action_fun = getattr(self, action_name)

        # execute action
        action_fun(**kwargs)

    ...
    def openBrowser(browserName=None):
        if browserName == 'Chrome':
            self.driver = webdriver.Chrome()
        ...

核心层:转换后代码使用了 unittest 这个框架来管理用例执行

工具层:实际执行 action 时使用了 selenium 框架

总结

"Unix 哲学"的根本原则: 尽量用简单的方法解决问题(出自 关于 Unix 哲学)其实也适用于自动化框架的设计。每个框架专心做好自己要做的事情,然后通过不同框架的组合就能做出灵活适应各种项目的完整的自动化测试框架。

再好的框架也只是工具。一个好的测试项目不仅仅需要合适的框架,更需要好的用例设计、执行策略等非技术因素。因此不要只追求好的测试框架,而忽略了其他。


↙↙↙阅读原文可查看相关链接,并与作者交流