0%

API网关kong单元测试

Kong 自行封装了一套基于Busted 的测试框架, Busted约定将测试放在 spec 文件夹中,命名为 xx_spec.lua.

网关项目的测试代码存放路径: spec/spec/custom-plugins, 运行单元测试后测试框架将生成一个Kong实例并对其执行测试, 具体查阅 spec/kong_tests.conf 配置文件。

注:Kong的单元测试依赖于测试框架 Busted, 具体详细查看Busted介绍部分

引用:

OpenResty提供了一个数据驱动的测试支架,用于为NGINX C模块,Lua库甚至OpenResty应用程序编写声明性测试用例
用于Nginx C模块和基于 Nginx / OpenResty 的库和应用程序的数据驱动测试脚手架

参考:
https://github.com/openresty/test-nginx
https://openresty.gitbooks.io/programming-openresty/content/testing/running-tests.html

运行所有测试:
cd member-api-gateway/spec, 命令行运行 :

1
./bin/busted -o TAP  spec/custom-plugins/

执行命令后将运行custom-plugins文件夹下的 *_spec.lua 中的所有测试。

运行特定插件测试

1
./bin/busted -o TAP  spec/custom-plugins/logout(特定插件测试目录)

只需在路径后带上子目录。 其他用法例如:

1
cd kong/bin/   &&  busted -o gtest -v  --config-file=../.busted   --exclude-tags=flaky,ipv6,cassandra,off   

若要将测试集成到 travis ci, 需指定测试结果输出格式为TAP:

1
./bin/busted -o TAP   spec/custom-plugins/

调试测试代码

调试kong插件:

1.在插件中直接打印错误, 例: require “pl.pretty”.dump(allow_origin_domains)
2.在对应插件的单元测试代码中,暂停代码执行. 例:os.execute(“sleep 1000”)
3.进入测试框架生成的kong运行实例的目录中, 查看错误日志. 实例运行目录在 member-api-gateway/spec/spec/kong_test.conf 中的 prefix 配置项定义.
vim prefix 路径/logs/error.log 查看插件输出信息.

测试代码说明

数据库对象

bp 的对象是在spec/fixtures/blueprints.lua 中的_M.new(db) 进行定义, 这里定义后, 用于在插入数据时给出默认值,如:自增数据

1
2
3
4
5
6
local bp, db = helpers.get_db_utils(strategy, {
'routes',
'services',
'plugins',
'app_config', --这里指定要清空数据的表
})

db.app_config 对象必须要加载插件request-check 才能访问到, 因为daos.lua文件定义了app_config对象, 通过

1
helpers.start_kong({    plugins = 'bundled,request-check' }))

加载插件表对象

1
db.plugins:load_plugin_schemas(conf.loaded_plugins)

启动测试用kong实例

1
2
3
4
assert(helpers.start_kong({
database = strategy,
nginx_conf = "spec/fixtures/custom_nginx.template",
}))

mock 外部接口

spec/helpers.lua 文件中定义了 mock_upstream_url 地址, 通过这个地址访问mock url.

需要mocker的url, 需到spec/fixtures/custom_nginx.template文件中的定义行为

server {
server_name mock_upstream;

​ ….

   location = /你要mock的url {
        content_by_lua_block {
            local mu = require "spec.fixtures.mock_upstream"
            ...
            你自定义的mock 返回数据
            return mu.send_default_json_response({
                delay = delay_seconds,
            })
        }
    }

}

– before_each(function()
– helpers.kill_all()
– assert(db:truncate())
– local service2 = bp.services:insert()
– assert(helpers.start_kong({
– database = strategy,
– nginx_conf = “spec/fixtures/custom_nginx.template”,
– }))
– end)
– assert(helpers.start_kong({
– database = strategy,
– nginx_conf = “spec/fixtures/custom_nginx.template”,
– }))
– db:truncate(“plugins”)
– assert(db:truncate())
– db:reset()
– os.execute(“sleep 1000”)
– assert 返回 Failure
– error(err, 2) 返回 Error
– assert.is_true(json.code == 0)
– assert.falsy(err)
– assert.truthy(body.headers[‘authorization’])
– assert.are.same(6, tonumber(res.headers[“x-ratelimit-limit-minute”]))
– assert.same({ message = “API rate limit exceeded” }, json)
– assert.equal(0, body.code)
– assert.not_equal(headers[‘access-token’], res.headers[‘access-token’])
– local row, err = db.plugins:select({id = plugin.id}, { nulls = true })

– local res = assert(admin_client:send {
– method = “GET”,
– path = “/plugins”,
– headers = {
– [“Content-Type”] = “application/json”
– }
– })
– local body, err = res:read_body()
– dump(body)

Busted介绍

Busted 是一个 BDD 行为驱动开发测试框架. 《BDD in action》这本书详细讲解行为驱动开发, 属于敏捷开发工程方法范畴。

kong 官方的单元测试示例:https://discuss.konghq.com/t/kong-testing-framework/689/2

busted 框架文档 http://olivinelabs.com/busted/#spies-mocks-stubs

安装

安装前需要注意:

busted works with lua >= 5.1, moonscript, terra, and LuaJIT >= 2.0.0.

即Lua版本必须>=5.1, LuaJIT >= 2.0.0

首先要通过 luarocks 安装 busted 框架,执行如下命令:

luarocks install busted

备注:如需要运行异步测试, 则需安装如下依赖

1
2
3
4
5
apt-get install libev-dev, git
luarocks install copas
luarocks install lua-ev scm --server=http://luarocks.org/repositories/rocks-scm/
luarocks install moonscript

然后重新安装和运行测试

1
2
3
luarocks remove busted --force
luarocks make
busted spec

命令行执行:busted

出现如下信息代表安装成功:

1
0 successes / 0 failures / 2 errors / 0 pending : 0.000012 seconds

然后再使用框架执行单元测试test.lua文件:

1
busted test.lua

task 任务和预定义配置

Busted 1.6 添加了任务概念和预定义配置。

任务: 如项目下存在 .busted文件, 则会自动加载.busted定义的任务.
预定义配置: Busted –config-file=FILE 使用–config-file 加载配置文件将会覆盖掉根目录下的 .busted 文件

任务:

​ 在项目根目录下创建 .busted 文件, 运行 busted 时如果存在此文件会自动加载它, .busted文件格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
return {
-- 这个key会被所有task继承, 这里指定所有task都进行覆盖率分析, coverage 是 Busted 可执行文件的参数, 你可以添加 busted --help 列出的参数
_all = {
coverage = true
},

-- 未指定任务,则运行default选项, verbose 是 Busted 可执行文件的参数
default = {
verbose = true
},

-- 任务项
apiUnit = {
tags = "api",
ROOT = {"spec/unit"},
verbose = true
}
}

创建 .busted 文件并指定配置后, 运行 busted --run=apiUnit, 即可跑apiUnit测试项. 实际跑运行命令: busted --coverage --tags=api --verbose spec/unit

如果直接运行 busted, 实际跑运行命令: busted --coverage --verbose

预定义配置:

​ Busted –config-file=FILE 使用–config-file加载配置文件将会覆盖掉根目录下的 .busted 文件配置

运行方式

1.独立运行

​ 在 test.lua 文件的头部添加 require 'busted.runner'() , 它就变成可独立运行的测试,可直接 lua test.lua 运行测试用例.

​ 独立运行方式仍可使用 busted –help 列出的参数, 如:lua test.lua -t "tag" --verbose

2.busted执行器运行

​ busted test.lua

定义测试用例

​ 测试用例由 describe 和 it 块组成,describe(别名:context) 可以嵌套多层. 函数before_each, after_each运行在嵌套测试块之前和之后, 函数setup, teardown 运行在describe块之前和之后.

​ 函数pending 用于留下占位符, 以便稍后编写测试代码, #hashtags 给指定测试打标记。 以便用命令行运行时,通过 -t 选项来运行指定标记测试。 多个标记用逗号分隔。

describe:Context blocks

1
2
3
4
5
6
7
8
9
10
11
describe("a test", function()
-- tests go here

describe("a nested block", function()
describe("can have many describes", function()
-- tests
end)
end)

-- more tests pertaining to the top level
end)

insulate, expose:

​ 这两个指令是 describe 的别名 , 用于控制 busted 执行的context块的沙箱级别. insulate 块隔离测试环境, expose 块将测试环境暴露给外部上下文块.

​ 默认情况下, 每个测试文件运行在一个相互隔离的块中, 可以使用 –no-auto-insulate 来禁止这个特性.

​ 测试环境隔离块中的 lua 全局表 _G, 还有require加载模块缓存变量 package.loaded, 在离开insulate块后, 将还原它们到最初的状态, 因此,其它 describe 块都无法访问 insulate 块中的变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
insulate("an insulated test", function()
require("mymodule")
_G.myglobal = true

-- tests go here

describe("a nested block", function()
describe("can have many describes", function()
-- tests
end)
end)

-- more tests pertaining to the top level
end)

describe("a test", function()
it("tests insulate block does not update environment", function()
assert.is_nil(package.loaded.mymodule) -- mymodule is not loaded
assert.is_nil(_G.myglobal) -- _G.myglobal is not set
assert.is_nil(myglobal)
end)

-- tests go here
end)

​ expose块会导出全局表_G和package.loaded的更改到后续的上下文块中(describe 块)。另外,expose 块内创建的任何全局变量,都是在 context block 2 level 之外的环境创建的,在文件头部使用expose 块,将提升require和global的级别到root环境, 这些变量将扩散到后续的测试文件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
-- test1_spec.lua
expose("an exposed test", function()
require("mymodule")
_G.myglobal = true

-- tests can go here

describe("a nested block", function()
describe("can have many describes", function()
-- tests
end)
end)

-- more tests pertaining to the top level
end)

describe("a test in same file", function()
it("tests expose block updates environment", function()
assert.is_truthy(package.loaded.mymodule) -- mymodule is still loaded
assert.is_true(_G.myglobal) -- _G.myglobal is still set
assert.is_equal(myglobal)
end)

-- tests go here
end)

1
2
3
4
5
6
7
8
9
10
-- test2_spec.lua
describe("a test in separate file", function()
it("tests expose block updates environment", function()
assert.is_truthy(package.loaded.mymodule) -- mymodule is still loaded
assert.is_true(_G.myglobal) -- _G.myglobal is still set
assert.is_equal(_G.myglobal, myglobal)
end)

-- tests go here
end)

Tagging Tests

​ 用#tags对测试用例进行标记,运行测试用 -t 运行指定用例

1
2
3
4
5
6
7
8
9
10
11
12
13
describe("a test #tag_1", function()
-- tests go here
end)

describe("a nested block #another", function()
describe("can have many describes", function()
-- tests
end)

-- more tests pertaining to the top level
end)

busted -t "tag_1" ./test.lua

–exclude-tags 选项用于排除指定测试用例, 例如:busted --exclude-tags="another" ./test.lua

​ -t, –tags,–exclude-tags 一起使用时, –exclude-tags 优先级最高.

Randomizing Tests

​ 调用 randomize() 使测试随机化

1
2
3
4
5
6
describe("a ramdomized test", function()
randomize()

it("runs a test", function() end)
it("runs another test", function() end)
end)

​ –shuffle 选项使所有测试用例随机运行, 则可以通过 randomize(false) 使得指定的用例的不进行随机化, 即每次都运行.

1
2
3
4
5
6
describe("a non-randomized test", function()
randomize(false)

it("runs a test", function() end)
it("runs another test", function() end)
end)

It

​ describe 用于定义上下文块Context blocks, it 用于定义测试, 如果it块中的 assert functions 测试未通过将抛出错误, assert 也可以用 spec, test 等别名替代

1
2
3
4
5
6
7
describe("busted", function()
it("has tests", function()
local obj1 = { test = "yes" }
local obj2 = { test = "yes" }
assert.same(obj1, obj2)
end)
end)

before_each, after_each, setup, teardown

​ before_each在每次子测试(it 块)之前运行,after_each则在之后运行。

​ setup在describe块中最先运行,teardown在describe块中最后运行。

​ setup, teardown 有两种模式, lazy(懒惰) 或者 strict(严格),

​ 只有当前或嵌套的 describe 块中至少存在一个子测试时, 才会运行lazy_setup和lazy_teardown.

​ strict_setup和strict_teardown 总是在describe块中运行, 即使没有子测试.

​ 默认,setup和teardown是strict模式,但可以用 –lazy 选项改变位懒惰模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
describe("busted", function()
local obj1, obj2
local util

setup(function()
util = require("util")
end)

teardown(function()
util = nil
end)

before_each(function()
obj1 = { test = "yes" }
obj2 = { test = "yes" }
end)

it("sets up vars with the before_each", function()
obj2 = { test = "no" }
assert.are_not.same(obj1, obj2)
end)

it("sets up vars with the before_each", function()
-- obj2 is reset thanks to the before_each
assert.same(obj1, obj2)
end)

describe("nested", function()
it("also runs the before_each here", function()
-- if this describe also had a before_each, it would run
-- both, starting with the parents'. You can go n-deep.
end)
end)
end)

finally

    作为更轻的替代方案,避免设置upvalues
    it('checks file contents',function()
      local f = io.popen('stupid_process')

      -- ensure that once test has finished f:close() is called
      -- independent of test outcome
      -- 确保一旦测试完成就调用f:close,  独立于测试结果
      finally(function() f:close() end)

      -- do things with f
    end)


  

Pending

​ 计划稍后编写(或修复)的测试的占位符

1
2
3
describe("busted pending tests", function()
pending("I should finish this test later")
end)

Asserts 断言

​ 它是busted的核心,是用来实际编写测试的东西. Busted使用luassert库来提供断言。

​ 注意,一些断言/修饰符是Lua关键字(true,false,nil,function和not), 不能使用 ‘.’ 来使用它们,因为这会导致编译错误。 可以使用’_’(下划线)或首字母大写来使用。

​ 示例:

1
2
3
4
5
6
7
assert.is_true(json.code == 0)
assert.falsy(err)
assert.truthy(body.headers['authorization'])
assert.are.same(6, tonumber(res.headers["x-ratelimit-limit-minute"]))
assert.same({ message = "API rate limit exceeded" }, json)
assert.equal(0, body.code)
assert.not_equal(headers['access-token'], res.headers['access-token'])

详情参考:https://olivinelabs.com/busted/#asserts

MOCK

项目代码往往有很多依赖,各种调用将各个模块连接耦合起来。在设计不佳的系统中甚至可能为了测试一个模块而初始化所有模块. 针对这种情况,使用 mock 可以解决一部分问题.
Mock 就是一个真实模块的替代品。可以类比于演员的替身来理解。它与真实模块有相同的接口,但是接口返回值是我们直接指定的特定值,这样就达成了其他模块改变时我们测试的这个模块获得的返回值依然是固定的,也就达成了与其他模块隔离及解耦的目的。

通过 Mock 的使用,我们还可以监测对一个模块的调用行为,可以通过 Mock 来统计调用次数,也能知道调用时传入的参数。

Busted 的 Mock 主要提供了调用次数统计、调用时传入参数的获取的功能。但是没有起到让我们指定返回值的作用。在 NUnit 中的 Mock 通常是自行实现一个与真实模块具有相同接口(interface)的类,然后进行使用。在 NUnit 中,Mock 更多地是一种概念而不是实际接口;在 Busted 中则提供了实际的 mock 函数,但是仅仅是有一些监测功能。但是核心问题在于,测试的函数可能依赖多个模块,要调用其他模块的函数、取其中的值。Lua busted 的 Mock 并不能满足需求,所以替代真实模块、隔离系统其他模块的任务,还是需要我们根据实际情况来手工解决。

busted 提供的调用统计的方法

​ 在 busted 中,进行参数和调用统计的 Mock 分为 spy 和 stub 两种。
​ spy 对目标进行监测,可以监测其是否被调用、被用什么函数调用。
​ stub 则是对目标进行包装,同样监测其是否被调用、被用什么函数调用。与 spy 不同之处是,stub 会截取调用,不会实际调用目标。适合测试数据层,这样不会实际找数据库要数据。
​ spy 和 stub 是单体操作,mock 是对整个表进行的操作,mock(t) 得到 t 的 spies, mock(t, true) 得到 t 的 stubs

模块隔离/替代的办法

​ 解决全局变量、require 的隔离.

​ 全局量

​ 如果待测试的代码使用了全局变量 GLOBAL,那么使用 stub: stub(_G, “GLOBAL”)

​ require

​ 使用 lua 的 package 机制来改变 require.

​ 如果要导入 src/logic/sample_module.lua 这个包,即require("src/logic/sample_module"),则使用:

    package.preload["src/logic/sample_module"] = function ()

​ –print(“fake load module”)
​ end

​ 这样在 require 这一模块时,不会去加载实际的文件,而是运行这里定义函数。在这里面就可以提供我们自己的 Mock 了

Busted 支持给测试打标签,这样方便独立运行具有某些 tag 或者不具有某些 tag 的测试

使用方法:

  • 只测有该标签的测试:busted -o TAP --tags=InternalVariableUsed
  • 排除有该标签的测试:busted -o TAP --exclude-tags=InternalVariableUsed

参考资料

OpenResty提供了一个数据驱动的测试支架,用于为NGINX C模块,Lua库甚至OpenResty应用程序编写声明性测试用例
Test :: Nginx - 用于Nginx C模块和基于Nginx / OpenResty的库和应用程序的数据驱动测试脚手架
参考:
https://github.com/openresty/test-nginx
https://openresty.gitbooks.io/programming-openresty/content/testing/running-tests.html

参考链接: