动机
实现Cut(C++ Unified Test Framework)的动机,请参阅:无法忍受 Google Test 的 9 个特性
灵感
Cut(C++ Unified Test Framework)是一个简单的、可扩展的、使用C\\+\\+11实现的xUnit测试框架。Cut设计灵感来自于Java社区著名的测试框架JUnit。
安装
GitHub
- 地址:https://github.com/horance-liu/cut
编译环境
支持的平台:
- [MAC OS X] supported
- [Linux] supported
- [Windows] not supported
支持的编译器:
- [CLANG] 3.4 or later.
- [GCC] 4.8 or later.
- [MSVC] not supported.
安装CMake
CMake的下载地址:http://www.cmake.org。
安装Cut
$ git clone https://gitlab.com/horance-liu/cut.git
$ cd cut
$ mkdir build
$ cd build
$ cmake ..
$ make
$ sudo make install
测试Cut
$ cd cut/build
$ cmake -DENABLE_TEST=on ..
$ make
$ test/cut-test
破冰之旅
物理目录
quantity
├── include
│ └── quantity
├── src
│ └── quantity
└── test
│ ├── main.cpp
└── CMakeLists.txt
main函数
#include "cut/cut.hpp"
int main(int argc, char** argv)
{
return cut::run_all_tests(argc, argv);
}
CMakeList脚本
project(quantity)
cmake_minimum_required(VERSION 2.8)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++0x")
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include)
file(GLOB_RECURSE all_files
src/*.cpp
src/*.cc
src/*.c
test/*.cpp
test/*.cc
test/*.c)
add_executable(quantity-test ${all_files})
target_link_libraries(quantity-test cut)
构建
$ mkdir build
$ cd build
$ cmake ..
$ make
运行
$ ./quantity-test
[==========] Running 0 test cases.
[----------] 0 tests from All Tests
[----------] 0 tests from All Tests
[==========] 0 test cases ran.
[ TOTAL ] PASS: 0 FAILURE: 0 ERROR: 0 TIME: 0 us
体验Cut
第一个用例
#include
#include "quantity/Length.h"
USING_CUM_NS
FIXTURE(LengthTest)
{
TEST("1 FEET should equal to 12 INCH")
{
ASSERT_THAT(Length(1, FEET), eq(Length(12, INCH)));
}
};
使用 Cut,只需要包含 cut.hpp
一个头文件即可。Cut 使用 Hamcrest 的断言机制,
使得断言更加统一、自然,且具有良好的扩展性;使用 USING_CUM_NS
,从而可以使用 eq
代
替 cum::eq
,简短明确;除非出现名字冲突,否则推荐使用简写的 eq
。
Length实现
// quantity/Length.h
#include "quantity/Amount.h"
enum LengthUnit
{
INCH = 1,
FEET = 12 * INCH,
};
struct Length
{
Length(Amount amount, LengthUnit unit);
bool operator==(const Length& rhs) const;
bool operator!=(const Length& rhs) const;
private:
const Amount amountInBaseUnit;
};
// quantity/Length.cpp
#include "quantity/Length.h"
Length::Length(Amount amount, LengthUnit unit)
: amountInBaseUnit(unit * amount)
{
}
bool Length::operator==(const Length& rhs) const
{
return amountInBaseUnit == rhs.amountInBaseUnit;
}
bool Length::operator!=(const Length& rhs) const
{
return !(*this == rhs);
}
构建
$ mkdir build
$ cd build
$ cmake ..
$ make
运行
$ ./quantity-test
[==========] Running 1 test cases.
[----------] 1 tests from All Tests
[----------] 1 tests from LengthTest
[ RUN ] LengthTest::1 FEET should equal to 12 INCH
[ OK ] LengthTest::1 FEET should equal to 12 INCH(13 us)
[----------] 1 tests from LengthTest
[----------] 1 tests from All Tests
[==========] 1 test cases ran.
[ TOTAL ] PASS: 1 FAILURE: 0 ERROR: 0 TIME: 13 us
Fixture
FIXTURE的参数可以是任意的C\\+\\+标识符。一般而言,将其命名为CUT(Class Under Test)的名字即可。根据作用域的大小,Fixture可分为三个类别:独立的Fixture,共享的Fixture,全局的Fixture。
支持BDD风格
xUnit | BDD |
---|---|
FIXTURE | CONTEXT |
SETUP | BEFORE |
TEARDOWN | AFTER |
ASSERT_THAT | EXPECT |
独立的Fixture
#include
FIXTURE(LengthTest)
{
Length length;
SETUP()
{}
TEARDOWN()
{}
TEST("length test1")
{}
TEST("length test2")
{}
};
执行序列为:
-
Length
构造函数 SETUP
TEST("length test1")
TEARDOWN
-
Length
析构函数 -
Length
构造函数 SETUP
TEST("length test2")
TEARDOWN
-
Length
析构函数
共享的Fixture
#include
FIXTURE(LengthTest)
{
Length length;
BEFORE_CLASS()
{}
AFTER_CLASS()
{}
BEFORE()
{}
AFTER()
{}
TEST("length test1")
{}
TEST("length test2")
{}
};
执行序列为:
BEFORE_CLASS
-
Length
构造函数 BEFORE
TEST("length test1")
AFTER
-
Length
析构函数 -
Length
构造函数 BEFORE
TEST("length test2")
AFTER
-
Length
析构函数 AFTER_CLASS
全局的Fixture
有时候需要在所有用例启动之前完成一次性的全局性的配置,在所有用例运行完成之后完成一次性的清理工作。Cut则使用BEFORE_ALL
和AFTER_ALL
两个关键字来支持这样的特性。
#include
BEFORE_ALL("before all 1")
{
}
BEFORE_ALL("before all 2")
{
}
AFTER_ALL("after all 1")
{
}
AFTER_ALL("after all 2")
{
}
BEFORE_ALL
和AFTER_ALL
向系统注册Hook
即可,Cut便能自动地发现它们,并执行它们。犹如C\\+\\+不能保证各源文件中全局变量初始化的顺序一样,避免在源文件之间的BEFORE_ALL
和AFTER_ALL
设计不合理的依赖关系。
#include
FIXTURE(LengthTest)
{
Length length;
BEFORE_CLASS()
{}
AFTER_CLASS()
{}
BEFORE()
{}
AFTER()
{}
TEST("length test1")
{}
TEST("length test2")
{}
};
#include
FIXTURE(VolumeTest)
{
Volume volume;
BEFORE_CLASS()
{}
AFTER_CLASS()
{}
BEFORE()
{}
AFTER()
{}
TEST("volume test1")
{}
TEST("volume test1")
{}
};
Cut可能的一个执行序列为:
BEFORE_ALL("before all 1")
BEFORE_ALL("before all 2")
LengthTest::BEFORE_CLASS
-
Length
构造函数 LengthTest::BEFORE
TEST("length test1")
LengthTest::AFTER
-
Length
析构函数 -
Length
构造函数 LengthTest::BEFORE
TEST("length test2")
LengthTest::AFTER
-
Length
析构函数 LengthTest::AFTER_CLASS
VolumeTest::BEFORE_CLASS
-
Volume
构造函数 LengthTest::BEFORE
TEST("volume test1")
LengthTest::AFTER
-
Volume
析构函数 -
Volume
构造函数 LengthTest::BEFORE
TEST("volume test2")
LengthTest::AFTER
-
Volume
析构函数 VolumeTest::AFTER_CLASS
AFTER_ALL("after all 2")
AFTER_ALL("after all 1")
用例设计
自动标识
Cut能够自动地实现测试用例的标识功能,用户可以使用字符串来解释说明测试用例的意图,使得用户在描述用例时更加自然和方便。
#include
#include "quantity/length/Length.h"
USING_CUM_NS
FIXTURE(LengthTest)
{
TEST("1 FEET should equal to 12 INCH")
{
ASSERT_THAT(Length(1, FEET), eq(Length(12, INCH)));
}
TEST("1 YARD should equal to 3 FEET")
{
ASSERT_THAT(Length(1, YARD), eq(Length(3, FEET)));
}
TEST("1 MILE should equal to 1760 YARD")
{
ASSERT_THAT(Length(1, MILE), eq(Length(1760, YARD)));
}
};
面向对象
Cut实现xUnit时非常巧妙,使得用户设计用例时更加面向对象。RobotCleaner robot
在每个用例执行时都将获取一个独立的、全新的实例。
#include "cut/cut.hpp"
#include "robot-cleaner/RobotCleaner.h"
#include "robot-cleaner/Position.h"
#include "robot-cleaner/Instructions.h"
USING_CUM_NS
FIXTURE(RobotCleanerTest)
{
RobotCleaner robot;
TEST("at the beginning, the robot should be in at the initial position")
{
ASSERT_THAT(robot.getPosition(), is(Position(0, 0, NORTH)));
}
TEST("left instruction: 1-times")
{
robot.exec(left());
ASSERT_THAT(robot.getPosition(), is(Position(0, 0, WEST)));
}
TEST("left instruction: 2-times")
{
robot.exec(left());
robot.exec(left());
ASSERT_THAT(robot.getPosition(), is(Position(0, 0, SOUTH)));
}
};
函数提取
提取的相关子函数,可以直接放在Fixture
的内部,使得用例与其的距离最近,更加体现类作用域的概念。
#include "cut/cut.hpp"
#include "robot-cleaner/RobotCleaner.h"
#include "robot-cleaner/Position.h"
#include "robot-cleaner/Instructions.h"
USING_CUM_NS
FIXTURE(RobotCleanerTest)
{
RobotCleaner robot;
void WHEN_I_send_instruction(Instruction* instruction)
{
robot.exec(instruction);
}
void AND_I_send_instruction(Instruction* instruction)
{
WHEN_I_send_instruction(instruction);
}
void THEN_the_robot_cleaner_should_be_in(const Position& position)
{
ASSERT_THAT(robot.getPosition(), is(position));
}
TEST("at the beginning")
{
THEN_the_robot_cleaner_should_be_in(Position(0, 0, NORTH));
}
TEST("left instruction: 1-times")
{
WHEN_I_send_instruction(left());
THEN_the_robot_cleaner_should_be_in(Position(0, 0, WEST));
}
TEST("left instruction: 2-times")
{
WHEN_I_send_instruction(repeat(left(), 2));
THEN_the_robot_cleaner_should_be_in(Position(0, 0, SOUTH));
}
TEST("left instruction: 3-times")
{
WHEN_I_send_instruction(repeat(left(), 3));
THEN_the_robot_cleaner_should_be_in(Position(0, 0, EAST));
}
TEST("left instruction: 4-times")
{
WHEN_I_send_instruction(repeat(left(), 4));
THEN_the_robot_cleaner_should_be_in(Position(0, 0, NORTH));
}
};
断言
ASSERT_THAT
Cut只支持一种断言原语:ASSERT_THAT
, 从而避免用户在选择ASSERT_EQ/ASSERT_NE, ASSERT_TRUE/ASSERT_FALSE
时的困扰,使其断言更加具有统一性,一致性。
此外,ASSERT_THAT
使得断言更加具有表达力,它将实际值放在左边,期望值放在右边,更加符合英语习惯。
#include
FIXTURE(CloseToTest)
{
TEST("close to double")
{
ASSERT_THAT(1.0, close_to(1.0, 0.5));
ASSERT_THAT(0.5, close_to(1.0, 0.5));
ASSERT_THAT(1.5, close_to(1.0, 0.5));
}
};
Hamcrest
Hamcrest是Java社区一个轻量级的,可扩展的Matcher框架,曾被Kent Beck引入到JUnit框架中,用于增强断言的机制。Cut引入了Hamcrest的设计,实现了一个C\\+\\+移植版本的Hamcrest,使得Cut的断言更加具有扩展性和可读性。
结构
anything
匹配器 | 说明 |
---|---|
anything | 总是匹配 |
_ | anything语法糖 |
#include
USING_CUM_NS
FIXTURE(AnythingTest)
{
TEST("should always be matched")
{
ASSERT_THAT(1, anything());
ASSERT_THAT(1u, anything());
ASSERT_THAT(1.0, anything());
ASSERT_THAT(1.0f, anything());
ASSERT_THAT(false, anything());
ASSERT_THAT(true, anything());
ASSERT_THAT(nullptr, anything());
}
TEST("should support _ as syntactic sugar")
{
ASSERT_THAT(1u, _(int));
ASSERT_THAT(1.0f, _(float));
ASSERT_THAT(false, _(int));
ASSERT_THAT(nullptr, _(std::nullptr_t));
}
};
比较器
匹配器 | 说明 |
---|---|
eq | 相等 |
ne | 不相等 |
lt | 小于 |
gt | 大于 |
le | 小于或等于 |
ge | 大于或等于 |
#include
USING_CUM_NS
FIXTURE(EqualToTest)
{
TEST("should allow compare to integer")
{
ASSERT_THAT(0xFF, eq(0xFF));
ASSERT_THAT(0xFF, is(eq(0xFF)));
ASSERT_THAT(0xFF, is(0xFF));
ASSERT_THAT(0xFF == 0xFF, is(true));
}
TEST("should allow compare to bool")
{
ASSERT_THAT(true, eq(true));
ASSERT_THAT(false, eq(false));
}
TEST("should allow compare to string")
{
ASSERT_THAT("hello", eq("hello"));
ASSERT_THAT("hello", eq(std::string("hello")));
ASSERT_THAT(std::string("hello"), eq(std::string("hello")));
}
};
FIXTURE(NotEqualToTest)
{
TEST("should allow compare to integer")
{
ASSERT_THAT(0xFF, ne(0xEE));
ASSERT_THAT(0xFF, is_not(0xEE));
ASSERT_THAT(0xFF, is_not(eq(0xEE)));
ASSERT_THAT(0xFF != 0xEE, is(true));
}
TEST("should allow compare to boolean")
{
ASSERT_THAT(true, ne(false));
ASSERT_THAT(false, ne(true));
}
TEST("should allow compare to string")
{
ASSERT_THAT("hello", ne("world"));
ASSERT_THAT("hello", ne(std::string("world")));
ASSERT_THAT(std::string("hello"), ne(std::string("world")));
}
};
修饰器
匹配器 | 说明 |
---|---|
is | 可读性装饰器 |
is_not | 可读性装饰器 |
#include
USING_CUM_NS
FIXTURE(IsNotTest)
{
TEST("integer")
{
ASSERT_THAT(0xFF, is_not(0xEE));
ASSERT_THAT(0xFF, is_not(eq(0xEE)));
}
TEST("string")
{
ASSERT_THAT("hello", is_not("world"));
ASSERT_THAT("hello", is_not(eq("world")));
ASSERT_THAT("hello", is_not(std::string("world")));
ASSERT_THAT(std::string("hello"), is_not(std::string("world")));
}
};
空指针
匹配器 | 说明 |
---|---|
nil | 空指针 |
#include
USING_CUM_NS
FIXTURE(NilTest)
{
TEST("equal_to")
{
ASSERT_THAT(nullptr, eq(nullptr));
ASSERT_THAT(0, eq(NULL));
ASSERT_THAT(NULL, eq(NULL));
ASSERT_THAT(NULL, eq(0));
}
TEST("is")
{
ASSERT_THAT(nullptr, is(nullptr));
ASSERT_THAT(nullptr, is(eq(nullptr)));
ASSERT_THAT(0, is(0));
ASSERT_THAT(NULL, is(NULL));
ASSERT_THAT(0, is(NULL));
ASSERT_THAT(NULL, is(0));
}
TEST("nil")
{
ASSERT_THAT((void*)NULL, nil());
ASSERT_THAT((void*)0, nil());
ASSERT_THAT(nullptr, nil());
}
};
字符串
匹配器 | 说明 |
---|---|
contains_string | 断言是否包含子串 |
contains_string_ignoring_case | 忽略大小写,断言是否包含子 |
starts_with | 断言是否以该子串开头 |
starts_with_ignoring_case | 忽略大小写,断言是否以该子串开头 |
ends_with | 断言是否以该子串结尾 |
ends_with_ignoring_case | 忽略大小写,断言是否以该子串结尾 |
#include
USING_CUM_NS
FIXTURE(StartsWithTest)
{
TEST("case sensitive")
{
ASSERT_THAT("ruby-cpp", starts_with("ruby"));
ASSERT_THAT("ruby-cpp", is(starts_with("ruby")));
ASSERT_THAT(std::string("ruby-cpp"), starts_with("ruby"));
ASSERT_THAT("ruby-cpp", starts_with(std::string("ruby")));
ASSERT_THAT(std::string("ruby-cpp"), starts_with(std::string("ruby")));
}
TEST("ignoring case")
{
ASSERT_THAT("ruby-cpp", starts_with_ignoring_case("Ruby"));
ASSERT_THAT("ruby-cpp", is(starts_with_ignoring_case("Ruby")));
ASSERT_THAT(std::string("ruby-cpp"), starts_with_ignoring_case("RUBY"));
ASSERT_THAT("Ruby-Cpp", starts_with_ignoring_case(std::string("rUBY")));
ASSERT_THAT(std::string("RUBY-CPP"), starts_with_ignoring_case(std::string("ruby")));
}
};
浮点数
匹配器 | 说明 |
---|---|
close_to | 断言浮点数近似等于 |
nan | 断言浮点数不是一个数字 |
#include
#include
USING_CUM_NS
FIXTURE(IsNanTest)
{
TEST("double")
{
ASSERT_THAT(sqrt(-1.0), nan());
ASSERT_THAT(sqrt(-1.0), is(nan()));
ASSERT_THAT(1.0/0.0, is_not(nan()));
ASSERT_THAT(-1.0/0.0, is_not(nan()));
}
};
程序选项
TestOptions::TestOptions() : desc("cut")
{
desc.add({
{"help, h", "help message"},
{"filter, f", "--filter=pattern"},
{"color, c", "--color=[yes|no]"},
{"xml, x", "print test result into XML file"},
{"list, l", "list all tests without running them"},
{"progress, p", "print test result in progress bar"},
{"verbose, v", "verbosely list tests processed"},
{"repeat, r", "how many times to repeat each test"}
});
// default value
options["color"] = "yes";
options["repeat"] = "1";
}
设计与实现
核心领域
Cut整体的结构其实是一棵树,用于用例的组织和管理。
struct TestResult;
DEFINE_ROLE(Test)
{
ABSTRACT(const std::string& getName () const);
ABSTRACT(int countTestCases() const);
ABSTRACT(int countChildTests() const);
ABSTRACT(void run(TestResult&));
};
适配
如何让FIXTURE
中一个普通的成员函数TEST
在运行时表现为一个TestCase
呢?在C++
的实现中,似乎变得非常困难。Cut
的设计非常简单,将TEST
的元信息在编译时注册到框架,简单地使用了C++
元编程的技术,及其C++11
的一些特性保证,从而解决了C++
社区一直未解决此问题的关键。
TEST
的运行时信息由TestMethod
的概念表示,其代表FIXTURE
中一个普通的成员函数TEST
,它们都具有同样的函数原型: void Fixture::*)()
; TestMethod
是一个泛型类,泛型参数是Fixture
;形式化地描述为:
template
struct TestMethod
{
using Method = void(Fixture::*)();
};
TestCaller
也是一个泛型类,它将一个TestMethod
适配为一个普通的TestCase
。
template
struct TestCaller : TestCase
{
using Method = void(Fixture::*)();
TestCaller(const std::string& name, Method method)
: TestCase(name), fixture(0), method(method)
{}
private:
OVERRIDE(void setUp())
{
fixture = new Fixture;
fixture->setUp();
}
OVERRIDE(void tearDown())
{
fixture->tearDown();
delete fixture;
fixture = 0;
}
OVERRIDE(void runTest())
{
(fixture->*method)();
}
private:
Fixture* fixture;
Method method;
};
装饰
TestDecorator
其实是对Cut
核心领域的一个扩展,从而保证核心领域的不变性,而使其具有最大的可扩展性和灵活性。
工厂
在编译时通过测试用例TEST的元信息的注册,使用TestFactory
很自然地将这些用例自动生成出来了。因为Magallan
组织用例是一刻树,TestFactory
也被设计为一棵树,从而使得其与框架核心领域保持高度的一致性,更加自然、漂亮。
监听状态
Cut
通过TestListener
对运行时的状态变化进行监控,从而实现了Cut
不同格式报表打印的变化。