测试框架
GTest
gtest是一个跨平台(Liunx、Mac OS X、Windows、Cygwin、Windows CE and Symbian)的C++测试框架,有google公司发布。gtest测试框架是在不同平台上为编写C++测试而生成的。
git clone https://github.com/google/googletest
git checkout release-1.8.0
cd ~/googletest && cmake .
make && sudo make install
Test 和 TestF
TEST宏的作用是创建一个简单测试,它定义了一个测试函数,在这个函数里可以使用任何C++代码并使用提供的断言来进行检查。
TEST语法定义:
TEST(test_case_name, test_name)
test_case_name第一个参数是测试用例名,通常是取测试函数名或者测试类名
test_name 第二个参数是测试名这个随便取,但最好取有意义的名称
当测试完成后显示的测试结果将以”测试用例名.测试名”的形式给出
#include <gtest/gtest.h>
int Factorial( int n ) {
if(n==2) return 100; //故意出个错,嘻嘻
return n<=0? 1 : n*Factorial(n - 1);
}
//用TEST做简单测试
TEST(TestFactorial, ZeroInput) //第一个参数是测试用例名,第二个参数是测试名:随后的测试结果将以"测试用例名.测试名"的形式给出
{
EXPECT_EQ(1, Factorial(0)); //EXPECT_EQ稍候再说,现在只要知道它是测试两个数据是否相等的就行了。
}
TEST(TestFactorial, OtherInput) {
EXPECT_EQ(1, Factorial(1));
EXPECT_EQ(2, Factorial(2));
EXPECT_EQ(6, Factorial(3));
EXPECT_EQ(40320, Factorial(8));
}
int main(int argc, char* argv[]) {
testing::InitGoogleTest(&argc,argv); //用来处理Test相关的命令行开关,如果不关注也可不加
RUN_ALL_TESTS(); //看函数名就知道干啥了
return 0;
}
TEST_F宏
TEST_F主要是进行多样测试,就是多种不同情况的测试TestCase中都会使用相同一份的测试数据的时候将会才用它。
即用相同的数据测试不同的行为,如果采用TEST宏进行测试那么将会为不同的测试case创建一份数据。TEST_F宏将会共用一份避免重复拷贝共具灵活性。
语法定义为:
TEST_F(test_case_name, test_name);
test_case_name第一个参数是测试用例名,必须取类名。这个和TEST宏不同
test_name 第二个参数是测试名这个随便取,但最好取有意义的名称
使用TEST_F时必须继承::testing::Test类。并且该类提供了两个接口void SetUp(); void TearDown();
void SetUp()函数,为测试准备对象.
void TearDown()函数 为测试后销毁对象资源。
如下程序测试一个Base类的两个方法,它们都共用相同的数据(Base类对象):
程序通过BaseTest类创建一个共用的数据资源,这个在测试时将无需为没有测试用例单独创建Base对象。
#include <iostream>
#include <memory>
#include <gtest/gtest.h>
#include <Memory.h>
using namespace sampleCXX::common;
class Base {
public:
Base(std::string name):m_name{name} {
std::cout << "Create constructor name: " << m_name << std::endl;
}
std::string getName() {
return m_name;
}
void setName(const std::string &name) {
m_name = std::string(name);
}
~Base() {
std::cout << "Destory base" << std::endl;
}
private:
std::string m_name;
};
class BaseTest : public ::testing::Test {
protected:
// 为测试准备数据对象
void SetUp() override {
m_base = std::make_shared<Base>("SvenBaseTest");
}
// 清除资源
void TearDown() override {
m_base.reset();
}
std::shared_ptr<Base> m_base;
};
TEST_F(BaseTest, testCreateInstance) {
std::unique_ptr<Base> instance = make_unique<Base>("SvenBaseUnique");
EXPECT_NE(instance, nullptr);
instance.reset();
EXPECT_EQ(instance, nullptr);
}
TEST_F(BaseTest, testGetName) {
auto name = m_base->getName();
EXPECT_STREQ(name.c_str(), "SvenBaseTest");
}
TEST_F(BaseTest, testSetName) {
m_base->setName("NewSvenBase");
auto name = m_base->getName();
EXPECT_STREQ(name.c_str(), "NewSvenBase");
}
Mock
gmock是一款开源的白盒测试工具,测试一个模块的时候,可能涉及到和其他模块交互,可以将模块之间的接口mock起来,模拟交互过程。例如:
下面简单的说说打桩在白盒测试中的重要性:
1、比如银行业务,需要测试业务模块。此时,不可能去操作真实的数据库,并且搭建新的数据库可能比较复杂或者耗时。那么就可以用gmock将数据库接口地方打桩,来模拟数据库操作。
2、比如要测试A模块,必过A模块需要调用B模块的函数。如果B模块还没有实现,此时,就可以用gmock将B模块的某些接口打桩。这样就可以让A模块的测试继续进行下去。
3、比如网关设备,在用gtest测试device模块的时候,必须有真实的设备才能让测试进行下去。如果用gmock模拟一套sdk接口,那么无需真实的设备也能让测试进行下去。
举例
我们工程有一个类CD是这样的
class CD
{
public:
CD() {}
virtual ~CD() {}
virtual std::string getAttrString() = 0;
virtual int getPosition(int parm) = 0;
};
而后须要定义个 Mock 类来继承我们要mock的类CD,而且定义须要模拟(mock)的方法:getAttrString, getPosition。这里咱们用到了宏定义MOCK_METHOD0,MOCK_METHOD1后面的数字表明了模拟函数的参数个数,如MOCK_METHOD0,MOCK_METHOD1。它接受两个参数:
头文件中还有其他类似宏定义,如MOCK_METHOD0,MOCK_METHOD2…
MOCK_METHOD#1(#2, #3(#4) )
#2是你要mock的方法名称!#1表示你要mock的方法共有几个参数,#4是这个方法具体的参数,#3表示这个方法的返回值类型。很简单不是?
class MockCD:public CD
{
public:
//0和1代表了参数的个数
MOCK_METHOD0(getAttrString,std::string());
MOCK_METHOD1(getPosition,int(int));
};
经过这个宏定义,已经初步模拟出对应的方法了。接下来在TEST里告诉 Mock Object 被调用时该如何动作(就是给测试模拟什么样的输出):
TEST(MockTestCase, Demo1)
{
int n = 100;
std::string value = "Hello World!";
MockCD mockFoo;
//期待运行1次,且返回值为value的字符串<--就是告诉测试,调到getAttrString方法就模拟返回value
EXPECT_CALL(mockFoo, getAttrString())
.Times(1)
.WillOnce(testing::Return(value));
std::string returnValue = mockFoo.getAttrString();
std::cout << "Returned Value: " << returnValue << std::endl;
//期待运行两次,返回值分别为335 和 455<--就是告诉测试,调到getPosition方法就模拟第一次返回334,第二次返回455
EXPECT_CALL(mockFoo, getPosition(testing::_))
.Times(2)
.WillOnce(testing::Return(335))
.WillOnce(testing::Return(455));
int val = mockFoo.getPosition(0); //355
int val2 = mockFoo.getPosition(1); //455
std::cout << "Returned Value: " << val << " " << val2 << std::endl;
}
最后我们运行编译,得到的结果如下:
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from MockTestCase
[ RUN ] MockTestCase.Demo1
Returned Value: Hello World!
Returned Value: 335 455
[ OK ] MockTestCase.Demo1 (17 ms)
[----------] 1 test from MockTestCase (19 ms total)
[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran. (28 ms total)
[ PASSED ] 1 test.
.Makefile里面需要加入 -lgmock才能正常连接
AM_LDFLAGS=-lpthread -lc -lm -lrt -lgtest -lgmock
自定义方法/成员函数的期望行为
从上述的例子中可以看出,当我们针对懒同学的接口定义好了Mock类后,在单元测试/主程序中使用这个Mock类中的方法时最关键的就是对期望行为的定义。
对方法期望行为的定义的语法格式如下:
EXPECT_CALL(mock_object, method(matcher1, matcher2, ...))
.With(multi_argument_matcher)
.Times(cardinality)
.InSequence(sequences)
.After(expectations)
.WillOnce(action)
.WillRepeatedly(action)
.RetiresOnSaturation();
解释一下这些参数:
- mock_object就是你的Mock类的对象
- method(matcher1, matcher2, …)中的method是你Mock类中的某个方法名,比如上述的 getAttrString
而matcher(匹配器)的意思是定义方法参数的类型。 - Times(cardinality) 之前定义的method运行几次。。
- InSequence(sequences) 定义这个方法被执行顺序(优先级)。
- WillOnce(action) 定义一次调用时所产生的行为,比如定义该方法返回怎么样的值等等。
- WillRepeatedly(action) 缺省/重复行为。
Filters
–gtest_list_tests
使用这个参数时,将不会执行里面的测试案例,而是输出一个案例的列表。
–gtest_filter
对执行的测试案例进行过滤,支持通配符? 单个字符* 任意字符- 排除,如,-a 表示除了a: 取或,如,a:b 表示a或b比如下面的例子:
./foo_test 没有指定过滤条件,运行所有案例
./foo_test –gtest_filter=* 使用通配符*,表示运行所有案例
./foo_test –gtest_filter=FooTest.* 运行所有“测试案例名称(testcase_name)”为FooTest的案例
./foo_test –gtest_filter=Null:Constructor 运行所有“测试案例名称(testcase_name)”或“测试名称(test_name)”包含Null或Constructor的案例
./foo_test –gtest_filter=-DeathTest. 运行所有非死亡测试案例。
./foo_test –gtest_filter=FooTest.*-FooTest.Bar 运行所有“测试案例名称(testcase_name)”为FooTest的案例,但是除了FooTest.Bar这个案例
–gtest_also_run_disabled_tests
执行案例时,同时也执行被置为无效的测试案例。关于设置测试案例无效的方法为:在测试案例名称或测试名称中添加DISABLED前缀,比如:
// Tests that Foo does Abc.
TEST(FooTest, DISABLED_DoesAbc) { }
class DISABLED_BarTest : public testing::Test { };
// Tests that Bar does Xyz.
TEST_F(DISABLED_BarTest, DoesXyz) { }
–gtest_repeat
设置案例重复运行次数,非常棒的功能!比如:
–gtest_repeat=1000 重复执行1000次,即使中途出现错误。
–gtest_repeat=-1 无限次数执行。。。。
–gtest_repeat=1000 –gtest_break_on_failure 重复执行1000次,并且在第一个错误发生时立即停止。这个功能对调试非常有用。
–gtest_repeat=1000 –gtest_filter=FooBar 重复执行1000次测试案例名称为FooBar的案例。
–gtest_color
–gtest_color=(yes|no|auto) 输出命令行时是否使用一些五颜六色的颜色。默认是auto。
–gtest_print_time
输出命令行时是否打印每个测试案例的执行时间。默认是不打印的。
–gtest_output
–gtest_output=xml[:DIRECTORY_PATH|:FILE_PATH] 将测试结果输出到一个xml中。
1.–gtest_output=xml: 不指定输出路径时,默认为案例当前路径。
2.–gtest_output=xml:d:\ 指定输出到某个目录
3.–gtest_output=xml:d:\foo.xml 指定输出到d:\foo.xml如果不是指定了特定的文件路径,gtest每次输出的报告不会覆盖,而会以数字后缀的方式创建。xml的输出内容后面介绍吧。
–gtest_break_on_failure
调试模式下,当案例失败时停止,方便调试
–gtest_throw_on_failure
当案例失败时以C++异常的方式抛出
–gtest_catch_exceptions
是否捕捉异常。gtest默认是不捕捉异常的,因此假如你的测试案例抛了一个异常,很可能会弹出一个对话框,这非常的不友好,同时也阻碍了测试案例的运行。如果想不弹这个框,可以通过设置这个参数来实现。如将–gtest_catch_exceptions设置为一个非零的数。注意:这个参数只在Windows下有效。
xml报告
<?xml version="1.0" encoding="UTF-8"?>
<testsuites tests="3" failures="1" errors="0" time="35" name="AllTests">
<testsuite name="MathTest" tests="2" failures="1"* errors="0" time="15">
<testcase name="Addition" status="run" time="7" classname="">
<failure message="Value of: add(1, 1) Actual: 3 Expected: 2" type=""/>
<failure message="Value of: add(1, -1) Actual: 1 Expected: 0" type=""/>
</testcase>
<testcase name="Subtraction" status="run" time="5" classname="">
</testcase>
</testsuite>
<testsuite name="LogicTest" tests="1" failures="0" errors="0" time="5">
<testcase name="NonContradiction" status="run" time="5" classname="">
</testcase>
</testsuite>
</testsuites>
从报告里可以看出,我们之前在TEST等宏中定义的测试案例名称(testcase_name)在xml测试报告中其实是一个testsuite name,而宏中的测试名称(test_name)在xml测试报告中是一个testcase name,概念上似乎有点混淆,就看你怎么看吧。
当检查点通过时,不会输出任何检查点的信息。当检查点失败时,会有详细的失败信息输出来failure节点。
在我使用过程中发现一个问题,当我同时设置了–gtest_filter参数时,输出的xml报告中还是会包含所有测试案例的信息,只不过那些不被执行的测试案例的status值为“notrun”。而我之前认为的输出的xml报告应该只包含我需要运行的测试案例的信息。不知是否可提供一个只输出需要执行的测试案例的xml报告。因为当我需要在1000个案例中执行其中1个案例时,在报告中很难找到我运行的那个案例,虽然可以查找,但还是很麻烦。
QTest
QTest是Qt开发使用的测试框架。Qt使用界面,当然就需要和用户交互,比如,鼠标点击,在文本框中输入文本,既然要自动化测试,那必须将鼠标的点击事件,键盘的输入事件等进行模拟。
为此,QTest::keyClicks()模拟在该控件上输入按键序列,基本等同于输入字符串。此外,还也可以指定键盘组合按键,例如与ctrl,shift等按键的组合,并在每次单击按键后设置延迟(以毫秒为单位)。类似的方式,还可以使用QTest::keyClick()、QTest::keyPress()、QTest::keyRelease()、QTest::mouseClick()、QTest::mouseDClick()、QTest::mouseMove()、QTest::mousePress()和QTest::mouseRelease()函数来模拟GUI事件。
下面的一个例子,我们主要介绍keyClicks的用法,其它类同。如下图所示,在输入价格和成本后,自动显示利润。我们为该窗口类取名为CommodityWidget
1 创建CommodityWidget窗口
ui界面如上图所示。
commoditywidget.h
#ifndef COMMODITYWIDGET_H
#define COMMODITYWIDGET_H
#include <QWidget>
namespace Ui {
class CommodityWidget;
}
class CommodityWidget : public QWidget
{
Q_OBJECT
public:
friend class CommodityTest;
explicit CommodityWidget(QWidget *parent = 0);
~CommodityWidget();
double costing() const;
double price() const;
double profit() const;
private slots:
void showProfit();
void on_line_price_textChanged(const QString &arg1);
void on_line_costing_textChanged(const QString &arg1);
private:
Ui::CommodityWidget *ui;
};
#endif // COMMODITYWIDGET_H
在头文件中的
friend class CommodityTest;
因为CommdityTest模拟键盘事件,需要直接访问ui界面中的控件,所以将它声明为CommodityWidget的友元类。
commoditywidget.cpp
#include "commoditywidget.h"
#include "ui_commoditywidget.h"
#include "commodity.h"
CommodityWidget::CommodityWidget(QWidget *parent) :
QWidget(parent),
ui(new Ui::CommodityWidget)
{
ui->setupUi(this);
}
CommodityWidget::~CommodityWidget()
{
delete ui;
}
double CommodityWidget::costing() const
{
return ui->line_costing->text().toDouble();
}
double CommodityWidget::price() const
{
return ui->line_price->text().toDouble();
}
double CommodityWidget::profit() const
{
return ui->line_profit->text().toDouble();
}
void CommodityWidget::showProfit()
{
double c = costing();
double p = price();
Commodity commodity("beer_1", "啤酒", c, p);
ui->line_profit->setText(QString::number(commodity.profit()));
}
void CommodityWidget::on_line_price_textChanged(const QString &arg1)
{
showProfit();
}
void CommodityWidget::on_line_costing_textChanged(const QString &arg1)
{
showProfit();
}
2 编写测试函数
在CommodityTest的头文件中添加如下槽函数
头文件:
private slots:
//成本
void case1_gui_costing();
//价格
void case2_gui_price();
//利润
void case3_gui_profit();
源文件,注意这里添加了ui头文件:
#include "commoditytest.h"
#include "commodity.h"
#include "commoditywidget.h"
#include "ui_commoditywidget.h"
void CommodityTest::case1_gui_costing()
{
CommodityWidget w;
//模拟按键,在键盘上输入成本 5.0
QTest::keyClicks(w.ui->line_costing, "5.0");
QCOMPARE(w.costing(), 5.0);
}
void CommodityTest::case2_gui_price()
{
CommodityWidget w;
//模拟按键,在键盘上输入价格 7.2
QTest::keyClicks(w.ui->line_price, "7.2");
QCOMPARE(w.price(), 7.2);
}
void CommodityTest::case3_gui_profit()
{
CommodityWidget w;
//模拟按键,在键盘上输入成本5.0,价格7.2
//最后比较利润是否为2.2
QTest::keyClicks(w.ui->line_costing, "5.0");
QTest::keyClicks(w.ui->line_price, "7.2");
QCOMPARE(w.profit(), 2.2);
}
获取界面每个控件的入参状态后,通过QTest对控件对象进行操作,完成模拟人工输入
键盘相关 | 鼠标相关 |
---|---|
keyClick(…) 键盘按一个键 keyClicks(…) 键盘按多个键 keyEvent(…) 键盘事件 keyPress(…) 键盘按下 keyRelease(…) 键盘释放 |
mouseClick(…) 鼠标单击 mouseDClick(…) 鼠标双击 mouseMove(…) 鼠标移动 mousePress(…) 鼠标按下 mouseRelease(…) 鼠标释放 |
PySide
Python中PySide.QtTest.QTest.keyClicks方法的典型用法代码示例
# 需要导入模块: from PySide.QtTest import QTest [as 别名]
# 或者: from PySide.QtTest.QTest import keyClicks [as 别名]
def keyClicks(self, widget, sequence, modifier=qt.Qt.NoModifier, delay=-1):
"""Simulate clicking a sequence of keys.
See QTest.keyClick for details.
"""
QTest.keyClicks(widget, sequence, modifier, delay)
self.qWait(20)