Cut: A Simple xUnit Test Framewo

作者: 刘光聪 | 来源:发表于2016-06-13 09:17 被阅读320次

    动机

    实现Cut(C++ Unified Test Framework)的动机,请参阅:无法忍受 Google Test 的 9 个特性

    灵感

    Cut(C++ Unified Test Framework)是一个简单的、可扩展的、使用C\\+\\+11实现的xUnit测试框架。Cut设计灵感来自于Java社区著名的测试框架JUnit。

    安装

    GitHub

    编译环境

    支持的平台:

    • [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 <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 一个头文件即可。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 <cut/cut.hpp>
    
    FIXTURE(LengthTest)
    {
        Length length;
    
        SETUP()
        {}
    
        TEARDOWN()
        {}
    
        TEST("length test1")
        {}
    
        TEST("length test2")
        {}
    };
    

    执行序列为:

    1. Length 构造函数
    2. SETUP
    3. TEST("length test1")
    4. TEARDOWN
    5. Length 析构函数
    6. Length 构造函数
    7. SETUP
    8. TEST("length test2")
    9. TEARDOWN
    10. Length 析构函数

    共享的Fixture

    #include <cut/cut.hpp>
    
    FIXTURE(LengthTest)
    {
        Length length;
    
        BEFORE_CLASS()
        {}
    
        AFTER_CLASS()
        {}
    
        BEFORE()
        {}
    
        AFTER()
        {}
    
        TEST("length test1")
        {}
    
        TEST("length test2")
        {}
    };
    

    执行序列为:

    1. BEFORE_CLASS
    2. Length 构造函数
    3. BEFORE
    4. TEST("length test1")
    5. AFTER
    6. Length 析构函数
    7. Length 构造函数
    8. BEFORE
    9. TEST("length test2")
    10. AFTER
    11. Length 析构函数
    12. AFTER_CLASS

    全局的Fixture

    有时候需要在所有用例启动之前完成一次性的全局性的配置,在所有用例运行完成之后完成一次性的清理工作。Cut则使用BEFORE_ALLAFTER_ALL两个关键字来支持这样的特性。

    #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_ALLAFTER_ALL向系统注册Hook即可,Cut便能自动地发现它们,并执行它们。犹如C\\+\\+不能保证各源文件中全局变量初始化的顺序一样,避免在源文件之间的BEFORE_ALLAFTER_ALL设计不合理的依赖关系。

    #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可能的一个执行序列为:

    1. BEFORE_ALL("before all 1")
    2. BEFORE_ALL("before all 2")
    3. LengthTest::BEFORE_CLASS
    4. Length构造函数
    5. LengthTest::BEFORE
    6. TEST("length test1")
    7. LengthTest::AFTER
    8. Length析构函数
    9. Length构造函数
    10. LengthTest::BEFORE
    11. TEST("length test2")
    12. LengthTest::AFTER
    13. Length析构函数
    14. LengthTest::AFTER_CLASS
    15. VolumeTest::BEFORE_CLASS
    16. Volume构造函数
    17. LengthTest::BEFORE
    18. TEST("volume test1")
    19. LengthTest::AFTER
    20. Volume析构函数
    21. Volume构造函数
    22. LengthTest::BEFORE
    23. TEST("volume test2")
    24. LengthTest::AFTER
    25. Volume析构函数
    26. VolumeTest::AFTER_CLASS
    27. AFTER_ALL("after all 2")
    28. AFTER_ALL("after all 1")

    用例设计

    自动标识

    Cut能够自动地实现测试用例的标识功能,用户可以使用字符串来解释说明测试用例的意图,使得用户在描述用例时更加自然和方便。

    #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)));
        }
    };
    

    面向对象

    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 <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社区一个轻量级的,可扩展的Matcher框架,曾被Kent Beck引入到JUnit框架中,用于增强断言的机制。Cut引入了Hamcrest的设计,实现了一个C\\+\\+移植版本的Hamcrest,使得Cut的断言更加具有扩展性和可读性。

    结构
    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 断言是否以该子串结尾
    ends_with_ignoring_case 忽略大小写,断言是否以该子串结尾
    #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")));
        }
    };
    
    浮点数
    匹配器 说明
    close_to 断言浮点数近似等于
    nan 断言浮点数不是一个数字
    #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()));
        }
    };
    

    程序选项

    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 <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其实是对Cut核心领域的一个扩展,从而保证核心领域的不变性,而使其具有最大的可扩展性和灵活性。

    工厂

    在编译时通过测试用例TEST的元信息的注册,使用TestFactory很自然地将这些用例自动生成出来了。因为Magallan组织用例是一刻树,TestFactory也被设计为一棵树,从而使得其与框架核心领域保持高度的一致性,更加自然、漂亮。

    监听状态

    Cut通过TestListener对运行时的状态变化进行监控,从而实现了Cut不同格式报表打印的变化。

    相关文章

      网友评论

        本文标题:Cut: A Simple xUnit Test Framewo

        本文链接:https://www.haomeiwen.com/subject/mokydttx.html