目标

提供比较实用的 Lua Busted 单元测试实例。

环境

  • Unity 2018.2.5f1 Personal (64bit)
  • IntelliJ IDEA 2018.2.3 (Community Edition), Build #IC-182.4323.46, built on September 4, 2018
  • JRE: 1.8.0_152-release-1248-b8 amd64
  • JVM: OpenJDK 64-Bit Server VM by JetBrains s.r.o
  • Windows 10 10.0

目录结构及简单示例

└─project       项目文件夹├─spec      测试代码(每个文件命名为 *_spec.lua 的形式)└─src       项目代码

Busted 约定将测试放在 spec 文件夹中,命名为 *xx*_spec.lua。这样在 spec 父文件夹层级(即project)打开命令行使用 busted,会运行 spec 文件夹下的 *_spec.lua 中的所有测试。

spec 下新建文件如下:

-- spec/sample_spec.lua
describe("basic test", function()it("should pass", function()assert.truthy("yup")end)it("should throw error if assert false", function()assert.falsy("yup")end)
end)

project 下打开命令行,输入 bustedbusted -o TAP 回车,

运行结果:

D:\tmp\lua>busted -o TAP
ok 1 - basic test should pass
not ok 2 - basic test should throw error if assert false
# spec\sample_spec.lua @ 6
# Failure message: spec\sample_spec.lua:7: Expected to be falsy, but value was:
# (string) 'yup'
1..2

我们使用了 TAP 形式的输出,可以看到 busted 将 describe 与 it 连接起来形成了最后的测试名,因为我们的书写方式,最后的测试名是一个完整的句子,这方便通过测试名就对测试所关注的功能一目了然。

测试结果中第一个通过了,因为我们 assert.truty() 里面的值只要不是 falsenil 就能通过,字符串 "yup" 自然是可以通过的。

测试中第二个没有通过,因为我们在传入相同的字符串的时候,使用了 assert.falsy(),这当然会失败。从这个例子我们可以看到,当测试不通过时,busted 会给出错误所在行数,也会详细地告诉我们测试希望的值是什么、实际得到的值是什么。

概念说明与规范

在实际的单元测试中,项目代码往往有很多依赖,各种调用将各个模块连接耦合起来。在设计不佳的系统中甚至可能为了测试一个模块而初始化所有模块。UI 逻辑多了不好测。UI 需要加载其他的模块也不好独立出来测。

针对这种情况,使用 mock 可以解决一部分问题。不过,这在 MVC 分离不全情况下也难以应用。能测的主要是那种独立函数、独立类。涉及 UI 的测试还是要另想办法。

A mock object is simply a testing replacement for a
real-world object.

– Pragmatic Unit Testing in C# with NUnit 2e

Mock 就是一个真实模块的替代品。可以类比于演员的替身来理解。它与真实模块有相同的接口,但是接口返回值是我们直接指定的特定值,这样就达成了其他模块改变时我们测试的这个模块获得的返回值依然是固定的,也就达成了与其他模块隔离及解耦的目的。

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

Busted 的 Mock 主要提供了调用次数统计、调用时传入参数的获取的功能。但是没有起到让我们指定返回值的作用。在 NUnit 中的 Mock 通常是自行实现一个与真实模块具有相同接口(interface)的类,然后进行使用。在 NUnit 中,Mock 更多地是一种概念而不是实际接口;在 Busted 中则提供了实际的 mock 函数,但是仅仅是有一些监测功能。

但是核心问题在于,测试的函数可能依赖多个模块,要调用其他模块的函数、取其中的值。Lua busted 的 Mock 并不能满足需求,所以替代真实模块、隔离系统其他模块的任务,还是需要我们根据实际情况来手工解决。

以下先介绍 busted 提供的调用统计的方法,再介绍模块隔离/替代的办法,最后对 Busted 的测试命名规范给出一些建议。在下一小节将对介绍的内容给出综合示例。

调用统计

在 busted 中,进行参数和调用统计的 Mock 分为 spystub 两种。

  • 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 了。

命名规范

笔者比较喜欢 describe it style (参照的是 https://www.bignerdranch.com/blog/why-do-javascript-test-frameworks-use-describe-and-beforeeach/), 因为最后的测试名是一个完整的句子,这方便通过测试名就对测试所关注的功能一目了然。

以下提供一些格式,| 分隔不同的段,最后一个段放在 it() 里面,前面的都放在 describe() 里面,最终组合成完整的句子:

Target | when | does sth / returns sth
Target | should have sth
Target | can do sth

以上的格式概括了常见的测试关注点:模块可以做什么、应该有什么、在某种情况下应该做什么或者返回什么。

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

例如笔者定义了 #InternalVariableUsed 这个标签来标记使用了私有或者保护变量的测试,这种测试很容易因为重构而失败。毕竟一般的单元测试应该测公共的方法和接口。

使用方法:

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

另外,busted 采用的 assert 形式,期望的值写在前,实际值写在后,例如:

assert.are.equal(expected, actual)

更加复杂的实例

此处创建常规的模块 sample_module,提供全局变量的模块 sample_module_global,以及没有用到只是 require 的模块 sample_module_empty,还有 require 了其他模块的模块 main_module,分别写出对它们的测试方法。

针对 main_module,提供了隔离其他模块的单元测试,也提供了实际加载其他模块的集成测试。

各文件代码如下:

src/logic/main.lua

local main = require("main_module")print(main:TimeSixValue(2))

src/logic/main_module.lua

local MainModule = {}local SampleModule = require("sample_module")
require("sample_module_global")
local Empty = require("sample_module_empty")function MainModule:TimeSixValue(value)return SampleModule:ModifyInt(value) * SampleGlobal.ModifyInt(value) / value
endfunction MainModule:ReturnGlobalString()return GLOBAL_STRING
endreturn MainModule

src/logic/sample_module.lua

local SampleModule = {}function SampleModule:ModifyInt(value)return value * 2
end
return SampleModule

src/logc/sample_module_empty.lua

local SampleModuleEmpty = {}return SampleModuleEmpty

src/logic/sample_module_global.lua

SampleGlobal = {}function SampleGlobal.TestFunc()print("SampleGlobal.TestFunc called")
endfunction SampleGlobal.ModifyInt(value)return value * 3
endGLOBAL_STRING = "a global string"

spec/main_module_spec.lua

describe("MainModule", function()local mainModule = {}-- By default each test file runs in a separate insulate block-- run before the describe block.setup(function ()package.preload["sample_module"] = function ()local mock = {}function mock:ModifyInt(value)return value * 2endreturn mockendpackage.preload["sample_module_global"] = function ()SampleGlobal = {}function SampleGlobal.ModifyInt(value)return value * 3end_G.SampleGlobal = SampleGlobalendpackage.preload["sample_module_empty"] = function ()endmainModule = require("../src/logic/main_module")end)it("should make value six times of itself", function()assert.are.equal(12, mainModule:TimeSixValue(2))end)it("should return global string", function ()stub(_G, "GLOBAL_STRING")--print(mainModule:ReturnGlobalString())assert.are_not.equal("a global string", mainModule:ReturnGlobalString())end)
end)

在这里使用 package.preload 给各个模块写了 Mock。对于常规的模块很方便。对于全局的略麻烦,用到了 _G。正常情况下能写 local return 式的模块时还是尽量不要写 global 的。对于 empty 的模块或者没使用的模块,给一个空函数就完全够用了。把全部变量封住就用 stub

spec/main_module_integrated_spec.lua

describe("MainModule", function()local mainModule = {}setup(function ()package.path = "./src/logic/?.lua;"..package.pathmainModule = require("../src/logic/main_module")end)it("should make value six times of itself #Integrated", function()assert.are.equal(12, mainModule:TimeSixValue(2))end)
end)

做集成测试主要得注意 package.path,要把源码路径加好。

spec/sample_module_spec.lua

describe("SampleModule", function()local sampleModule = {}setup(function ()sampleModule = require("../src/logic/sample_module")end)it("should double value", function()assert.are.equal(4, sampleModule:ModifyInt(2))end)
end)

spec/sample_module_global_spec.lua

describe("SampleGlobal", function()setup(function ()require("../src/logic/sample_module_global")end)it("should triple value", function()assert.are.equal(6, SampleGlobal.ModifyInt(2))end)
end)

运行结果

D:\tmp\lua>busted -o TAP
ok 1 - MainModule should make value six times of itself #Integrated
ok 2 - MainModule should make value six times of itself
ok 3 - MainModule should return global string
ok 4 - SampleGlobal should triple value
ok 5 - SampleModule should double value
1..5

复杂例子打包下载

  • https://gitee.com/hustlion-dev/busted-real

针对 Lua 单元测试的思考

Lua 单元测试有一个特点就是想要使用私有成员变量时非常方便,但是这也导致测试更加容易失败。因为内部实现总是没有外部接口那么稳定的。需要建立一些原则来进行规范。

工作单元有三种最终结果:返回值、内部状态改变、调用第三方对象。基于值的测试验证了一个函数的返回值;基于状态的测试改变被测试对象的状态,然后验证其可见的状态变化;交互测试则是验证一个对象如何向其他对象发送消息的测试。实际单元测试中,优先考虑基于值、基于状态的测试方法,因为这两种测试可以减少对代码内部细节的假设。
来自:桃子妈咪 https://www.jianshu.com/p/c7d5a214c485

参考

  • 单元测试之如何破除外部依赖
  • 单元测试之如何编写优秀的单元测试用例
  • mocking out a lua module using package.preload
  • Testing Private Methods with JUnit and SuiteRunner
  • How do you mock out the file system in C# for unit testing?
  • How to unit test a node.js module that requires other modules
  • Pragmatic Unit Testing in C# with NUnit 2e
  • Mocking globals using Busted
  • how to mock a global function?
  • Unit testing the Lua code

Lua Busted 单元测试实战相关推荐

  1. Lua Busted 单元测试简介(Windows 环境)

    简介 本文目标是在 Windows 环境下使用 Busted 进行 Lua 单元测试. Busted 是一款 BDD 风格的 Lua 单元测试框架,支持 TAP 风格输出. 环境 Lua 5.3.5 ...

  2. Springboot单元测试mysql_Springboot Mybatis-Plus数据库单元测试实战(三种方式)

    单元测试长久以来是热门话题,本文不会讨论需不需要写单测,可以看看参考资料1,我个人认为写好单测应该是每个优秀开发者必备的技能,关于写单测的好处在这里我就不展开讨论了,快速进入本文着重讨论的话题,如何写 ...

  3. TestNG单元测试实战

    TestNG单元测试实战 单元测试是什么 对软件中最小可测试单元进行验证.小到一个方法,大到一个类.一个模块.都可以用单元测试覆盖. 单元测试有必要写吗 现状: 面对日复一日排满的需求,没有时间去写. ...

  4. 使用Junit对Spring进行单元测试实战小结

    Demo代码: Java代码   @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = "cla ...

  5. 一文说尽Golang单元测试实战的那些事儿

    导语 | 单元测试,通常是单独测试一个方法.类或函数,让开发者确信自己的代码在按预期运行,为确保代码可以测试且测试易于维护.腾讯后台开发工程师张力结合了公司级漏洞扫描系统洞犀在DevOps上探索的经验 ...

  6. Mock(Mockito)单元测试实战

    文章目录 1.什么是Mock? 1.1. Mockito中文文档 1.2.Mockito基本使用方法简介 初始化MockMvc对象(BaseUnitTest) 初始化Mock框架 测试方法 2.0 m ...

  7. 在C++项目中引入Lua(AlphaGo使用的方案)

    最近大火的AlphaGo,其中的deepmind已经开源,可以到github中下载https://github.com/deepmind/lab·,网上还有一个基于Python开源AlphaGo,那个 ...

  8. 使用Codeception进行Yii2的单元测试(一)安装以及简介篇

    一.简介 作为php工程师,我们很可能遇到一些有必要的进行单元测试的场景.本系列文章就来介绍下,在Yii2框架下,如何使用Codeception进行单元测试.本篇就首先介绍Yii2,以及Codecep ...

  9. ABP入门系列(11)——编写单元测试

    1. 前言 In computer programming, unit testing is a software testing method by which individual units o ...

最新文章

  1. Fourinone2.0对分布式文件的简化操作
  2. 大唐杯5g题库_“大唐杯”全国大学生移动通信技术大赛暨5G教育创新论坛成功举行...
  3. 广播,实现强制下线功能(项目文件已上传GitHub)
  4. 《认清C++语言》のrandom_shuffle()和transform()算法
  5. Educational Codeforces Round 114 (Rated for Div. 2) D. The Strongest Build 暴力 + bfs
  6. token验证_如何利用 C# 爬取带 Token 验证的网站数据?
  7. 数据结构--红黑树 Red Black Tree
  8. Keepalived+Nginx实现高可用,反向代理---模拟实现线上环境
  9. (转)invalidate()和postInvalidate() 的区别及使用
  10. Netty优雅退出机制和原理
  11. Javashop 7.0 统一登录unionID问题修改
  12. Python+OpenCV实现sobel边缘检测
  13. 华硕老毛子padavan固件花生壳ddns设置教程
  14. 用Python告诉你深圳房租有多高,做程序员真的能买得起嘛
  15. Hash算法总结(转载)
  16. RABBIT API (随机ACG图片接口推荐)
  17. SSRF - ctfhub -2【FastCGI协议、Redis协议、URL Bypass、数字IP Bypass、302跳转 Bypass、DNS重绑定Bypass】
  18. SonarQube最全使用教程
  19. 爱的时候,好好爱(转)
  20. Chips-2.0(二)DEMO工程综合实现

热门文章

  1. Excel·VBA选中列一键计算小计总计
  2. l7sa008b故障代码_美国凯利冷机故障码表
  3. Ubuntu设置屏幕分辨率及屏幕翻转
  4. html如何制作正方形,正方形的立方体怎么做 怎么用纸做十厘米的正方体?
  5. 【Linux】Nvidia显卡驱动安装教程
  6. 洛谷P4711 【化学】 相对分子质量 简单题解
  7. IE浏览器JSON未定义
  8. 怎样查看服务器上的文件夹大小写,查看ftp服务器所有文件夹大小写
  9. 云原生数据中台:架构、方法论与实践
  10. 如何选择最省心的云主机?