既然fixture内部可以定义setup,我们自然希望也能对称地定义teardown,用于处理同一个fixture中所有测试用例共同的善后工作。
比如我们希望有如下的测试用例描述方式:
FIXTURE(TestTearDown)
{
TEARDOWN()
{
ASSERT_EQ(__int(0), expected);
}
TEST("test 1")
{
using expected = __int(0);
};
TEST("test 2")
{
using expected = __sub(__int(1), __int(1));
};
};
如上TEARDOWN
的定义一般在fixture中所有测试用例的前面,但执行却在每个测试用例的后面。要如何实现TEARDOWN
呢?借助前面实现setup的经验,似乎我们需要用户通过TEARDOWN
定义一个类,每个测试用例能够继承它,然后再将自身传递给它。
于是我们想到CRTP(Curiously Recurring Template Pattern)
模式,它的用法如下:
template
struct TlpTestTeardown
{
// 可以在此处使用TestCase类型的内部定义
};
struct TlpTestCase : TlpTestTearDown
{
// ...
};
可以看到CRTP允许我们先定义一个父类模板,然后子类继承这个模板的时候把自身当做模板参数再传递给父类模板,于是父类模板中就可以使用子类类型了。
问题是对于完成teardown功能的父类模板到底如何使用作为每个测试用例的子类类型?最直接的想法是让TlpTestTeardown再继承自Test,这样每个测试用例中的内容就对TlpTestTeardown直接可见了。
template
struct TlpTestTeardown : TestCase
{
// ...
};
struct TlpTestCase : TlpTestTearDown
{
// ...
};
遗憾的是,上述代码并不能编译通过。原因是TlpTestTeardown
将TlpTestCase作为父类,所以编译器编译到这句的时候需要TlpTestCase的完整定义,而此时TlpTestCase才开始定义。
这警告了我们,如果TlpTestTearDown继承了TlpTestCase,就得在TlpTestTearDown
时刻看到TlpTestCase的完整定义。那么我们是否可以将TlpTestTearDown
挪到TlpTestCase定义的后面。
struct TlpTestCase
{
// ...
};
struct : TlpTestTearDown{};
这可以通过编译,但是从此以后所有的测试用例后面都得加上struct : TlpTestTearDown
。这意味着我们需要给宏TEST
实现一个对应的结束宏,专门用来隐藏struct : TlpTestTearDown
。这导致我们以后定义测试用例不能再使用常用的花括号,而是要像下面这样:
TEST_BEGIN("test description")
// 测试用例内容实现于此
TEST_END() // 用于隐藏“}; struct : TlpTestTearDown{};”
这很讨厌!由于定义测试用例是如此的频繁,我们还是希望保持之前那种简洁的定义方式!
我们再回到struct TlpTestCase : TlpTestTearDown
写法中,看来将这种组合关系的定义放在花括号的前面就处理完毕是必要的。那么还有办法吗?我们想起,如果在TlpTestTearDown的成员方法中使用TlpTestCase类型,那么可以延迟对TlpTestCase完整定义的依赖时刻。于是我们将TlpTestTearDown的实现修改如下:
template struct TlpTestTeardown
{
TlpTestTeardown()
{
struct Teardown : TestCase
{
// 在这里实现teardown的定义,这里可以使用TestCase的内部定义!
};
}
};
在上面的代码中,我们在TlpTestTeardown
模板的构造函数里面定义了一个临时类Teardown
继承了模板的入参TestCase。在该临时类里面我们可以定义teardown的具体内容,它可以访问TestCase类的内部定义。由于现在该临时类定义在TlpTestTeardown的成员函数里,所以struct TlpTestCase : TlpTestTearDown
时刻并不需要TlpTestCase的完整定义,编译器会将TlpTestTearDown的构造函数的实例化放到当前cpp文件的最后,这时已经能够看到TlpTestCase的完整定义了。
当使用上面这种技术之后,TEST
宏定义修改如下:
#define TEST(name) struct UNIQUE_NAME(tlp_test_) :TlpTestSetup, TlpTestTearDown
于是定义测试用例的用法还和以前一样,仍旧可以使用熟悉的花括号。然而teardown的定义却会稍微的不统一:
#define TEARDOWN_BEGIN() \
template struct TlpTestTeardown \
{ \
TlpTestTeardown() \
{ \
struct Teardown : TestCase \
{
#define TEARDOWN_END() }; } };
可以看到对于teardown的定义将不能像setup和testcase那样使用花括号了,而是使用TEARDOWN_BEGIN()
和TEARDOWN_END()
。虽然风格有所差别,但这已经是我能想到的最好办法了。
为了让setup的定义能够和teardown统一,我们也给setup的定义提供了一套对称的宏定义SETUP_BEGIN()
,SETUP_END()
。当fixture中需要同时出现setup和teardown时,setup可以使用和teardown一致的风格去定义。其它情况下,setup还是使用花括号的风格。
上述技术中其实还存在一个问题,TlpTestTeardown构造函数中Teardown类继承的TestCase是从模板参数输入的,所以在Teardown中是不能直接引用TestCase中的定义的,必须加上TestCase::
前缀。为此我们实现了一个更有语义的宏__test_refer()
,用于显示指定teardown中某一个变量引用自TestCase。
#define __test_refer(...) typename TestCase::__VA_ARGS__
最终,我们可以如下定义一个完整的fixture:
FIXTURE(TestWithSetUpAndTearDown)
{
SETUP_BEGIN()
using Expected = __int(2);
SETUP_END()
TEARDOWN_BEGIN()
ASSERT_EQ(__test_refer(Result), Expected);
TEARDOWN_END()
TEST("test1")
{
using Result = __add(__int(1), __int(1));
};
TEST("test2")
{
using Result = __div(__int(4), __int(2));
};
};
我们不得不承认,引入teardown后让整个框架复杂了很多,而且风格也趋向不一致。但好在对于具有不可变性的元编程来说,清理测试上下文的事情本就意义不大,所以一般在fixture中需要定义teardown的机会并不多。关键地我们没有因为引入teardown的功能损害到原有定义testcase的简洁性。而且在没有teardown的fixture中,setup仍旧可以像以前一样使用花括号。所以绝大多数情况下,还是可以接受的!
不过我相信应该还有更好的实现方式,不过限于本人能力,只能到此!如果你了解更好的实现方式,还请不吝赐教!
测试报告
返回 C++11模板元编程 - 目录