動(dòng)機(jī)
實(shí)現(xiàn)Cut(C++ Unified Test Framework)的動(dòng)機(jī)苏遥,請(qǐng)參閱:無法忍受 Google Test 的 9 個(gè)特性
靈感
Cut(C++ Unified Test Framework)是一個(gè)簡(jiǎn)單的正林、可擴(kuò)展的、使用C\\+\\+11實(shí)現(xiàn)的xUnit測(cè)試框架创夜。Cut設(shè)計(jì)靈感來自于Java社區(qū)著名的測(cè)試框架JUnit。
安裝
GitHub
編譯環(huán)境
支持的平臺(tái):
- [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
測(cè)試Cut
$ cd cut/build
$ cmake -DENABLE_TEST=on ..
$ make
$ test/cut-test
破冰之旅
物理目錄
quantity
├── include
│ └── quantity
├── src
│ └── quantity
└── test
│ ├── main.cpp
└── CMakeLists.txt
main函數(shù)
#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)
構(gòu)建
$ mkdir build
$ cd build
$ cmake ..
$ make
運(yùn)行
$ ./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
體驗(yàn)Cut
第一個(gè)用例
#include <cut/cut.hpp>
#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
一個(gè)頭文件即可驰吓。Cut 使用 Hamcrest 的斷言機(jī)制,
使得斷言更加統(tǒng)一涧尿、自然,且具有良好的擴(kuò)展性;使用 USING_CUM_NS
,從而可以使用 eq
代
替 cum::eq
,簡(jiǎn)短明確;除非出現(xiàn)名字沖突,否則推薦使用簡(jiǎn)寫的 eq
。
Length實(shí)現(xiàn)
// 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);
}
構(gòu)建
$ mkdir build
$ cd build
$ cmake ..
$ make
運(yùn)行
$ ./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的參數(shù)可以是任意的C\\+\\+標(biāo)識(shí)符檬贰。一般而言姑廉,將其命名為CUT(Class Under Test)的名字即可。根據(jù)作用域的大小翁涤,F(xiàn)ixture可分為三個(gè)類別:獨(dú)立的Fixture桥言,共享的Fixture,全局的Fixture葵礼。
支持BDD風(fēng)格
xUnit | BDD |
---|---|
FIXTURE | CONTEXT |
SETUP | BEFORE |
TEARDOWN | AFTER |
ASSERT_THAT | EXPECT |
獨(dú)立的Fixture
#include <cut/cut.hpp>
FIXTURE(LengthTest)
{
Length length;
SETUP()
{}
TEARDOWN()
{}
TEST("length test1")
{}
TEST("length test2")
{}
};
執(zhí)行序列為:
-
Length
構(gòu)造函數(shù) SETUP
TEST("length test1")
TEARDOWN
-
Length
析構(gòu)函數(shù) -
Length
構(gòu)造函數(shù) SETUP
TEST("length test2")
TEARDOWN
-
Length
析構(gòu)函數(shù)
共享的Fixture
#include <cut/cut.hpp>
FIXTURE(LengthTest)
{
Length length;
BEFORE_CLASS()
{}
AFTER_CLASS()
{}
BEFORE()
{}
AFTER()
{}
TEST("length test1")
{}
TEST("length test2")
{}
};
執(zhí)行序列為:
BEFORE_CLASS
-
Length
構(gòu)造函數(shù) BEFORE
TEST("length test1")
AFTER
-
Length
析構(gòu)函數(shù) -
Length
構(gòu)造函數(shù) BEFORE
TEST("length test2")
AFTER
-
Length
析構(gòu)函數(shù) AFTER_CLASS
全局的Fixture
有時(shí)候需要在所有用例啟動(dòng)之前完成一次性的全局性的配置号阿,在所有用例運(yùn)行完成之后完成一次性的清理工作。Cut則使用BEFORE_ALL
和AFTER_ALL
兩個(gè)關(guān)鍵字來支持這樣的特性鸳粉。
#include <cut/cut.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
向系統(tǒng)注冊(cè)Hook
即可扔涧,Cut便能自動(dòng)地發(fā)現(xiàn)它們,并執(zhí)行它們届谈。猶如C\\+\\+不能保證各源文件中全局變量初始化的順序一樣枯夜,避免在源文件之間的BEFORE_ALL
和AFTER_ALL
設(shè)計(jì)不合理的依賴關(guān)系。
#include <cut/cut.hpp>
FIXTURE(LengthTest)
{
Length length;
BEFORE_CLASS()
{}
AFTER_CLASS()
{}
BEFORE()
{}
AFTER()
{}
TEST("length test1")
{}
TEST("length test2")
{}
};
#include <cut/cut.hpp>
FIXTURE(VolumeTest)
{
Volume volume;
BEFORE_CLASS()
{}
AFTER_CLASS()
{}
BEFORE()
{}
AFTER()
{}
TEST("volume test1")
{}
TEST("volume test1")
{}
};
Cut可能的一個(gè)執(zhí)行序列為:
BEFORE_ALL("before all 1")
BEFORE_ALL("before all 2")
LengthTest::BEFORE_CLASS
-
Length
構(gòu)造函數(shù) LengthTest::BEFORE
TEST("length test1")
LengthTest::AFTER
-
Length
析構(gòu)函數(shù) -
Length
構(gòu)造函數(shù) LengthTest::BEFORE
TEST("length test2")
LengthTest::AFTER
-
Length
析構(gòu)函數(shù) LengthTest::AFTER_CLASS
VolumeTest::BEFORE_CLASS
-
Volume
構(gòu)造函數(shù) LengthTest::BEFORE
TEST("volume test1")
LengthTest::AFTER
-
Volume
析構(gòu)函數(shù) -
Volume
構(gòu)造函數(shù) LengthTest::BEFORE
TEST("volume test2")
LengthTest::AFTER
-
Volume
析構(gòu)函數(shù) VolumeTest::AFTER_CLASS
AFTER_ALL("after all 2")
AFTER_ALL("after all 1")
用例設(shè)計(jì)
自動(dòng)標(biāo)識(shí)
Cut能夠自動(dòng)地實(shí)現(xiàn)測(cè)試用例的標(biāo)識(shí)功能艰山,用戶可以使用字符串來解釋說明測(cè)試用例的意圖湖雹,使得用戶在描述用例時(shí)更加自然和方便。
#include <cut/cut.hpp>
#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)));
}
};
面向?qū)ο?/h4>
Cut實(shí)現(xiàn)xUnit時(shí)非常巧妙程剥,使得用戶設(shè)計(jì)用例時(shí)更加面向?qū)ο蟆?code>RobotCleaner robot在每個(gè)用例執(zhí)行時(shí)都將獲取一個(gè)獨(dú)立的劝枣、全新的實(shí)例。
#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)));
}
};
函數(shù)提取
提取的相關(guān)子函數(shù)织鲸,可以直接放在Fixture
的內(nèi)部舔腾,使得用例與其的距離最近,更加體現(xiàn)類作用域的概念搂擦。
#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
時(shí)的困擾稳诚,使其斷言更加具有統(tǒng)一性,一致性瀑踢。
此外扳还,ASSERT_THAT
使得斷言更加具有表達(dá)力,它將實(shí)際值放在左邊橱夭,期望值放在右邊氨距,更加符合英語習(xí)慣。
#include <cut/cut.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
Hamcrest是Java社區(qū)一個(gè)輕量級(jí)的棘劣,可擴(kuò)展的Matcher框架俏让,曾被Kent Beck引入到JUnit框架中,用于增強(qiáng)斷言的機(jī)制。Cut引入了Hamcrest的設(shè)計(jì)首昔,實(shí)現(xiàn)了一個(gè)C\\+\\+移植版本的Hamcrest寡喝,使得Cut的斷言更加具有擴(kuò)展性和可讀性。
結(jié)構(gòu)
anything
匹配器 | 說明 |
---|---|
anything | 總是匹配 |
_ | anything語法糖 |
#include <cut/cut.hpp>
USING_CUM_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 <cut/cut.hpp>
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 <cut/cut.hpp>
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 <cut/cut.hpp>
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 | 斷言是否以該子串結(jié)尾 |
ends_with_ignoring_case | 忽略大小寫,斷言是否以該子串結(jié)尾 |
#include <cut/cut.hpp>
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")));
}
};
浮點(diǎn)數(shù)
匹配器 | 說明 |
---|---|
close_to | 斷言浮點(diǎn)數(shù)近似等于 |
nan | 斷言浮點(diǎn)數(shù)不是一個(gè)數(shù)字 |
#include <cut/cut.hpp>
#include <math.h>
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()));
}
};
程序選項(xiàng)
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";
}
設(shè)計(jì)與實(shí)現(xiàn)
核心領(lǐng)域
Cut整體的結(jié)構(gòu)其實(shí)是一棵樹赊颠,用于用例的組織和管理格二。
struct TestResult;
DEFINE_ROLE(Test)
{
ABSTRACT(const std::string& getName () const);
ABSTRACT(int countTestCases() const);
ABSTRACT(int countChildTests() const);
ABSTRACT(void run(TestResult&));
};
適配
如何讓FIXTURE
中一個(gè)普通的成員函數(shù)TEST
在運(yùn)行時(shí)表現(xiàn)為一個(gè)TestCase
呢?在C++
的實(shí)現(xiàn)中巨税,似乎變得非常困難蟋定。Cut
的設(shè)計(jì)非常簡(jiǎn)單,將TEST
的元信息在編譯時(shí)注冊(cè)到框架草添,簡(jiǎn)單地使用了C++
元編程的技術(shù),及其C++11
的一些特性保證扼仲,從而解決了C++
社區(qū)一直未解決此問題的關(guān)鍵远寸。
TEST
的運(yùn)行時(shí)信息由TestMethod
的概念表示,其代表FIXTURE
中一個(gè)普通的成員函數(shù)TEST
屠凶,它們都具有同樣的函數(shù)原型: void Fixture::*)()
; TestMethod
是一個(gè)泛型類驰后,泛型參數(shù)是Fixture
;形式化地描述為:
template <typename Fixture>
struct TestMethod
{
using Method = void(Fixture::*)();
};
TestCaller
也是一個(gè)泛型類矗愧,它將一個(gè)TestMethod
適配為一個(gè)普通的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
其實(shí)是對(duì)Cut
核心領(lǐng)域的一個(gè)擴(kuò)展,從而保證核心領(lǐng)域的不變性唉韭,而使其具有最大的可擴(kuò)展性和靈活性夜涕。
工廠
在編譯時(shí)通過測(cè)試用例TEST的元信息的注冊(cè),使用TestFactory
很自然地將這些用例自動(dòng)生成出來了属愤。因?yàn)?code>Magallan組織用例是一刻樹女器,TestFactory
也被設(shè)計(jì)為一棵樹,從而使得其與框架核心領(lǐng)域保持高度的一致性住诸,更加自然驾胆、漂亮。
監(jiān)聽狀態(tài)
Cut
通過TestListener
對(duì)運(yùn)行時(shí)的狀態(tài)變化進(jìn)行監(jiān)控贱呐,從而實(shí)現(xiàn)了Cut
不同格式報(bào)表打印的變化丧诺。