刚开始工作的两年,在做 API 型服务端的自动化测试。当时老大安利了 robotframework,一直在用。由于可以编写自定义的 library,robot 的扩展能力非常强,基本上 python 能做的事情,都可以在 robot 里面实现。通过编写 library,我实现了很多好玩的功能。例如,检测数据库,mc,日志,和运行时更换 mock 服务。

去年想尝试新事物,便换了项目。新项目的自动化从零开始,我也不想继续再用 robot。不用 robot,用什么呢?当时我也没有答案。不过当时我在做一件事情,就是把新项目的接口调用,都封装到一个 python 写的库里面。既然像模像样地写一个库,自然要有逼格,src,test,doc 这些目录都要有,不然会被程序员 BS 。写了好几个接口后,突然来了灵感,我写在 test 下面的那些 unittest,不就可以用来做服务端的自动化测试吗?事实证明,完全可以! 被测对象,从 python 库,变成了服务端 API。

举个例子,服务端有个 login 接口,接收 name 和 password 参数。我把这个接口封装成了 python 调用,类似于 RPC。

resp = User.login(name, password)

这样,如果有人需要访问 login 接口,只要像上面这样调用就可以了。

我这样写 unittest。

def test_login():
    resp = User.login("xxx", "123")
    assert resp.nick_name == "xxx"

在 robot 里面淫浸多年,我非常喜欢数据驱动方式的自动化测试,于是通过 pytest 来解决这个问题。

import pytest
@pytest.mark.parametrize("name, password, expected", [
    ("xxx", "123", "xxx"),
    ("yyy", "123", "yyy"),
])
def test_login(name, password, expected):
    resp = User.login(name, password)
    assert resp.nick_name == expected

Bingo!现在完全可以通过不同的 name 和 password,来测试服务端的 login 接口了。接着我又把我在 robot 里面已经实现的一些功能,搬了过来。

测试缓存不存在的情况。

def test_login():
    with mc_delete("userinfo_xxx"):
        resp = User.login("xxx", "123")
        assert resp.nick_name == "xxx"

熟悉的 with !不过这还不能完全体现 with 的优势,来看看另外一个例子。

假设 login 接口,需要再调用内部系统去完成,那么我们可以测试内部系统响应异常的情况。

def test_login():
    with mock("microservice", "error"):
        resp = User.login("xxx", "123")
        assert resp.not_ok
        assert resp.nick_name == ""

mock 函数把内部系统的响应,mock 为 error,导致我们的系统,不能正常登录。很妙的是,mock 还是一个 context manager,离开 with 块之后,自动取消 mock 状态。测试用例还可以写成这样。

def test_login():
    with mock("microservice", "error"):
        resp = User.login("xxx", "123")
        assert resp.not_ok
        assert resp.nick_name == ""
    resp = User.login("xxx", "123")
    assert resp.ok
    assert resp.nick_name == "xxx"

很快,这样子写出来的用例,就有了 600 多个,每天都在 jenkins 上面做自动化回归测试。于是我成功地告别了 robot。

撸完这些后,忽然觉得心里空荡荡的。这大概就是我所理解的,API 接口自动化的最好状态了。

Simple is better than complex!