1. EUnit - a Lightweight Unit Testing Framework for Erlang
1 eUnit--Erlang的轻量级单元测试框架
eUnit是Erlang的单元测试框架。它功能强大灵活,使用方便,句法开销小。
Unit testing
Terminology
Getting started
EUnit macros
EUnit test representation
EUnit构建了Beck和Gamma(以及Beck以前的Smalltalk框架SUnit)源自面向对象语言的单元测试框架系列的思想。然而,EUnit使用更适合于功能和并发编程的技术,并且通常不会比其亲属冗长。
尽管EUnit使用了许多预处理器宏,但它们被设计成尽可能不侵入,并且不应与现有代码发生冲突。因此,向模块添加EUnit测试通常不需要更改现有代码。此外,只执行模块的导出功能的测试总是可以放置在完全独立的模块中,从而完全避免任何冲突。
1.1单元测试
单元测试正在相对独立地测试单个程序“单元”。没有特定的尺寸要求:一个单元可以是一个功能,一个模块,一个过程甚至整个应用程序,但最典型的测试单元是单独的功能或模块。为了测试一个单元,你需要指定一组单独的测试,为能够运行这些测试设置最小的必要环境(通常,你根本不需要做任何设置),运行测试并收集结果,最后你做任何必要的清理,以便稍后再次运行测试。单元测试框架试图在这个过程的每个阶段为你提供帮助,因此写测试很容易,运行起来也很容易,而且很容易看出哪些测试失败(这样你就可以修复这些错误)。
单元测试的优势
减少更改程序的风险
大多数程序在它们的生命周期中都会被修改:错误将被修复,功能将被添加,优化可能变得必要,或者代码需要被重构或者以其他方式清理以使其更容易处理。但是对工作程序的每一次改变都可能会引入新的错误 - 或重新引入之前已修复的错误。有一组单元测试,你可以用很少的努力来运行,这使得很容易知道代码仍然能够正常工作(这种用法称为回归测试
;请参阅Terminology
)。这对于减少对改变和重构代码的阻力有很大的作用。
帮助指导和加快开发过程。
通过专注于让代码通过测试,程序员可以变得更有效率,不过分指定或在过早优化中迷失方向,并从一开始就创建正确的代码(所谓的测试驱动开发
;请参阅参考资料Terminology
)。
帮助将接口与实现分开
编写测试时,程序员可能会发现应该不存在的依赖关系(以便让测试运行),以及需要抽象哪些依赖关系以获得更清晰的设计。这有助于在散布整个代码之前消除不良的依赖关系。
使组件集成更容易
通过以自下而上的方式进行测试,从最小的程序单位开始测试并确信它们按照它们的工作方式进行测试,可以更容易地测试由多个此类单元组成的更高级别组件也可以按照规范行事(称为集成测试
;请参阅Terminology
)。
是自我记录
测试可以理解为文档,通常显示正确和不正确使用的示例,以及预期的结果。
1.2术语
单元测试
根据其规格测试程序单元的行为(本身)。单元测试作为回归测试具有重要功能,当程序稍后因某种原因而被修改时,因为它们检查程序仍然按照规范行事。
回归检验
对程序进行更改后运行一组测试,检查程序的行为是否与更改之前的行为一样(当然,除了行为的任何有意更改外)。单元测试作为回归测试很重要,但回归测试不仅仅涉及单元测试,还可能测试可能不属于正常规范的行为(例如bug-for-bug兼容性)。
集成测试
测试一些单独开发的程序单元(假设已经单独进行单元测试)按预期一起工作。根据正在开发的系统,集成测试可能很简单,只是“单元测试的另一个级别”,但也可能涉及其他类型的测试(比较系统测试
)。
系统测试
测试一个完整的系统是否符合其规范。具体来说,系统测试不应该要求知道实现的任何细节。它通常包括测试系统行为的许多不同方面,除了基本功能,如性能、可用性和可靠性。
测试驱动开发
一种程序开发技术,您可以在其中不断地编写测试。以前
您实现了应该通过这些测试的代码。这可以帮助您专注于解决正确的问题,而不是让单元测试确定程序何时“完成”,而不是进行比必要更复杂的实现:如果程序符合其规范,就没有必要继续添加功能。
模拟对象
有时候,测试某个单元A
(例如函数)需要与其他单元以某种方式进行协作B
(可能作为参数或通过引用传递) - 但B
尚未实现。一个“模拟对象” - 为了测试的目的A
,外观和行为就像一个真实的对象B
- 然后可以用来代替。(当然这只有在实现一个真实的工作B
比创建一个模拟对象的工作要多得多时才有用。)
测试用例
一种单一的,定义明确的测试,它可以被唯一地识别出来。执行时,测试用例可以传球
或失败
测试报告应该准确地识别哪些测试用例失败
了。
测试套件
测试用例集合,通常具有特定的、通用的测试目标,例如单个功能、模块或子系统。测试套件也可以由较小的测试套件递归地组成。
1.3开始
Including the EUnit header file
Writing simple test functions
Running EUnit
Writing test generating functions
An example
Disabling testing
Avoiding compile-time dependency on EUnit
包括eUnit头文件
在Erlang模块中使用EUnit最简单的方法是在模块的开始处(在-module
声明之后,但在任何函数定义之前)添加以下行:
-include_lib("eunit/include/eunit.hrl").
这将产生以下效果:
- 创建一个导出的函数
test()
(除非关闭了测试,并且该模块尚未包含test()函数),该函数可用于运行模块中定义的所有单元测试
- 导致名称匹配的所有函数
..._test()
或..._test_()
从模块自动导出的所有函数(除非关闭测试,或EUNIT_NOAUTO
定义宏)
- 使eUnit的所有预处理器宏可用,以帮助编写测试
注意:
为了-include_lib(...)
工作,Erlang模块搜索路径必须
包含一个名称以endn 结尾的目录eunit/ebin
(指向ebin
EUnit安装目录的子目录)。如果EUnit安装lib/eunit
在Erlang / OTP系统目录下,ebin
当Erlang启动时,它的子目录将自动添加到搜索路径中。否则,您需要通过-pa
向erl
or erlc
命令传递标志来显式添加目录。例如,Makefile可以包含以下用于编译.erl
文件的操作:
erlc -pa "path/to/eunit/ebin" $(ERL_COMPILE_FLAGS) -o$(EBIN) $<
或者,如果希望eUnit在交互运行Erlang时始终可用,则可以向您的$HOME/.erlang
档案:
code:add_path("/path/to/eunit/ebin").
编写简单测试函数
eUnit框架使用Erlang编写单元测试变得非常容易。不过,有几种不同的编写方法,所以我们从最简单的开始:
名称以..._test()
end 结尾的函数被EUnit识别为一个简单的测试函数 - 它不需要参数,它的执行成功(返回EUnit将丢弃的任意值),或者通过抛出某种异常(或通过不终止,在这种情况下它会在一段时间后中止)。
简单测试函数的一个例子可以是:
reverse_test() -> lists:reverse([1,2,3]).
这只是测试函数lists:reverse(List)
不会崩溃时List
是[1,2,3]
这并不是一个很好的测试,但是很多人都会编写像这个这样的简单函数来测试他们代码的基本功能,只要他们的函数名匹配,这些测试就可以直接由eUnit使用,而不会改变。
使用异常来表示失败
为了编写更有趣的测试,当他们没有得到他们期望的结果时,我们需要使它们崩溃(抛出异常)。这样做的一个简单方法是使用模式匹配=
,如下例所示:
reverse_nil_test() -> [] = lists:reverse([]).
reverse_one_test() -> [1] = lists:reverse([1]).
reverse_two_test() -> [2,1] = lists:reverse([1,2]).
如果有一些错误lists:reverse/1
使得它返回的东西不是[2,1]
当它[1,2]
作为输入时,那么上面的最后一个测试会抛出一个badmatch
错误。前两个(我们假设他们没有得到badmatch
)将分别返回[]
并[1]
分别取得成功。(请注意,EUnit不是心理上的:如果您编写一个测试返回一个值,即使它的值是错误的,EUnit也会认为它是成功的。您必须确保测试已写入,以便导致崩溃结果不是它应该的。)
使用断言宏
如果你想在你的测试中使用布尔运算符,这个assert
宏就派上用场了(EUnit macros
详情参见):
length_test() -> ?assert(length([1,2,3]) =:= 3).
该?assert(Expression)
宏将评估Expression
,如果这个计算结果不是true
,它会抛出一个异常; 否则它只是返回ok
。在上面的例子中,如果调用length
不返回3 ,测试将失败。
运行eUnit
如果您添加了声明-include_lib("eunit/include/eunit.hrl")
如前所述,只需编译模块并运行自动导出函数即可。test()
例如,如果您的模块被命名为m
,然后打电话m:test()
将在模块中定义的所有测试上运行eUnit。你不需要写-export
测试函数的声明。这一切都是用魔法完成的。
您也可以使用该函数eunit:test/1
运行任意测试,例如尝试一些更高级的测试描述符(请参阅参考资料EUnit test representation
)。例如,运行eunit:test(m)
与自动生成的函数完全相同m:test()
,但eunit:test{inparallel, m})运
行相同的测试用例,但并行执行它们。
将测试放在单独的模块中
如果您想将测试代码与普通代码分开(至少用于测试导出的函数),则只需将测试函数写入名为m_tests
(note:not m_test
)的模块中(如果模块已命名)即可m
。然后,无论何时您要求EUnit测试模块m
,它也会查找模块m_tests
并运行这些测试。详情请参阅ModuleName
本节Primitives
。
eUnit捕获标准输出
如果您的测试代码写入标准输出,您可能会惊讶地发现测试运行时文本没有出现在控制台上。这是因为EUnit捕获测试功能的所有标准输出(这还包括设置和清理功能,但不包括生成器功能),以便在出现错误时将其包含在测试报告中。要在测试时直接绕过EUnit并将文本直接打印到控制台,您可以写入user
输出流,如下所示io:format(user, "~w", [Term])
。推荐的方法是使用EUnit Debugging macros
,这使得它更简单。
写测试生成函数
简单测试函数的缺点是
您必须为每个测试用例编写一个单独的函数(使用单独的名称)。编写测试(和更加灵活,我们将看到)的更紧凑的方式,是
编写函数返回
,而不是
测试,是
测试。
一个以名字结尾的函数..._test_()
(注意最后的下划线)被EUnit识别为测试生成器
函数。测试生成器
返回由EUnit执行的一组测试
的表示
。
将测试表示为数据
测试最基本的表示形式是一个没有参数的单一有趣表达式。例如,以下测试生成器:
basic_test_() ->
fun () -> ?assert(1 + 1 =:= 2) end.
将产生与以下简单测试相同的效果:
simple_test() ->
?assert(1 + 1 =:= 2).
(事实上,EUnit将处理所有简单的测试,就像处理fun-expressions一样:它会将它们放入一个列表中,并逐个运行它们)。
使用宏编写测试
为了使测试更加紧凑和可读,并且在测试发生的源代码中自动添加有关行号的信息(并减少必须输入的字符数),可以使用_test
宏注意最初的下划线字符),如下所示:
basic_test_() ->
?_test(?assert(1 + 1 =:= 2)).
该_test
宏接受任何表情(以下简称“机构”)作为参数,和一个有趣的表达式中放置它(有一些额外的信息一起)。身体可以是任何类型的测试表达式,就像简单测试函数的主体一样。
下划线前缀的宏创建测试对象
但是这个例子可以变得更短!大多数测试宏,比如assert
宏系列,都有一个带有初始下划线字符的相应表单,它会自动添加一个?_test(...)
包装。上面的例子可以简单地写成:
basic_test_() ->
?_assert(1 + 1 =:= 2).
它具有完全相同的含义(注意_assert
替代assert
)。您可以将初始下划线看作是信号测试对象
。
一个例子
有时候,一个例子能说出一千多个字。下面的小Erlang模块展示了eUnit如何在实践中使用。
-module(fib).
-export([fib/1]).
-include_lib("eunit/include/eunit.hrl").
fib(0) -> 1;
fib(1) -> 1;
fib(N) when N > 1 -> fib(N-1) + fib(N-2).
fib_test_() ->
[?_assert(fib(0) =:= 1),
?_assert(fib(1) =:= 1),
?_assert(fib(2) =:= 2),
?_assert(fib(3) =:= 3),
?_assert(fib(4) =:= 5),
?_assert(fib(5) =:= 8),
?_assertException(error, function_clause, fib(-1)),
?_assert(fib(31) =:= 2178309)
].
(作者注意:当我第一次写这个例子的时候,我碰巧写了一个,*
而不是+
在fib
函数中,当然,当我运行这些测试时,它立即显示出来。)
见EUnit test representation
有关可以在eUnit中指定测试集的所有方法的完整列表。
禁用测试
可以通过定义NOTEST
在编译时,例如作为erlc
,见:
erlc -DNOTEST my_module.erl
或者通过向代码中添加宏定义,在包含eUnit头文件之前
*
-define(NOTEST, 1).
%28值不重要,但通常应为1或true
29%。注意,除非EUNIT_NOAUTO
宏定义后,禁用测试也会自动从代码中删除所有测试函数,但显式声明为导出的所有函数除外。
例如,若要在应用程序中使用eUnit,但在默认情况下关闭测试,请将以下行放入头文件中:
-define(NOTEST, true).
-include_lib("eunit/include/eunit.hrl").
然后确保应用程序的每个模块都包含该头文件。这意味着您只有一个位置可以修改,以便更改测试的默认设置。若要覆盖NOTEST
设置而不修改代码,则可以定义TEST
在编译器选项中,如下所示:
erlc -DTEST my_module.erl
见Compilation control macros
有关这些宏的详细信息。
避免编译时对eUnit的依赖
如果您正在为您的应用程序分发源代码以供其他人编译和运行,您可能希望确保即使eUnit不可用,代码也会编译。与上一节中的示例一样,您可以将以下行放在一个公共头文件中:
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
-endif.
当然,还要确保将使用eUnit宏的所有测试代码都放在-ifdef(TEST)
或-ifdef(EUNIT)
各部分。
1.4 eUnit宏
尽管eUnit的所有功能都是可用的,即使不使用预处理器宏。eUnit头文件定义了许多这样的宏,以便尽可能简单地编写单元测试,并且不需要太多的细节。
除非明确声明,否则使用eUnit宏永远不会在eUnit库代码上引入运行时依赖项,不管您的代码是在启用测试的情况下编译的还是禁用的。
Basic macros
Compilation control macros
Utility macros
Assert macros
Macros for running external commands
Debugging macros
基本宏
_test(Expr)
转弯Expr通过在一个有趣的表达式和一个源行号中将它包装成一个“test对象”。从技术上讲,这和{?LINE, fun () -> (Expr) end}...
编译控制宏
EUNIT
这个宏总是被定义为true
在编译时启用eUnit时。这通常用于将测试代码放在条件编译中,如:
-ifdef(EUNIT).
% test code here
...
-endif.
例如,在禁用测试时,确保代码可以在不包含eUnit头文件的情况下编译。也见宏TEST
和NOTEST
...
EUNIT_NOAUTO
如果定义了此宏,则将禁用测试函数的自动导出或剥离。
TEST
这个宏总是被定义为%28totrue
,除非用户先前定义在编译时启用eUnit时具有另一个值%29。这可用于将测试代码置于条件编译中;也可参见宏。NOTEST
和EUNIT
...
对于严格依赖eUnit的代码,最好使用EUNIT
宏,而对于使用更一般测试约定的代码,则使用TEST
宏可能是首选。
大TEST
宏也可用于覆盖NOTEST
宏。如果TEST
被定义以前
即使在以下情况下,eUnit头文件也包含%28NOTEST
也定义为%29,则代码将在启用eUnit的情况下编译。
NOTEST
这个宏总是被定义为%28totrue
,除非用户先前定义为在eUnit为残废
在编译时。%28 CompareTEST
宏%29
此宏也可用于条件编译,但通常用于禁用测试:NOTEST
被定义以前
包含eUnit头文件,并且TEST
是不
定义后,代码将被编译为禁用eUnit。另见Disabling testing
...
NOASSERT
如果定义了此宏,则当测试也被禁用时,断言宏将没有任何效果。见Assert macros
当启用测试时,断言宏总是自动启用,不能禁用。
ASSERT
如果定义了此宏,则它将重写NOASSERT宏,从而强制断言宏始终处于启用状态,而不管其他设置如何。
NODEBUG
如果定义了此宏,则调试宏将没有任何效果。见Debugging macros
...NODEBUG
也意味着NOASSERT
,除非启用了测试。
DEBUG
如果定义了这个宏,它将重写NODEBUG宏,从而强制启用调试宏。
实用程序宏
以下宏可以使测试更加紧凑和可读性:
LET(Var,Arg,Expr)
创建本地绑定。Var = Arg在Expr.%28这与(fun(Var)->(Expr)end)(Arg).%29请注意绑定未导出在Expr,而在Expr,这个绑定Var的任何绑定Var在周围的范围内。
IF(Cond,TrueCase,FalseCase)
评价TrueCase如果Cond评估为true,或以其他方式评估FalseCase如果Cond评估为false.%28这与(case (Cond) of true->(TrueCase false->(FalseCase) end).%29请注意,如果Cond不会产生布尔值。
断言宏
%28请注意,这些宏也有相应的形式,以“_
“%28下划线%29字符,如?_assert(BoolExpr)
,这将创建一个“测试对象”,而不是立即执行测试。这相当于写?_test(assert(BoolExpr))
,等.%29
如果宏NOASSERT
在包含eUnit头文件之前定义,这些宏在也禁用测试时没有任何效果;请参见Compilation control macros
关于细节。
assert(BoolExpr)
计算表达式BoolExpr
,如果启用了测试。除非结果是true
,将生成一个信息丰富的异常。如果没有例外,则宏表达式的结果是原子。ok
的价值BoolExpr
被丢弃了。如果禁用测试,则宏将不会生成除原子之外的任何代码。ok
,和BoolExpr
不会被评估。
典型用法:
?assert(f(X, Y) =:= [])
大assert
宏可以在程序中的任何地方使用,而不仅仅是在单元测试中,以检查前后条件和不变量。例如:
some_recursive_function(X, Y, Z) ->
?assert(X + Y > Z),
...
assertNot(BoolExpr)
相当于assert(not (BoolExpr))
...
assertMatch(GuardedPattern, Expr)
评价Expr并将结果与GuardedPattern,如果启用了测试。如果匹配失败,将生成一个信息丰富的异常;请参见assert宏以获取更多详细信息。GuardedPattern控件的左手边可以写的任何东西。->CASE-子句中的符号,但它不能包含逗号分隔的保护测试。
使用的主要原因assertMatch
也适用于简单匹配,而不是与=
,它会产生更详细的错误消息。
例子:
?assertMatch{found, {fred, _}}, lookup(bloggs, Table))
?assertMatch([X|_] when X > 0, binary_to_list(B))
assertNotMatch(GuardedPattern, Expr)
为了方便起见,断言Match的反例。
assertEqual(Expect, Expr)
计算表达式Expect
和Expr
如果启用测试,则比较结果是否相等。如果值不相等,将生成一个信息丰富的异常;请参见assert
宏以获取更多详细信息。
assertEqual
比assertMatch
当左边是一个计算值而不是一个简单的模式时,并且给出更多的细节?assert(Expect =:= Expr)
...
例子:
?assertEqual("b" ++ "a", lists:reverse("ab"))
?assertEqual(foo(X), bar(Y))
assertNotEqual(Unexpected, Expr)
为了方便起见,与断言相等的情况相反。
assertException(ClassPattern, TermPattern, Expr)assertError(TermPattern, Expr)assertExit(TermPattern, Expr)assertThrow(TermPattern, Expr)
评价Expr
,捕捉任何异常并测试它是否与预期的ClassPattern:TermPattern
如果匹配失败,或者没有异常抛出Expr
,将生成一个信息丰富的异常;请参阅assert
宏以获取更多详细信息。大assertError
,,,assertExit
,和assertThrow
宏,等于使用assertException
带着ClassPattern
成error
,,,exit
,或throw
分别。
例子:
?assertError(badarith, X/0)
?assertExit(normal, exit(normal))
?assertException(throw, {not_found,_}, throw{not_found,42}))
运行外部命令的宏
请记住,外部命令高度依赖于操作系统。您可以使用标准库函数。os:type()
在测试生成器功能中,根据当前操作系统生成不同的测试集。
注意:如果在启用测试的情况下编译的话,这些宏将引入eUnit库代码的运行时依赖项。
assertCmd(CommandString)
跑CommandString
作为外部命令,如果启用了测试。除非返回的状态值为0,否则将生成一个信息丰富的异常。如果没有例外,则宏表达式的结果是原子。ok
如果禁用测试,则宏将不会生成除原子之外的任何代码。ok
,则不会执行该命令。
典型用法:
?assertCmd("mkdir foo")
assertCmdStatus(N, CommandString)
就像assertCmd(CommandString)
宏,但除非返回的状态值为N
...
assertCmdOutput(Text, CommandString)
跑CommandString
作为外部命令,如果启用了测试。除非命令生成的输出与指定的字符串完全匹配Text
,将生成一个信息丰富的异常。%28注意输出是标准化的,在所有平台上使用单个LF字符作为换行符。%29如果没有例外,则宏表达式的结果是原子。ok
如果禁用测试,则宏将不会生成除原子之外的任何代码。ok
,则不会执行该命令。
cmd(CommandString)
跑CommandString
作为外部命令。除非返回的状态值为0%28表示成功%29,否则将生成一个信息丰富的异常;否则,宏表达式的结果是命令生成的输出,作为平面字符串。输出被标准化为在所有平台上使用一个LF字符作为换行符。
此宏在安装和清理固定装置的部分非常有用,例如用于创建和删除文件或执行类似的操作系统特定任务,以确保测试系统被告知任何故障。
一个特定于Unix的示例:
{setup,
fun () -> ?cmd("mktemp") end,
fun (FileName) -> ?cmd("rm " ++ FileName) end,
...}
调试宏
为了帮助调试,eUnit定义了几个有用的宏,用于直接将消息打印到控制台%28,而不是输出到标准的输出%29。此外,这些宏都使用相同的基本格式,其中包括它们发生的文件和行号,使得在某些开发环境中(%28e)成为可能。当在Emacs缓冲区%29中运行Erlang时,只需单击消息并直接跳转到代码中的相应行。
如果宏NODEBUG
在包含eUnit头文件之前定义,这些宏没有任何效果;请参见Compilation control macros
关于细节。
debugHere
只需打印一个标记,显示当前文件和行号。请注意,这是一个非参数宏。结果总是ok
...
debugMsg(Text)
输出消息Text
%28,它可以是普通字符串、IO列表,也可以是原子%29。结果总是ok
...
debugFmt(FmtString, Args)
这使文本格式如下io:format(FmtString, Args)
并输出如下debugMsg
结果总是ok
...
debugVal(Expr)
打印两个源代码Expr
以及它的现值。例如,?debugVal(f(X))
可能会显示为“f(X) = 42
“.%28大项被截断为宏给定的深度EUNIT_DEBUG_VAL_DEPTH
,它默认为15,但可以被用户重写。%29结果始终是Expr
,因此可以将此宏封装在任何表达式周围,以便在已启用调试的情况下编译代码时显示其值。
debugVal(Expr, Depth)
就像debugVal(Expr)
,但是打印的项被截断到给定的深度。
debugTime(Text,Expr)
指纹Text
的评估用的挂钟时间。Expr
结果总是Expr
,因此可以将此宏包装在任何表达式周围,以显示在已启用调试的情况下编译代码时的运行时。例如,List1 = ?debugTime("sorting", lists:sort(List))
可能会显示为“sorting: 0.015 s
“
1.5 eUnit测试表示
eUnit将测试和测试集表示为数据的方式是灵活、强大和简洁的。本节详细描述了表示形式。
Simple test objects
Test sets and deep lists
Titles
Primitives
Control
Fixtures
Lazy generators
简单测试对象
阿简单测试对象
为下列之一:
- a髓功能值%28I。e.,一个零参数的乐趣%29。例子: 乐趣%28%29->...结束 找点乐子[医]函数/0 找点乐子[医]模块:一些[医]函数/0
- 元组
{test, ModuleName, FunctionName}
,在哪里ModuleName
和FunctionName
是原子,指的是函数ModuleName:FunctionName/0
- %28%29一对原子
{ModuleName, FunctionName}
,相当于{test, ModuleName, FunctionName}
如果没有其他匹配的话。这可能会在将来的版本中被删除。
- 一对
{LineNumber, SimpleTest}
,在哪里LineNumber
是一个非负整数,并且SimpleTest
是另一个简单的测试对象。LineNumber
应该指示测试的源行。像这样的对通常是通过?_test(...)
宏;见Basic macros
...
简单地说,一个简单的测试对象由一个函数组成,它不带参数%28--可能带有附加的元数据注释,即行号%29。对职能的评价成功
,通过返回忽略%29的值%28,或失败
抛出一个例外。
测试集和深度列表
通过将一系列测试对象放置在列表中,可以轻松地创建测试集。如果T_1
...T_N
是单个的测试对象,那么[T_1, ..., T_N]
是一个测试集,它由这些对象%28按该顺序%29组成。
测试集可以以相同的方式连接:如果S_1
...S_K
是测试集,那么[S_1, ..., S_K]
也是一个测试集,其中S_i
的命令S_(i+1)
,对于每个子集S_i
...
因此,测试集的主要表示形式是深表
,而一个简单的测试对象可以被视为只包含单个测试的测试集;T
和[T]
...
模块也可用于表示测试集;请参见ModuleName
下Primitives
下面。
标题
任何测试或测试集T
可以用标题进行注释,方法是将标题包装成一对。{Title, T}
,在哪里Title
是一根绳子。为了方便起见,通常使用元组表示的任何测试都可以简单地作为第一个元素(即写入)给出一个标题字符串。{"The Title", ...}
而不是添加额外的元组包装器,如{"The Title", {...}}
...
原语
以下是原语,它们不包含其他测试集作为参数:
ModuleName::atom()
一个原子表示一个模块名,相当于{module, ModuleName}
.这常用于呼叫eunit:test(some_module)
...
{module, ModuleName::atom()}
它由指定模块的导出测试函数组成一个测试集,即具有零的函数,其名称以_test
或_test_
基本上,..._test()
函数成为简单的测试,而..._test_()
功能变成发电机。
此外,eUnit还将查找另一个名为ModuleName
加上后缀_tests
,如果存在,则还将添加来自该模块的所有测试。%28ModuleName
已经包含后缀。_tests
,这还没有完成。%29,例如,规范。{module, mymodule}
将运行模块中的所有测试。mymodule
和mymodule_tests
.通常情况下,_tests
模块只应包含使用主模块%28的公共接口而不使用其他代码%29的测试用例。
{application, AppName::atom(), Info::list()}
这是一个普通的Erlang/OTP应用程序描述符,如.app
档案。生成的测试集由modules
进入Info
...
{application, AppName::atom()}
通过查阅应用程序%27,这将从属于指定应用程序的所有模块创建测试集。.app
文件%28见{file, FileName}
%29,或者如果不存在此类文件,则通过测试应用程序%27中的所有对象文件ebin
-目录%28{dir, Path}
%29;如果不存在,则code:lib_dir(AppName)
使用目录。
Path::string()
单个字符串表示文件或目录的路径,它等效于{file, Path}
,或{dir, Path}
分别取决于Path
在文件系统中引用。
{file, FileName::string()}
如果FileName
有一个后缀,表示对象文件%28.beam
%29,eUnit将尝试从指定的文件重新加载模块并对其进行测试。否则,该文件将被假定为包含测试规范的文本文件,该文本文件将使用标准库函数读取。file:path_consult/2
...
除非文件名是绝对的,否则首先相对于当前目录搜索文件,然后使用普通搜索路径%28code:get_path()
29%。这意味着典型的“app”文件的名称可以直接使用,不需要路径,例如,"mnesia.app"
...
{dir, Path::string()}
这将测试指定目录中的所有对象文件,就好像它们是使用{file, FileName}
...
{generator, GenFun::(() -> Tests)}
发生器函数GenFun
调用以生成测试集。
{generator, ModuleName::atom(), FunctionName::atom()}
功能ModuleName:FunctionName()
调用以生成测试集。
{with, X::any(), [AbstractTestFun::((any()) -> any())]}
分配值X把它们变成髓质测试功能。安AbstractTestFun就像一个普通的测试乐趣,但采取一个参数,而不是零-它%27基本上遗漏了一些信息,才能成为一个适当的测试。在实践中,{with, X, [F_1, ..., F_N]}等于[fun () -> F_1(X) end, ..., fun () -> F_N(X) end]如果您的抽象测试函数已经实现为适当的函数,则这一点尤其有用:{with, FD, [fun filetest_a/1, fun filetest_b/1, fun filetest_c/1]}等于[fun () -> filetest_a(FD) end, fun () -> filetest_b(FD) end, fun () -> filetest_c(FD) end],但要紧凑得多。另见Fixtures,在下面。
控制
以下表示控制测试的执行方式和地点:
{spawn, Tests}
在单独的子进程中运行指定的测试,而当前的测试进程则等待它完成。这对于需要新的、孤立的进程状态的测试非常有用。%28注意到eUnit总是自动启动至少一个这样的子进程;调用方%27自己的进程从不执行测试。%29
{spawn, Node::atom(), Tests}
就像{spawn, Tests}
,但是在给定的Erlang节点上运行指定的测试。
{timeout, Time::number(), Tests}
在给定的超时下运行指定的测试。时间以秒为单位;例如,60表示一分钟,0.1表示1/第十秒。如果超过了超时,未完成的测试将被迫终止。注意,如果在一个夹具周围设置了超时,它包括安装和清理的时间,如果触发超时,则整个夹具突然终止%28,而不运行清理%29。单个测试的默认超时为5秒。
{inorder, Tests}
以严格的顺序运行指定的测试。亦见{inparallel, Tests}
默认情况下,测试既不标记为inorder
或inparallel
,但是可以根据测试框架的选择来执行。
{inparallel, Tests}
如果可能的话,在%28中并行运行指定的测试。亦见{inorder, Tests}
...
{inparallel, N::integer(), Tests}
就像{inparallel, Tests}
,但跑得不超过N
同时进行分测验。
固定装置
“夹具”是运行特定测试集所必需的某种状态。eUnit%27s对固定装置的支持使在本地为测试集设置这种状态变得非常容易,并且在测试集完成时会自动将其再拆下来,而不管结果%28成功、失败、超时等等。
为了使描述更简单,我们首先列出一些定义:
Setup | () -> (R::any()) |
---|---|
SetupX | (X::any()) -> (R::any()) |
Cleanup | (R::any()) -> any() |
CleanupX | (X::any(), R::any()) -> any() |
Instantiator | ((R::any()) -> Tests) | {with, AbstractTestFun::((any()) -> any())} |
Where | local | spawn | {spawn, Node::atom()} |
%28下文将更详细地解释这些情况。
以下表示指定测试集的夹具处理:
{setup, Setup, Tests | Instantiator}{setup, Setup, Cleanup, Tests | Instantiator}{setup, Where, Setup, Tests | Instantiator}{setup, Where, Setup, Cleanup, Tests | Instantiator}
setup
设置一个用于运行所有指定测试的单个夹具,并在之后进行可选的删除。下文将详细说明这些论点。
{node, Node::atom(), Tests | Instantiator}{node, Node::atom(), Args::string(), Tests | Instantiator}
node
就像setup
,但是有一个内置行为:它在测试期间启动一个从节点。原子Node
应该有格式nodename@full.machine.name
,和Args
是新节点的可选参数;请参见slave:start_link/3
关于细节。
{foreach, Where, Setup, Cleanup, [Tests | Instantiator]}{foreach, Setup, Cleanup, [Tests | Instantiator]}{foreach, Where, Setup, [Tests | Instantiator]}{foreach, Setup, [Tests | Instantiator]}
foreach
用于设置一个夹具,然后可选择地将其拆下,并对每个指定的测试集重复使用。
{foreachx, Where, SetupX, CleanupX, Pairs::[{X::any(), ((X::any(), R::any()) -> Tests)}]}{foreachx, SetupX, CleanupX, Pairs}{foreachx, Where, SetupX, Pairs}{foreachx, SetupX, Pairs}
foreachx
就像foreach
,但是使用了一个对列表,每个对都包含一个额外的参数。X
和一个扩展的实例器函数。
阿Setup
函数在运行任何指定的测试之前执行,并且Cleanup
函数时,无论原因如何,都不会运行更多指定的测试。阿Setup
函数不带参数,并返回一些值,这些值将以它的形式传递给Cleanup
功能。阿Cleanup
函数应该执行任何必要的操作,并返回一些任意值,例如原子。ok
.%28SetupX
和CleanupX
函数是相似的,但是接收一个额外的参数:一些值X
,这取决于上下文.%29(当没有Cleanup
函数,则使用没有效果的虚拟函数。
安Instantiator
函数接收的值与Cleanup
函数返回的值。Setup
功能。然后,它的行为应该非常像生成器%28(见Primitives
%29,并返回测试集,该测试集的测试实例化
用给定的值。特例是语法。{with, [AbstractTestFun]}
,它表示一个实例器函数,它将值分布在一元函数的列表上;请参见Primitives
*{with, X, [...]}
更多细节。
阿Where
术语控制指定测试的执行方式。默认情况是spawn
,这意味着当前进程处理设置和删除,而测试则在子进程中执行。{spawn, Node}
就像spawn
,但是在指定的节点上运行子进程。local
意味着当前进程将同时处理安装/拆卸和运行测试--缺点是,如果测试超时,进程就会被终止。清理将不执行
因此,对于持久化的固定装置(如文件操作),请避免这样做。总的来说,local
只应在下列情况下使用:
- 安装/拆卸需要由将运行测试的进程执行;
- 如果进程被杀死%28i,则无需再执行拆卸操作。e.进程之外的状态不受安装%29的影响。
惰性发电机
有时,在测试开始之前不生成完整的测试描述是很方便的;例如,如果您想要生成大量的测试,这些测试占用了太多的空间来同时保存在内存中。
编写一个生成器相当容易,每次调用它时,它要么生成一个空列表(如果完成),要么生成一个包含单个测试用例的列表,再加上一个新的生成器,它将产生其余的测试。这说明了基本模式:
lazy_test_() ->
lazy_gen(10000).
lazy_gen(N) ->
{generator,
fun () ->
if N > 0 ->
[?_test(...)
| lazy_gen(N-1)];
true ->
[]
end
end}.
当eUnit为了运行测试而遍历测试表示时,在执行上一个测试之前,将不会调用新的生成器来生成下一个测试。
注意,使用帮助函数编写这种递归生成器是最容易的,如lazy_gen/1
以上功能。它也可以使用递归的乐趣来编写,如果您不想混淆函数名称空间,并且很适合编写这种代码的话。