第三章 单元测试的工具——测试框架
3.1 常用的单元测试框架
工欲善其事, 必先利其器. 程序员在写单元测试代码时, 如果能借助一些单元测试框架,
那么使单元测试代码的书写、维护、分类、存档、运行和结果检查变得更为容易, 从而成倍地提高工作效率. 在本章中,
我们就将学习两种单元测试的框架.
第一种, 就是鼎鼎大名的xUnit测试框架家族. xUnit测试框架有助于我们更加结构化地书写测试代码,
更方便地运行单元测试并检查运行结果.
第二种框架称为隔离框架(isolation framework). 这种框架旨在帮助程序员自动地生成FakeCollaboratorClass代码.
但是隔离框架一般都要求ClassUnderTest依赖于CollaboratorService抽象接口,
而不是具体类CollaboratorClass, 所以在使用"Virtual and Override"手法时,
隔离框架就帮不忙了, 必须由程序员手写FakeCollaboratorClass的代码.
在接下来的几个小节中, 我们将针对C++, C#和Java语言, 分别介绍适用于它们的单元测试框架.
3.2 用于C++的单元测试框架
对于C++, 我们推荐使用Google C++ Unit Testing
Framework(简称gTest, 目前为1.5版)加HippoMocks(目前为3.1版)的组合.
如下的表格演示了如何使用这两个框架.
gTest的基本用法 |
|
声明Test
Fixture |
#ifndef
CLASS_UNDER_TEST_TEST_H
#define CLASS_UNDER_TEST_TEST_H
// In [ClassUnderTest]Test.h file.
#include <gtest/gtest.h>
// Test Fixture declaration.
class [ClassUnderTest]Test : public
testing::Test
{
protected:
// Optional SetUp() and TearDown().
virtual
void SetUp();
virtual void TearDown();
};
#endif |
定义Test
Method |
//
In [ClassUnderTest]Test.cpp file.
#include <gtest/gtest.h>
#include <hippomocks.h>
#include "[ClassUnderTest]Test.h"
// Optional SetUp() and TearDown().
void [ClassUnderTest]Test::SetUp()
{
...
}
void [ClassUnderTest]Test::TearDown()
{
...
}
// Test Method definition.
TEST_F([ClassUnderTest]Test, [Feature]_[Scenario]_[ExpectedBehavior])
{
...
} //
Temporarily ignore a test case.
TEST_F([ClassUnderTest]Test,
DISABLE_[Feature]_[Scenario]_[ExpectedBehavior])
{
...
} |
Test
Runner |
//
Currently, gTest only supports CUI.
The main() should be like this.
#include <gtest/gtest.h>
#include "[ClassUnderTest]Test.h"
int main(int argc, char** argv)
{ testing::InitGoogleTest(&argc,
argv); return
RUN_ALL_TESTS();
} |
gTest支持的命令行参数 |
|
--gtest-filter=[filter] |
对执行的测试案例进行过滤,支持
? 单个字符
* 任意字符
- 排除,如,-a 表示除了a
: 取或,如,a:b 表示a或b
比如下面的例子:
./foo_test 没有指定过滤条件, 运行所有案例
./foo_test --gtest_filter=* 使用通配符*,
表示运行所有案例
./foo_test --gtest_filter=FooTest.*
运行所有[ClassUnderTest]Test为FooTest的案例
./foo_test --gtest_filter=FooTest.*-FooTest.Bar
运行所有[ClassUnderTest]Test为FooTest的案例,
但是除了FooTest.Bar这个案例 |
--gtest_repeat=[COUNT] |
设置案例重复运行次数.
比如:
--gtest_repeat=1000
重复执行1000次, 即使中途出现错误.
--gtest_repeat=1000 --gtest_break_on_failure
重复执行1000次, 并且在第一个错误发生时立即停止. 这个功能对调试非常有用. |
--gtest_print_time |
打印每个测试方法运行的所用时间. |
--gtest_catch_exceptions |
捕捉异常.
这样当测试方法中抛出了异常时, 不会阻碍了后续测试方法的运行.
注意: 这个参数只在Windows下有效. |
--gtest_output=xml[:DIRECTORY_PATH/|:FILE_PATH] |
产生XML格式的报告。 |
gTest支持的断言 |
|
断言真伪 |
ASSERT_TRUE(condition)
[<< message];
ASSERT_FALSE(condition) [<<
message]; |
断言比较关系 |
ASSERT_EQ(expected,
actual) [<< message];
ASSERT_NE(expected, actual) [<<
message];
ASSERT_LT(expected, actual) [<<
message];
ASSERT_LE(expected, actual) [<<
message];
ASSERT_GT(expected, actual) [<<
message];
ASSERT_GE(expected, actual) [<<
message];
ASSERT_FLOAT_EQ(expected, actual)
[<< message];
ASSERT_DOUBLE_EQ(expected, actual)
[<< message];
ASSERT_NEAR(expected, actual, tolerance)
[<< message]; |
断言C字符串关系 |
ASSERT_STREQ(expected_cstr,
actual_cstr) [<< message];
ASSERT_STRNE(expected_cstr, actual_cstr)
[<< message];
ASSERT_STRCASEEQ(expected_cstr,
actual_cstr) [<< message];
ASSERT_STRCASENE(expected_cstr,
actual_cstr) [<< message]; |
断言异常 |
ASSERT_THROW({
statements }, exception_ctor);
ASSERT_NO_THROW({ statements });
ASSERT_ANY_THROW({ statements }); |
HippoMocks用于动态stub生成 |
|
准备工作 |
//
In [ClassUnderTest]Test.cpp file.
#include <gtest/gtest.h>
#include <hippomocks.h>
#include "[ClassUnderTest]Test.h"
// Test Method definition.
TEST_F([ClassUnderTest]Test, [Feature]_[Scenario]_[ExpectedBehavior])
{ MockRepository
mockEngine;
CollaboratorService* stub = mockEngine.InterfaceMock<CollaboratorService>();
...
} |
指定返回值 |
//
We don't care about the passed-in
arguments for CollaboratorService::methodName().
mockEngine.OnCall(stub, CollaboratorService::methodName).Return(stubResult);
// We care about the passed-in arguments
for CollaboratorService::methodName().
mockEngine.OnCall(stub, CollaboratorService::methodName).With(arg1,
arg2, ...).Return(stubResult);
// If CollaboratorService::methodName()
is overloaded, use the following
invocation forms.
mockEngine.OnCallOverload(stub,
(return_type(CollaboratorService::*)(arg1_type,
arg2_type, ...))&CollaboratorService::methodName).Return(stubResult);
mockEngine.OnCallOverload(stub,
(return_type(CollaboratorService::*)(arg1_type,
arg2_type, ...))&CollaboratorService::methodName).With(arg1,
arg2, ...).Return(stubResult); |
指定抛出异常 |
//
We don't care about the passed-in
arguments for CollaboratorService::methodName().
mockEngine.OnCall(stub, CollaboratorService::methodName).Throw(exception_ctor());
// We care about the passed-in arguments
for CollaboratorService::methodName().
mockEngine.OnCall(stub, CollaboratorService::methodName).With(arg1,
arg2, ...).Throw(exception_ctor());
// If CollaboratorService::methodName()
is overloaded, use the following
invocation forms.
mockEngine.OnCallOverload(stub,
(return_type(CollaboratorService::*)(arg1_type,
arg2_type, ...))&CollaboratorService::methodName).Throw(exception_ctor());
mockEngine.OnCallOverload(stub,
(return_type(CollaboratorService::*)(arg1_type,
arg2_type, ...))&CollaboratorService::methodName).With(arg1,
arg2, ...).Throw(exception_ctor());
|
HippoMocks用于动态mock生成 |
|
准备工作 |
//
In [ClassUnderTest]Test.cpp file.
#include <gtest/gtest.h>
#include <hippomocks.h>
#include "[ClassUnderTest]Test.h"
// Test Method definition.
TEST_F([ClassUnderTest]Test, [Feature]_[Scenario]_[ExpectedBehavior])
{ MockRepository
mockEngine;
CollaboratorService* mock = mockEngine.InterfaceMock<CollaboratorService>();
...
} |
设置期望:
一定不被调用 |
mockEngine.NeverCall(mock,
CollaboratorService::methodName);
mockEngine.NeverCallOverload(mock,
(return_type(CollaboratorService::*)(arg1_type,
arg2_type, ...))&CollaboratorService::methodName);
|
设置期望:
不一定被调用 |
mockEngine.OnCall(mock,
CollaboratorService::methodName);
mockEngine.OnCallOverload(mock,
(return_type(CollaboratorService::*)(arg1_type,
arg2_type, ...))&CollaboratorService::methodName); |
设置期望:
不一定被调用, 但如果一旦被调, 那么关心输入参数 |
mockEngine.OnCall(mock,
CollaboratorService::methodName).With(arg1,
arg2, ...);
mockEngine.OnCallOverload(mock,
(return_type(CollaboratorService::*)(arg1_type,
arg2_type, ...))&CollaboratorService::methodName).With(arg1,
arg2, ...); |
设置期望:
一定被调用, 但不关心输入参数 |
mockEngine.ExpectCall(mock,
CollaboratorService::methodName);
mockEngine.ExpectCallOverload(mock,
(return_type(CollaboratorService::*)(arg1_type,
arg2_type, ...))&CollaboratorService::methodName); |
设置期望:
一定被调用, 并关心输入参数 |
mockEngine.ExpectCall(mock,
CollaboratorService::methodName).With(arg1,
arg2, ...);
mockEngine.ExpectCallOverload(mock,
(return_type(CollaboratorService::*)(arg1_type,
arg2_type, ...))&CollaboratorService::methodName).With(arg1,
arg2, ...); |
设置期望:
一定被调用, 但不关心被调用的次序 |
mockEngine.autoExpect
= false;
mockEngine.ExpectCall(mock, CollaboratorService::methodName1);
mockEngine.ExpectCall(mock, CollaboratorService::methodName2); |
设置期望:
一定被调用, 并关心被调用的次序 |
mockEngine.ExpectCall(mock,
CollaboratorService::methodName1);
mockEngine.ExpectCall(mock, CollaboratorService::methodName2); |
显式验证 |
mockEngine.VerifyAll(); |
3.3 用于C#的单元测试框架
对于C#, 我们推荐使用NUnit(目前为2.5版)加RhinoMocks(目前为3.6版)的组合.
如下的表格演示了如何使用这两个框架.
NUnit的基本用法 |
|
Test Fixture和Test Method |
using NUnit.Framework;
using Rhino.Mocks;
[TestFixture]
public class [ClassUnderTest]Test
{ [SetUp]
public void
SetUp() {
... }
[TearDown]
public void TearDown()
{
... }
[Test]
public void [Feature]_[Scenario]_[ExpectedResult]()
{
... } //
Temporarily ignore a test case.
[Test]
[Ignore]
public void [Feature]_[Scenario]_[ExpectedResult]()
{
...
}
} |
Test Runner |
NUnit提供了GUI界面的Test
Runner, 只需把编译生成的程序集(assembly)加载到该Test
Runner中即可. |
NUnit支持的断言 |
|
断言真伪 |
Assert.That(condition,
Is.True);
Assert.That(condition, Is.False); |
断言比较关系 |
Assert.That(actual,
Is[.Not].EqualTo(expected));
Assert.That(actual. Is.LessThan(expected));
Assert.That(actual. Is.GreaterThan(expected));
Assert.That(actual. Is.AtMost(expected));
Assert.That(actual. Is.AtLeast(expected));
Assert.That(actual. Is.LessThan(expected));
Assert.That(actual. Is.EqualTo(expected).Within(tolerance)); |
断言对象关系 |
Assert.That(obj, Is[.Not].Null);
Assert.That(obj, Is[.Not].InstanceOfType(type));
Assert.That(actualObj, Is[.Not].SameAs(expectedObj)); |
断言容器关系 |
Assert.That(container,
Is[.Not].Empty);
Assert.That(container, Has.Length(length)); |
断言字符串关系 |
Assert.That(actual,
Is[.Not].EqualTo(expected)[.IgnoreCase]);
Assert.That(phrase, Is[.Not].StringContaining(substring)[.IgnoreCase]);
Assert.That(phrase, StartsWith(substring)[.IgnoreCase]);
Assert.That(phrase, EndsWith(substring)[.IgnoreCase]); |
断言异常 |
Assert.That(() =>
{ statements }, Throws.Exception.TypeOf<exception_ctor>());
Assert.That(() => { statements
}, Throws.Exception);
Assert.That(() => { statements
}, Throws.Nothing; |
RhinoMocks用于动态stub生成 |
|
准备工作 |
using NUnit.Framework;
using Rhino.Mocks;
[TestFixture]
public class [ClassUnderTest]Test
{ [Test]
public void [Feature]_[Scenario]_[ExpectedResult]()
{
MockRepository mockEngine = new MockRepository();
CollaboratorService stub = mockEngine.Stub<CollaboratorService>();
...
mockEngine.ReplayAll();
... }
} |
指定返回值 |
// We don't care about
the passed-in arguments for CollaboratorService::methodName().
Expect.Call(stub.methodName(dummy1,
dummy2, dummy3, ...)).Return(stubResult);
// We care about the passed-in arguments
for CollaboratorService::methodName().
Expect.Call(stub.methodName(dummy1,
dummy2, dummy3, ...)).
Constraints(Rhino.Mocks.Constraints.Is.Equal(arg1),
Rhino.Mocks.Constraints.Is.Null(),
Rhino.Mocks.Constraints.Is.Same(arg3),
...). Return(stubResult); |
指定抛出异常 |
// We don't care about
the passed-in arguments for CollaboratorService::methodName().
Expect.Call(stub.methodName(dummy1,
dummy2, dummy3, ...)).
Constraints(Rhino.Mocks.Constraints.Is.Anything(),
Rhino.Mocks.Constraints.Is.Anything(),
Rhino.Mocks.Constraints.Is.Anything(),
...). Throw(new
exception_ctor());
// We care about the passed-in arguments
for CollaboratorService::methodName().
Expect.Call(stub.methodName(arg1,
arg2, arg3, ...)).Throw(new exception_ctor()); |
RhinoMocks用于动态mock生成 |
|
准备工作 |
using
NUnit.Framework;
using Rhino.Mocks;
[Test]
public void [Feature]_[Scenario]_[ExpectedResult]()
{ MockRepository
mockEngine = new MockRepository();
CollaboratorService
mock = mockEngine.DynamicMock<CollaboratorService>();
...
mockEngine.ReplayAll();
...
} |
设置期望:
一定不被调用 |
Expect.Call(()
=> { mock.methodName(dummy1, dummy2,
...); }).Repeat.Never(); |
设置期望:
不一定被调用 |
Expect.Call(()
=> { mock.methodName(dummy1, dummy2,
...); }).Repeat.Any(); |
设置期望:
一定被调用, 但不关心输入参数 |
Expect.Call(()
=> { mock.methodName(dummy1, dummy2,
...); }).
Constraints(Rhino.Mocks.Constraints.Is.Anything(),
Rhino.Mocks.Constraints.Is.Anything(),
...); |
设置期望:
一定被调用, 并关心输入参数 |
Expect.Call(()
=> { mock.methodName(arg1, arg2,
...); }); |
设置期望:
一定被调用, 但不关心调用次序 |
Expect.Call(()
=> { mock.methodName1(arg1, arg2,
...); });
Expect.Call(() => { mock.methodName2(arg1,
arg2, ...); }); |
设置期望:
一定被调用, 并关心调用次序 |
using
(mockEngine.Ordered())
{ Expect.Call(()
=> { mock.methodName1(arg1, arg2,
...); });
Expect.Call(() => { mock.methodName2(arg1,
arg2, ...); });
} |
显式验证 |
mockEngine.VerifyAll(); |
|