文章目录
在设计测试案例时,经常需要考虑给被测函数传入不同的值的情况。我们之前的做法通常是写一个通用方法,然后编写在测试案例调用它。即使使用了通用方法,这样的工作也是有很多重复性的,程序员都懒,都希望能够少写代码,多复用代码。Google的程序员也一样,他们考虑到了这个问题,并且提供了一个灵活的参数化测试的方案。
为了对比,我还是把旧的方案提一下。首先我先把被测函数IsPrime帖过来(在gtest的example1.cc中),这个函数是用来判断传入的数值是否为质数的。
// Returns true iff n is a prime number.
bool IsPrime(int n)
{
// Trivial case 1: small numbers
if (n <= 1) return false;
// Trivial case 2: even numbers
if (n % 2 == 0) return n == 2;
// Now, we have that n is odd and n >= 3.
// Try to divide n by every odd number i, starting from 3
for (int i = 3; ; i += 2) {
// We only have to try i up to the squre root of n
if (i > n/i) break;
// Now, we have i <= n/i < n.
// If n is divisible by i, n is not prime.
if (n % i == 0) return false;
}
// n has no integer factor in the range (1, n), and thus is prime.
return true;
}
假如我要编写判断结果为True的测试案例,我需要传入一系列数值让函数IsPrime去判断是否为True(当然,即使传入再多值也无法确保函数正确,呵呵),因此我需要这样编写如下的测试案例:
TEST(IsPrimeTest, HandleTrueReturn)
{
EXPECT_TRUE(IsPrime(3));
EXPECT_TRUE(IsPrime(5));
EXPECT_TRUE(IsPrime(11));
EXPECT_TRUE(IsPrime(23));
EXPECT_TRUE(IsPrime(17));
}
我们注意到,在这个测试案例中,我至少复制粘贴了4次,假如参数有50个,100个,怎么办?同时,上面的写法产生的是1个测试案例,里面有5个检查点,假如我要把5个检查变成5个单独的案例,将会更加累人。
接下来,就来看看gtest是如何为我们解决这些问题的。
1️⃣ 告诉gtest你的参数类型是什么
你必须添加一个类,继承testing::TestWithParam,其中T就是你需要参数化的参数类型,比如上面的例子,我需要参数化一个int型的参数
class IsPrimeParamTest : public::testing::TestWithParam<int>
{
};
2️⃣ 告诉gtest你拿到参数的值后,具体做些什么样的测试
这里,我们要使用一个新的宏(嗯,挺兴奋的):TEST_P,关于这个"P"的含义,Google给出的答案非常幽默,就是说你可以理解为”parameterized" 或者 “pattern”。我更倾向于 ”parameterized"的解释,呵呵。在TEST_P宏里,使用GetParam()获取当前的参数的具体值。
TEST_P(IsPrimeParamTest, HandleTrueReturn)
{
int n = GetParam();
EXPECT_TRUE(IsPrime(n));
}
嗯,非常的简洁!
3️⃣ 告诉gtest你想要测试的参数范围是什么
使用INSTANTIATE_TEST_CASE_P这宏来告诉gtest你要测试的参数范围:
INSTANTIATE_TEST_CASE_P(TrueReturn, IsPrimeParamTest, testing::Values(3, 5, 11, 23, 17));
第一个参数是测试案例的前缀,可以任意取。
第二个参数是测试案例的名称,需要和之前定义的参数化的类的名称相同,如:IsPrimeParamTest
第三个参数是可以理解为参数生成器,上面的例子使用test::Values表示使用括号内的参数。Google提供了一系列的参数生成的函数:
| 函数 | 功能 |
| Range(begin, end[, step]) | 范围在begin~end之间,步长为step,不包括end |
| Values(v1, v2, ..., vN) | v1,v2到vN的值 |
| ValuesIn(container) and ValuesIn(begin, end) | 从一个C类型的数组或是STL容器,或是迭代器中取值 |
| Bool() | 取false 和 true 两个值 |
| Combine(g1, g2, ..., gN) | 这个比较强悍,它将g1,g2,...gN进行排列组合,g1,g2,...gN本身是一个参数生成器,每次分别从g1,g2,..gN中各取出一个值,组合成一个元组(Tuple)作为一个参数。 |
说明:这个功能只在提供了<tr1/tuple>头的系统中有效。gtest会自动去判断是否支持tr/tuple,如果你的系统确实支持,而gtest判断错误的话,你可以重新定义宏GTEST_HAS_TR1_TUPLE=1。
因为使用了参数化的方式执行案例,我非常想知道运行案例时,每个案例名称是如何命名的。我执行了上面的代码,输出如下:

从上面的框框中的案例名称大概能够看出案例的命名规则,对于需要了解每个案例的名称的我来说,这非常重要。 命名规则大概为:
prefix/test_case_name.test.name/index
gtest还提供了应付各种不同类型的数据时的方案,以及参数化类型的方案。我个人感觉这个方案有些复杂。首先要了解一下类型化测试,就用gtest里的例子了。
首先定义一个模版类,继承testing::Test:
template <typename T>
class FooTest : public testing::Test {
public:
typedef std::list<T> List;
static T shared_;
T value_;
};
接着我们定义需要测试到的具体数据类型,比如下面定义了需要测试char,int和unsigned int :
typedef testing::Types<char, int, unsigned int> MyTypes;
TYPED_TEST_CASE(FooTest, MyTypes);
又是一个新的宏,来完成我们的测试案例,在声明模版的数据类型时,使用TypeParam
TYPED_TEST(FooTest, DoesBlah) {
// Inside a test, refer to the special name TypeParam to get the type
// parameter. Since we are inside a derived class template, C++ requires
// us to visit the members of FooTest via 'this'.
TypeParam n = this->value_;
// To visit static members of the fixture, add the 'TestFixture::'
// prefix.
n += TestFixture::shared_;
// To refer to typedefs in the fixture, add the 'typename TestFixture::'
// prefix. The 'typename' is required to satisfy the compiler.
typename TestFixture::List values;
values.push_back(n);
}
上面的例子看上去也像是类型的参数化,但是还不够灵活,因为需要事先知道类型的列表。gtest还提供一种更加灵活的类型参数化的方式,允许你在完成测试的逻辑代码之后再去考虑需要参数化的类型列表,并且还可以重复的使用这个类型列表。下面也是官方的例子:
template <typename T>
class FooTest : public testing::Test {
};
TYPED_TEST_CASE_P(FooTest);
接着又是一个新的宏TYPED_TEST_P类完成我们的测试案例:
TYPED_TEST_P(FooTest, DoesBlah) {
// Inside a test, refer to TypeParam to get the type parameter.
TypeParam n = 0;
}
TYPED_TEST_P(FooTest, HasPropertyA) { }
接着,我们需要我们上面的案例,使用REGISTER_TYPED_TEST_CASE_P宏,第一个参数是testcase的名称,后面的参数是test的名称
REGISTER_TYPED_TEST_CASE_P(FooTest, DoesBlah, HasPropertyA);
接着指定需要的类型列表:
typedef testing::Types<char, int, unsigned int> MyTypes;
INSTANTIATE_TYPED_TEST_CASE_P(My, FooTest, MyTypes);
这种方案相比之前的方案提供更加好的灵活度,当然,框架越灵活,复杂度也会随之增加。
gtest为我们提供的参数化测试的功能给我们的测试带来了极大的方便,使得我们可以写更少更优美的代码,完成多种参数类型的测试案例。
【系列链接】
【单元测试】C++单元测试框架Google Test入门之一 : 初识Gtest
【单元测试】C++单元测试框架Google Test入门之二 : 断言
【单元测试】C++单元测试框架Google Test入门之三 : 事件机制
【单元测试】C++单元测试框架Google Test入门之四 : 参数化
【单元测试】C++单元测试框架Google Test入门之五 : 死亡测试
【单元测试】C++单元测试框架Google Test入门之六 : 运行参数
【单元测试】C++单元测试框架Google Test入门之七 : 深入解析GTEST
【单元测试】C++单元测试框架Google Test入门之八 : 打造自己的单元测试框架
很好奇,就使用rubyonrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提
我正在编写一个包含C扩展的gem。通常当我写一个gem时,我会遵循TDD的过程,我会写一个失败的规范,然后处理代码直到它通过,等等......在“ext/mygem/mygem.c”中我的C扩展和在gemspec的“扩展”中配置的有效extconf.rb,如何运行我的规范并仍然加载我的C扩展?当我更改C代码时,我需要采取哪些步骤来重新编译代码?这可能是个愚蠢的问题,但是从我的gem的开发源代码树中输入“bundleinstall”不会构建任何native扩展。当我手动运行rubyext/mygem/extconf.rb时,我确实得到了一个Makefile(在整个项目的根目录中),然后当
exe应该在我打开页面时运行。异步进程需要运行。有什么方法可以在ruby中使用两个参数异步运行exe吗?我已经尝试过ruby命令-system()、exec()但它正在等待过程完成。我需要用参数启动exe,无需等待进程完成是否有任何rubygems会支持我的问题? 最佳答案 您可以使用Process.spawn和Process.wait2:pid=Process.spawn'your.exe','--option'#Later...pid,status=Process.wait2pid您的程序将作为解释器的子进程执行。除
我有一个围绕一些对象的包装类,我想将这些对象用作散列中的键。包装对象和解包装对象应映射到相同的键。一个简单的例子是这样的:classAattr_reader:xdefinitialize(inner)@inner=innerenddefx;@inner.x;enddef==(other)@inner.x==other.xendenda=A.new(o)#oisjustanyobjectthatallowso.xb=A.new(o)h={a=>5}ph[a]#5ph[b]#nil,shouldbe5ph[o]#nil,shouldbe5我试过==、===、eq?并散列所有无济于事。
我有一些Ruby代码,如下所示:Something.createdo|x|x.foo=barend我想编写一个测试,它使用double代替block参数x,这样我就可以调用:x_double.should_receive(:foo).with("whatever").这可能吗? 最佳答案 specify'something'dox=doublex.should_receive(:foo=).with("whatever")Something.should_receive(:create).and_yield(x)#callthere
我正在为一个项目制作一个简单的shell,我希望像在Bash中一样解析参数字符串。foobar"helloworld"fooz应该变成:["foo","bar","helloworld","fooz"]等等。到目前为止,我一直在使用CSV::parse_line,将列分隔符设置为""和.compact输出。问题是我现在必须选择是要支持单引号还是双引号。CSV不支持超过一个分隔符。Python有一个名为shlex的模块:>>>shlex.split("Test'helloworld'foo")['Test','helloworld','foo']>>>shlex.split('Test"
我不确定传递给方法的对象的类型是否正确。我可能会将一个字符串传递给一个只能处理整数的函数。某种运行时保证怎么样?我看不到比以下更好的选择:defsomeFixNumMangler(input)raise"wrongtype:integerrequired"unlessinput.class==FixNumother_stuffend有更好的选择吗? 最佳答案 使用Kernel#Integer在使用之前转换输入的方法。当无法以任何合理的方式将输入转换为整数时,它将引发ArgumentError。defmy_method(number)
Sinatra新手;我正在运行一些rspec测试,但在日志中收到了一堆不需要的噪音。如何消除日志中过多的噪音?我仔细检查了环境是否设置为:test,这意味着记录器级别应设置为WARN而不是DEBUG。spec_helper:require"./app"require"sinatra"require"rspec"require"rack/test"require"database_cleaner"require"factory_girl"set:environment,:testFactoryGirl.definition_file_paths=%w{./factories./test/
两者都可以defsetup(options={})options.reverse_merge:size=>25,:velocity=>10end和defsetup(options={}){:size=>25,:velocity=>10}.merge(options)end在方法的参数中分配默认值。问题是:哪个更好?您更愿意使用哪一个?在性能、代码可读性或其他方面有什么不同吗?编辑:我无意中添加了bang(!)...并不是要询问nobang方法与bang方法之间的区别 最佳答案 我倾向于使用reverse_merge方法:option
我遵循MichaelHartl的“RubyonRails教程:学习Web开发”,并创建了检查用户名和电子邮件长度有效性的测试(名称最多50个字符,电子邮件最多255个字符)。test/helpers/application_helper_test.rb的内容是:require'test_helper'classApplicationHelperTest在运行bundleexecraketest时,所有测试都通过了,但我看到以下消息在最后被标记为错误:ERROR["test_full_title_helper",ApplicationHelperTest,1.820016791]test