Magellan是一个简单的、可扩展的、使用C\+\+11实现的xUnit测试框架。Magellan设计灵感来自于Java社区著名的测试框架JUnit。c++
地址:https://github.com/horance/magellan
做者:刘光聪
Email:horance@outlook.comgit
支持的平台:github
[MAC OS X] supported编程
[Linux] supportedruby
[Windows] not supportedbash
支持的编译器:框架
[CLANG] 3.4 or later.函数
[GCC] 4.8 or later.gitlab
[MSVC] not supported.测试
CMake的下载地址:http://www.cmake.org。
$ git clone https://gitlab.com/horance/magellan.git $ cd magellan $ git submodule init $ git submodule update $ mkdir build $ cd build $ cmake .. $ make $ sudo make install
$ cd magellan/build $ cmake -DENABLE_TEST=on .. $ make $ test/magellan-test $ lib/l0-infra/l0-infra-test $ lib/hamcrest/hamcrest-test
使用Rake可简化Magelan的构建和测试过程,而且使得Magellan自我测试变成可能。
$ rake # build, install, and test using clang $ rake clang # build, install, and test using clang $ rake gcc # build, install, and test using gcc $ rake clean # remove temp directory, and uninstall magellan $ rake uninstall # uninstall magellan only
quantity ├── include │ └── quantity ├── src │ └── quantity └── test │ ├── main.cpp └── CMakeLists.txt
#include "magellan/magellan.hpp" int main(int argc, char** argv) { return magellan::run_all_tests(argc, argv); }
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 magellan hamcrest l0-infra)
$ 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
#include <magellan/magellan.hpp> #include "quantity/Length.h" USING_HAMCREST_NS FIXTURE(LengthTest) { TEST("1 FEET should equal to 12 INCH") { ASSERT_THAT(Length(1, FEET), eq(Length(12, INCH))); } };
使用 Magellan,只须要包含 magellan.hpp
一个头文件便可。Magellan 使用 Hamcrest 的断言机制,
使得断言更加统1、天然,且具备良好的扩展性;使用 USING_HAMCREST_NS
,从而可使用 eq
代
替 hamcrest::eq
,简短明确;除非出现名字冲突,不然推荐使用简写的 eq
。
// 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的参数能够是任意的C\+\+标识符。通常而言,将其命名为CUT(Class Under Test)的名字便可。根据做用域的大小,Fixture可分为三个类别:独立的Fixture,共享的Fixture,全局的Fixture。
xUnit | BDD |
---|---|
FIXTURE | CONTEXT |
SETUP | BEFORE |
TEARDOWN | AFTER |
ASSERT_THAT | EXPECT |
#include <magellan/magellan.hpp> 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
析构函数
#include <magellan/magellan.hpp> 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
有时候须要在全部用例启动以前完成一次性的全局性的配置,在全部用例运行完成以后完成一次性的清理工做。Magellan则使用BEFORE_ALL
和AFTER_ALL
两个关键字来支持这样的特性。
#include <magellan/magellan.hpp> 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
便可,Magellan便能自动地发现它们,并执行它们。犹如C\+\+不能保证各源文件中全局变量初始化的顺序同样,避免在源文件之间的BEFORE_ALL
和AFTER_ALL
设计不合理的依赖关系。
#include <magellan/magellan.hpp> FIXTURE(LengthTest) { Length length; BEFORE_CLASS() {} AFTER_CLASS() {} BEFORE() {} AFTER() {} TEST("length test1") {} TEST("length test2") {} };
#include <magellan/magellan.hpp> FIXTURE(VolumeTest) { Volume volume; BEFORE_CLASS() {} AFTER_CLASS() {} BEFORE() {} AFTER() {} TEST("volume test1") {} TEST("volume test1") {} };
Magellan可能的一个执行序列为:
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")
Magellan可以自动地实现测试用例的标识功能,用户可使用字符串来解释说明测试用例的意图,使得用户在描述用例时更加天然和方便。
#include <magellan/magellan.hpp> #include "quantity/length/Length.h" USING_HAMCREST_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))); } };
Magellan实现xUnit时很是巧妙,使得用户设计用例时更加面向对象。RobotCleaner robot
在每一个用例执行时都将获取一个独立的、全新的实例。
#include "magellan/magellan.hpp" #include "robot-cleaner/RobotCleaner.h" #include "robot-cleaner/Position.h" #include "robot-cleaner/Instructions.h" USING_HAMCREST_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 "magellan/magellan.hpp" #include "robot-cleaner/RobotCleaner.h" #include "robot-cleaner/Position.h" #include "robot-cleaner/Instructions.h" USING_HAMCREST_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)); } };
Magellan只支持一种断言原语:ASSERT_THAT
, 从而避免用户在选择ASSERT_EQ/ASSERT_NE, ASSERT_TRUE/ASSERT_FALSE
时的困扰,使其断言更加具备统一性,一致性。
此外,ASSERT_THAT
使得断言更加具备表达力,它将实际值放在左边,指望值放在右边,更加符合英语习惯。
#include <magellan/magellan.hpp> 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是Java社区一个轻量级的,可扩展的Matcher框架,曾被Kent Beck引入到JUnit框架中,用于加强断言的机制。Magellan引入了Hamcrest的设计,实现了一个C\+\+移植版本的Hamcrest,使得Magellang的断言更加具备扩展性和可读性。
匹配器 | 说明 |
---|---|
anything | 老是匹配 |
_ | anything语法糖 |
#include <magellan/magellan.hpp> USING_HAMCREST_NS FIXTURE(AnythingTest) { TEST("should always be matched") { ASSERT_THAT(1, anything<int>()); ASSERT_THAT(1u, anything<unsigned int>()); ASSERT_THAT(1.0, anything<double>()); ASSERT_THAT(1.0f, anything<float>()); ASSERT_THAT(false, anything<bool>()); ASSERT_THAT(true, anything<bool>()); ASSERT_THAT(nullptr, anything<std::nullptr_t>()); } 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 <magellan/magellan.hpp> USING_HAMCREST_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 <magellan/magellan.hpp> USING_HAMCREST_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 <magellan/magellan.hpp> USING_HAMCREST_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 <magellan/magellan.hpp> USING_HAMCREST_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 <magellan/magellan.hpp> #include <math.h> USING_HAMCREST_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("magellan") { 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"; }
Magellan总体的结构实际上是一棵树,用于用例的组织和管理。
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++
的实现中,彷佛变得很是困难。Magellan
的设计很是简单,将TEST
的元信息在编译时注册到框架,简单地使用了C++
元编程的技术,及其C++11
的一些特性保证,从而解决了C++
社区一直未解决此问题的关键。
TEST
的运行时信息由TestMethod
的概念表示,其表明FIXTURE
中一个普通的成员函数TEST
,它们都具备一样的函数原型: void Fixture::*)()
; TestMethod
是一个泛型类,泛型参数是Fixture
;形式化地描述为:
template <typename Fixture> struct TestMethod { using Method = void(Fixture::*)(); };
TestCaller
也是一个泛型类,它将一个TestMethod
适配为一个普通的TestCase
。
template <typename Fixture> 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
实际上是对Magellan
核心领域的一个扩展,从而保证核心领域的不变性,而使其具备最大的可扩展性和灵活性。
在编译时经过测试用例TEST的元信息的注册,使用TestFactory
很天然地将这些用例自动生成出来了。由于Magallan
组织用例是一刻树,TestFactory
也被设计为一棵树,从而使得其与框架核心领域保持高度的一致性,更加天然、漂亮。
Magellan
经过TestListener
对运行时的状态变化进行监控,从而实现了Magellan
不一样格式报表打印的变化。