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)