Gtest/Gmock实战之Gmock

一、如何使用gMock

1 首先,使用一些简单的宏来描述要模拟的接口,它们将扩展到模拟类的实现;

2 接下来,创建一些模拟对象,并使用直观的语法指定它的期望和行为;

3.然后练习使用模拟对象的代码。一旦出现任何违反预期的情况,gMock将立即捕获。

mock的语法见:docs/reference/mocking.md

1.自定义方法/成员函数的期望行为

对方法期望行为的定义的语法格式如下:

EXPECT_CALL(mock_object, method(matcher1, matcher2, ...))
    .With(multi_argument_matcher)
    .Times(cardinality)
    .InSequence(sequences)
    .After(expectations)
    .WillOnce(action)
    .WillRepeatedly(action)
    .RetiresOnSaturation();1行的mock_object就是你的Mock类的对象
第1行的method(matcher1, matcher2,)中的method就是你Mock类中的某个方法名,比如上述的getArbitraryString;而matcher(匹配器)的意思是定义方法参数的类型,我们待会详细介绍。
第3行的Times(cardinality)的意思是之前定义的method运行几次。至于cardinality的定义,我也会在后面详细介绍。
第4行的InSequence(sequences)的意思是定义这个方法被执行顺序(优先级),我会再后面举例说明。
第6WillOnce(action)是定义一次调用时所产生的行为,比如定义该方法返回怎么样的值等等。
第7WillRepeatedly(action)的意思是缺省/重复行为。
  • eg:
EXPECT_CALL(mockTurtle, getX()).Times(testing::AtLeast(5)).
                WillOnce(testing::Return(100)).WillOnce(testing::Return(150)).
                WillRepeatedly(testing::Return(200))


调用mockTurtle的getX()方法
这个方法会至少调用5次
第一次被调用时返回1002次被调用时返回150
从第3次被调用开始每次都返回200

2.匹配器

Matcher用于定义Mock类中的方法的形参的值(当然,如果你的方法不需要形参时,可以保持match为空。),它有以下几种类型

通配符

  • 这里的_和*A*包括下面的那个匹配符都在Google Mock的*::testing*这个命名空间下,大家要用时需要先引入那个命名空间
    在这里插入图片描述

一般比较
在这里插入图片描述
浮点数的比较
在这里插入图片描述

字符串匹配

  • 这里的字符串即可以是C风格的字符串,也可以是C++风格的。
    在这里插入图片描述

容器的匹配

  • 很多STL的容器的比较都支持==这样的操作,对于这样的容器可以使用上述的Eq(container)来比较。但如果你想写得更为灵活,可以使用下面的这些容器匹配方法:
    在这里插入图片描述
  • eg:
#ifndef MOCKFOO_H_
#define MOCKFOO_H_

#include <gmock/gmock.h>
#include <string>
#include <vector>
#include "FooInterface.h"

namespace seamless {

class MockFoo: public FooInterface {
public:
        MOCK_METHOD(std::string, getArbitraryString, ());
        MOCK_METHOD1(void, setValue, (std::string& value));
        MOCK_METHOD2(void, setDoubleValues, (int x, int y));
};

}  // namespace seamless

#endif // MOCKFOO_H_
#include <cstdlib>
#include <gmock/gmock.h>
#include <iostream>
#include <string>

#include "MockFoo.h"

using namespace seamless;
using namespace std;

using ::testing::Assign;
using ::testing::Eq;
using ::testing::Ge;
using ::testing::Return;

int main(int argc, char** argv) {
        ::testing::InitGoogleMock(&argc, argv);

        string value = "Hello World!";
        MockFoo mockFoo;

        EXPECT_CALL(mockFoo, setValue(testing::_));
        mockFoo.setValue(value);

        // 这里我故意犯错
        EXPECT_CALL(mockFoo, setDoubleValues(Eq(1), Ge(1)));
        mockFoo.setDoubleValues(1, 0);

        return EXIT_SUCCESS;
}

第22行,让setValue的形参可以传入任意参数
另外,我在第26~27行故意犯了个错(为了说明上述这些匹配器的作用),我之前明明让setDoubleValues第二个参数得大于等于1,但我实际传入时却传入一个0。这时程序运行时就报错了:

unknown file: Failure
Unexpected mock function call – returning directly.
Function call: setDoubleValues(1, 0)
Google Mock tried the following 1 expectation, but it didn't match:

FooMain.cc:35: EXPECT_CALL(mockFoo, setDoubleValues(Eq(1), Ge(1)))…
Expected arg #1: is >= 1
Actual: 0
Expected: to be called once
Actual: never called – unsatisfied and active
FooMain.cc:35: Failure
Actual function call count doesn't match EXPECT_CALL(mockFoo, setDoubleValues(Eq(1), Ge(1)))…
Expected: to be called once
Actual: never called – unsatisfied and active

成员匹配器
在这里插入图片描述

  • eg:第5行,我们定义了一个Field(&Bar::num, Ge(0)),以说明Bar的成员变量num必须大于等于0。
TEST(TestField, Simple) {
        MockFoo mockFoo;
        Bar bar;
        EXPECT_CALL(mockFoo, get(Field(&Bar::num, Ge(0)))).Times(1);
        mockFoo.get(bar);
}

int main(int argc, char** argv) {
        ::testing::InitGoogleMock(&argc, argv);
        return RUN_ALL_TESTS();
}
  • eg:我们为了说明Field的作用,传入一个bar.num = -1试试。
TEST(TestField, Simple) {
        MockFoo mockFoo;
        Bar bar;
        bar.num = -1;
        EXPECT_CALL(mockFoo, get(Field(&Bar::num, Ge(0)))).Times(1);
        mockFoo.get(bar);
}
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from TestField
[ RUN ] TestField.Simple
unknown file: Failure
Unexpected mock function call – returning directly.
Function call: get(@0xbff335bc 4-byte object )
Google Mock tried the following 1 expectation, but it didn't match:

FooMain.cc:34: EXPECT_CALL(mockFoo, get(Field(&Bar::num, Ge(0))))…
Expected arg #0: is an object whose given field is >= 0
Actual: 4-byte object , whose given field is -1
Expected: to be called once
Actual: never called – unsatisfied and active
FooMain.cc:34: Failure
Actual function call count doesn't match EXPECT_CALL(mockFoo, get(Field(&Bar::num, Ge(0))))…
Expected: to be called once
Actual: never called – unsatisfied and active
[ FAILED ] TestField.Simple (0 ms)
[----------] 1 test from TestField (0 ms total)

[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran. (0 ms total)
[ PASSED ] 0 tests.
[ FAILED ] 1 test, listed below:
[ FAILED ] TestField.Simple

1 FAILED TEST

匹配函数或函数对象的返回值
在这里插入图片描述

指针匹配器
在这里插入图片描述

复合匹配器
在这里插入图片描述

  • eg:传入的参数必须 >5 并且 <= 10
EXPECT_CALL(foo, DoThis(AllOf(Gt(5), Ne(10))));
  • eg:第一个参数不包含“blah”这个子串
EXPECT_CALL(foo, DoThat(Not(HasSubstr("blah")), NULL));

3.基数Cardinalities

基数用于Times()中来指定模拟函数将被调用多少次

在这里插入图片描述

4.行为(Actions)

Actions(行为)用于指定Mock类的方法所期望模拟的行为:比如返回什么样的值、对引用、指针赋上怎么样个值,等等。

  • 值的返回
    在这里插入图片描述
  • 副作用(Side Effects)

在这里插入图片描述

  • 使用函数或者函数对象(Functor)作为行为
    在这里插入图片描述
  • 复合动作
    在这里插入图片描述
  • eg:例如有一个Mock方法:
irtual int getParamter(std::string* name,  std::string* value) = 0

对于这个方法,我这回需要操作的结果是将name指向value的地址,并且得到方法的返回值。
类似这样的需求,我们就可以这样定义期望过程:

  • 这时就用上了我们的DoAll()了,它将Assign()和Return()结合起来了。
TEST(SimpleTest, F1) {
    std::string* a = new std::string("yes");
    std::string* b = new std::string("hello");
    MockIParameter mockIParameter;
    EXPECT_CALL(mockIParameter, getParamter(testing::_, testing::_)).Times(1).\
        WillOnce(testing::DoAll(testing::Assign(&a, b), testing::Return(1)));
    mockIParameter.getParamter(a, b);
}

5.序列(Sequences)

默认时,对于定义要的期望行为是无序(Unordered)的,即当我定义好了如下的期望行为:

  • eg:对于这样的期望行为的定义,我何时调用mockFoo.getValue()或者何时mockFoo.getSize()都可以的。
MockFoo mockFoo;
        EXPECT_CALL(mockFoo, getSize()).WillOnce(Return(1));
        EXPECT_CALL(mockFoo, getValue()).WillOnce(Return(string("Hello World")));

但有时候我们需要定义有序的(Ordered)的调用方式,即序列 (Sequences) 指定预期的顺序. 在同一序列里的所有预期调用必须按它们指定的顺序发生; 反之则可以是任意顺序.

using ::testing::Return;
using ::testing::Sequence;

int main(int argc, char **argv) {
        ::testing::InitGoogleMock(&argc, argv);

        Sequence s1, s2;
        MockFoo mockFoo;
        EXPECT_CALL(mockFoo, getSize()).InSequence(s1, s2).WillOnce(Return(1));
        EXPECT_CALL(mockFoo, getValue()).InSequence(s1).WillOnce(Return(
                string("Hello World!")));
        cout << "First:\t" << mockFoo.getSize() << endl;
        cout << "Second:\t" << mockFoo.getValue() << endl;

        return EXIT_SUCCESS;
}
  • 首先在第8行建立两个序列:s1、s2。
  • 然后在第11行中,EXPECT_CALL(mockFoo, getSize()).InSequence(s1, s2)说明getSize()的行为优先于s1、s2.
  • 而第12行时,EXPECT_CALL(mockFoo, getValue()).InSequence(s1)说明getValue()的行为在序列s1- 中。
  • 结果:
First: 1
Second: Hello World!
  • eg:把mockFoo.getSize()和mockFoo.getValue()的调用对调时试试:
        cout << "Second:\t" << mockFoo.getValue() << endl;
        cout << "First:\t" << mockFoo.getSize() << endl;
unknown file: Failure
Unexpected mock function call – returning default value.
Function call: getValue()
Returns: ""
Google Mock tried the following 1 expectation, but it didn't match:

FooMain.cc:29: EXPECT_CALL(mockFoo, getValue())…
Expected: all pre-requisites are satisfied
Actual: the following immediate pre-requisites are not satisfied:
FooMain.cc:28: pre-requisite #0
(end of pre-requisites)
Expected: to be called once
Actual: never called – unsatisfied and active
Second:
First: 1
FooMain.cc:29: Failure
Actual function call count doesn't match EXPECT_CALL(mockFoo, getValue())…
Expected: to be called once
Actual: never called – unsatisfied and active

另外,我们还有一个偷懒的方法,就是不要这么傻乎乎地定义这些个Sequence s1, s2的序列,而根据我定义期望行为(EXPECT_CALL)的顺序而自动地识别调用顺序,这种方式可能更为地通用。

using ::testing::InSequence;
using ::testing::Return;

int main(int argc, char **argv) {
        ::testing::InitGoogleMock(&argc, argv);

        InSequence dummy;
        MockFoo mockFoo;
        EXPECT_CALL(mockFoo, getSize()).WillOnce(Return(1));
        EXPECT_CALL(mockFoo, getValue()).WillOnce(Return(string("Hello World")));

        cout << "First:\t" << mockFoo.getSize() << endl;
        cout << "Second:\t" << mockFoo.getValue() << endl;

        return EXIT_SUCCESS;
}

6.基于虚函数的Mock实战

VariantField.h

#ifndef VARIANTFIELD_H_
#define VARIANTFIELD_H_

#include <boost/cstdint.hpp>

namespace seamless
{

    union VariantField
    {
        const char *strVal;
        int32_t intVal;
    };

} // namespace mlr_isearch_api

#endif // VARIANTFIELD_H_

IAPIProviderInterface.h

#ifndef IAPIPROVIDERINTERFACE_H_
#define IAPIPROVIDERINTERFACE_H_

#include <boost/cstdint.hpp>

#include "IParameterInterface.h"
#include "VariantField.h"

namespace seamless
{

    class IAPIProviderInterface
    {
    public:
        IAPIProviderInterface() {}
        virtual ~IAPIProviderInterface() {}

    public:
        virtual IParameterInterface *getParameterInterface() = 0;
    };

}

#endif // IAPIPROVIDERINTERFACE_H_

IParameterInterface.h

#ifndef IPARAMETERINTERFACE_H_
#define IPARAMETERINTERFACE_H_

#include <boost/cstdint.hpp>

#include "VariantField.h"

namespace seamless
{

    class IParameterInterface
    {
    public:
        virtual ~IParameterInterface(){};

    public:
        virtual int32_t getParameter(const char *name, VariantField *&value) = 0;
    };

} // namespace

#endif // IPARAMETERINTERFACE_H_

MockIAPIProviderInterface.h

#ifndef MOCKIAPIPROVIDERINTERFACE_H_
#define MOCKIAPIPROVIDERINTERFACE_H_

#include <gmock/gmock.h>

#include "IAPIProviderInterface.h"
#include "IParameterInterface.h"

namespace seamless
{

    class MockIAPIProviderInterface : public IAPIProviderInterface
    {
    public:
        MOCK_METHOD(IParameterInterface *, getParameterInterface, ());
    };

} // namespace seamless

#endif // MOCKIAPIPROVIDERINTERFACE_H_

MockIParameterInterface.h

#ifndef MOCKIPARAMETERINTERFACE_H_
#define MOCKIPARAMETERINTERFACE_H_

#include <boost/cstdint.hpp>
#include <gmock/gmock.h>

#include "IParameterInterface.h"
#include "VariantField.h"

namespace seamless
{

    class MockIParameterInterface : public IParameterInterface
    {
    public:
        MOCK_METHOD(int32_t, getParameter, (const char *name, VariantField *&value));
    };

} // namespace seamless

#endif // MOCKIPARAMETERINTERFACE_H_

Rank.cc

#include <cstdlib>
#include <cstring>
#include <iostream>
#include <string>
#include "IAPIProviderInterface.h"
#include "IParameterInterface.h"
#include "VariantField.h"

#include "Rank.h"

using namespace seamless;
using namespace std;

namespace seamless
{

    void Rank::processQuery(IAPIProviderInterface *iAPIProvider)
    {
        IParameterInterface *iParameter = iAPIProvider->getParameterInterface();
        if (!iParameter)
        {
            cerr << "iParameter is NULL" << endl;
            return;
        }

        int32_t isRetailWholesale = 0;
        int32_t isUseAlipay = 0;

        VariantField *value = new VariantField;

        iParameter->getParameter("retail_wholesale", value);
        isRetailWholesale = (strcmp(value->strVal, "0")) ? 1 : 0;

        iParameter->getParameter("is_use_alipay", value);
        isUseAlipay = (strcmp(value->strVal, "0")) ? 1 : 0;

        cout << "isRetailWholesale:\t" << isRetailWholesale << endl;
        cout << "isUseAlipay:\t" << isUseAlipay << endl;

        delete value;
        delete iParameter;
    }

} // namespace seamless

Rank.h

#ifndef RANK_H_
#define RANK_H_

#include "IAPIProviderInterface.h"

namespace seamless
{

    class Rank
    {
    public:
        virtual ~Rank() {}

    public:
        void processQuery(IAPIProviderInterface *iAPIProvider);
    };

} // namespace seamless

#endif // RANK_H_

tester.cc

#include <boost/cstdint.hpp>
#include <boost/shared_ptr.hpp>
#include <cstdlib>
#include <gmock/gmock.h>

#include "MockIAPIProviderInterface.h"
#include "MockIParameterInterface.h"
#include "Rank.h"

using namespace seamless;
using namespace std;

using ::testing::_;
using ::testing::AtLeast;
using ::testing::DoAll;
using ::testing::Return;
using ::testing::SetArgumentPointee;

int main(int argc, char **argv)
{
    ::testing::InitGoogleMock(&argc, argv);

    MockIAPIProviderInterface *iAPIProvider = new MockIAPIProviderInterface;
    MockIParameterInterface *iParameter = new MockIParameterInterface;

    // 定义MockIAPIProviderInterface.getParameterInterface的的行为:程序至少被调用一次(Times(AtLeast(1))),每次调用都返回一个iParameter(即MockIParameterInterface*的对象)。
    EXPECT_CALL(*iAPIProvider, getParameterInterface()).Times(AtLeast(1)).WillRepeatedly(Return(iParameter));

    // 假设了一些Query的Segment的值
    boost::shared_ptr<VariantField> retailWholesaleValue(new VariantField);
    retailWholesaleValue->strVal = "0";

    boost::shared_ptr<VariantField> defaultValue(new VariantField);
    defaultValue->strVal = "9";

    // 定义MockIParameterInterface.getParameter的期望行为:这个方法至少被调用一次;第一次被调用时返回1并将第一个形参指向retailWholesaleValue;后续几次被调用时返回1,并指向defaultValue。
    EXPECT_CALL(*iParameter, getParameter(_, _)).Times(AtLeast(1)).WillOnce(DoAll(SetArgumentPointee<1>(*retailWholesaleValue), Return(1))).WillRepeatedly(DoAll(SetArgumentPointee<1>(*defaultValue), Return(1)));

    // 运行Rank类下的processQuery方法。
    Rank rank;
    rank.processQuery(iAPIProvider);

    delete iAPIProvider;

    return EXIT_SUCCESS;
}

7.Nice Mocks 和 Strict Mocks

Foo.h 带private方法的接口

class Foo {
private:
        virtual void setValue(int value) {};

public:
        int value;
};

MockFoo.h

class MockFoo: public Foo {
public:
        MOCK_METHOD1(setValue, void(int value));
};

当在调用Mock类的方法时,如果之前没有使用EXPECT_CALL来定义该方法的期望行为时,Google Mock在运行时会给你一些警告信息:

  • 一般用不上
GMOCK WARNING:
Uninteresting mock function call – returning default value.
Function call: setValue(1)
Returns: 0
Stack trace

对于这种情况,可以使用NiceMock来避免:

  • 使用NiceMock来替代之前的MockFoo。
using ::testing::NiceMock;
using ::testing::NaggyMock;
using ::testing::StrictMock;

        // MockFoo mockFoo;
        NiceMock<MockFoo> mockFoo;

当然,另外还有一种办法,就是使用StrictMock来将这些调用都标为失败:

StrictMock<MockFoo> mockFoo;

这时得到的结果:

unknown file: Failure
Uninteresting mock function call – returning default value.
Function call: setValue(1)
Returns: 0

8.ON_CALL与EXPECT_CALL区别

  • ref:gmock_faq.md

如果你确定这些调用是正确的。这将告诉gMock您确实期望调用,并且不应该打印任何警告。

  • 写法不同
  • The mock function has no default action set, and its return type has no default value set,则使用ON_CALL
  • Actual function call count doesn’t match EXPECT_CALL(mock, Bar(_))…
    Expected: to be called once
    Actual: never called - unsatisfied and active,则使用ON_CALL
using ::testing::_;
...
  EXPECT_CALL(foo, Bar(_))
      .WillRepeatedly(...);

instead of

using ::testing::_;
...
  ON_CALL(foo, Bar(_))
      .WillByDefault(...);

二、实战

Turtle 接口定义

class Turtle {
  ...
  virtual ~Turtle() {}
  virtual void PenUp() = 0;
  virtual void PenDown() = 0;
  virtual void Forward(int distance) = 0;
  virtual void Turn(int degrees) = 0;
  virtual void GoTo(int x, int y) = 0;
  virtual int GetX() const = 0;
  virtual int GetY() const = 0;
};

编写Mock类

#include "gmock/gmock.h"  // Brings in gMock.

class MockTurtle : public Turtle {
 public:
  ...
  MOCK_METHOD(void, PenUp, (), (override));
  MOCK_METHOD(void, PenDown, (), (override));
  MOCK_METHOD(void, Forward, (int distance), (override));
  MOCK_METHOD(void, Turn, (int degrees), (override));
  MOCK_METHOD(void, GoTo, (int x, int y), (override));
  MOCK_METHOD(int, GetX, (), (const, override));
  MOCK_METHOD(int, GetY, (), (const, override));
};

使用Mock

·1从测试名称空间导入gMock名称,以便可以不限定地使用它们(每个文件只需执行一次)。记住,名称空间是个好主意。
2.创建一些模拟对象。
3.指定您对它们的期望(一个方法将被调用多少次?用什么参数?它应该做什么?等等)。
4.练习一些使用模拟的代码;可选地,使用检查结果
5.googletest断言。如果一个模拟方法被调用的次数超过预期,或者使用了错误的参数,您将立即得到一个错误。
6.当一个mock被销毁时,gMock将自动检查对它的所有期望是否已得到满足。

```cpp
#include "path/to/mock-turtle.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"

using ::testing::AtLeast;                         // #1

TEST(PainterTest, CanDrawSomething) {
  MockTurtle turtle;                              // #2
  EXPECT_CALL(turtle, PenDown())                  // #3
      .Times(AtLeast(1));

  Painter painter(&turtle);                       // #4

  EXPECT_TRUE(painter.DrawCircle(0, 0, 10));      // #5
}

这个测试检查至少调用了一次PenDown()。如果painter对象没有调用这个方法,你的测试就会失败,并显示如下信息:

path/to/my_test.cc:119: Failure
Actual function call count doesn't match this expectation:
Actually: never called;
Expected: called at least once.
Stack trace:
...

给Mock设置EXPECT_CALL

宏有两个参数:第一个是模拟对象,然后是方法及其参数参数。注意,两者之间用逗号(,)隔开,而不是句号(.)。如果方法没有重载,宏也可以在没有匹配器的情况下被调用
在gMock中,我们使用EXPECT_CALL()宏来设置模拟方法的期望。一般语法是:

1.Cardinalities: How Many Times Will It Be Called?

在EXPECT_CALL()之后可以指定的第一个子句是Times()。我们将其参数称为基数,因为它告诉调用应该进行多少次

/*
Cardinalities:即.Times()
Times()子句可以省略。如果省略Times(), gMock将推断你的基数。

如果WillOnce()和willrepeated()都不在, EXPECT_CALL(),推断的基数是Times(1)。

如果有n个WillOnce()但没有willrepeated(),其中n >=1,基数为Times(n)。

如果有n个WillOnce()和一个willrepeated(),其中n >=0,基数为Times(AtLeast(n))。

*/
EXPECT_CALL(mock_object, method(matchers))
    .Times(cardinality)
    .WillOnce(action)
    .WillRepeatedly(action);

EXPECT_CALL(mock_object, non-overloaded-method)
    .Times(cardinality)
    .WillOnce(action)
    .WillRepeatedly(action);

/*
它表示turtle对象的GetX()方法将被调用五次第一次返回100,第二次返回150,然后每次返回200。*/
EXPECT_CALL(turtle, GetX())
    .Times(5)
    .WillOnce(Return(100))
    .WillOnce(Return(150))
    .WillRepeatedly(Return(200));

2.Matchers: What Arguments Do We Expect?

当mock函数接受实参时,我们可以指定期望的实参

补充:非重载方法的Mock,若下面的接口不是virtual的

// Expects the turtle to move forward by 100 units.
EXPECT_CALL(turtle, Forward(100));

/*
_ as the argument, which means "anything goes":
*/
// Expects that the turtle jumps to somewhere on the x=50 line.
EXPECT_CALL(turtle, GoTo(50, _));

// Expects the turtle moves forward by at least 100.
/*
Ge是Gtest内置的Matchers
Matchers:reference/matchers.md
*/
EXPECT_CALL(turtle, Forward(Ge(100)));

//如果你不关心任何参数,而不是为每个参数指定_,你可以省略形参列表:
// Expects the turtle to move forward.
EXPECT_CALL(turtle, Forward);
// Expects the turtle to jump somewhere.
EXPECT_CALL(turtle, GoTo);

3.Actions: What Should It Do?

首先,如果模拟函数的返回类型是内置类型或指针,该函数有一个默认动作(void函数将返回bool函数返回false,其他函数返回0),在c++ 11及以上版本中,一个模拟函数,其返回类型为default-constructible(即有一个默认构造函数)的默认动作为返回默认构造的值。如果你什么都不说,这种行为将被使用。

其次,如果模拟函数没有默认动作,或者默认动作不适合你,你可以指定每次要采取的行动使用一系列WillOnce()子句后跟一个可选WillRepeatedly ()

ReturnRef(variable)可以返回引用

/*
我们没有显式地编写Times()),并将分别返回100、200和300
*/
using ::testing::Return;
...
EXPECT_CALL(turtle, GetX())
     .WillOnce(Return(100))
     .WillOnce(Return(200))
     .WillOnce(Return(300));
/*
没有explicit Times()),前两次将分别返回100和200,从第三次开始是300。
*/
EXPECT_CALL(turtle, GetY())
     .WillOnce(Return(100))
     .WillOnce(Return(200))
     .WillRepeatedly(Return(300));

注意:EXPECT_CALL()语句只计算action子句一次,即使该操作可能被执行多次。所以你一定要注意副作用。

using ::testing::Return;
...
int n = 100;
EXPECT_CALL(turtle, GetX())
    .Times(4)
    .WillRepeatedly(Return(n++));

这里不是返回100 101 102,…连续地,这个模拟函数会总是返回100,就像n + +只被评估过一次

using ::testing::Return;
...
EXPECT_CALL(turtle, GetY())
    .Times(4)
    .WillOnce(Return(100));

记住,每次调用函数时都会使用WillOnce()子句,然后执行默认操作。因此正确答案是turtle.GetY()第一次返回100,但第二次返回0,因为返回0是int函数的默认操作。

4.Using Multiple Expectations

默认情况下,当调用mock方法时,gMock将按照定义期望的相反顺序搜索期望,并在找到与参数匹配的活动期望时停止(您可以将其理解为“更新的规则覆盖旧的。”)。

using ::testing::_;
...
EXPECT_CALL(turtle, Forward(_));  // #1
EXPECT_CALL(turtle, Forward(10))  // #2
    .Times(2);

如果Forward(10)连续被调用三次,那么第三次它将是错误,因为最后一个匹配期望(#2)已经饱和。然而,如果第三次Forward(10)调用被Forward(20)取代,那么它就OK了,现在第一个是匹配的期望。

5.Ordered vs Unordered Calls

在这个例子中,我们测试Foo()调用对象中的三个预期函数按写好的顺序。如果一个调用是无序的,它将是一个错误。

using ::testing::InSequence;
...
TEST(FooTest, DrawsLineSegment) {
  ...
  {
    InSequence seq;

    EXPECT_CALL(turtle, PenDown());
    EXPECT_CALL(turtle, Forward(100));
    EXPECT_CALL(turtle, PenUp());
  }
  Foo();
}

6.All Expectations Are Sticky

gMock中的期望在默认情况下是“粘性的”,即使在我们达到它们的调用上限之后,它们仍然是活动的。这是一个需要记住的重要规则,因为它影响到规范的含义.

假设turtle.GoTo(0,0)被调用三次。在第三次中,gMock将执行确保参数与期望#2匹配(请记住,我们总是选择最后匹配期望)。既然我们说过只有两个这样的调用,gMock会立即报告错误。

using ::testing::_;
using ::testing::AnyNumber;
...
EXPECT_CALL(turtle, GoTo(_, _))  // #1
     .Times(AnyNumber());
EXPECT_CALL(turtle, GoTo(0, 0))  // #2
     .Times(2);

如果你认为它说turtle.GetX()将被调用n次并返回10,20,30,…,连续,三思!问题是,正如我们所说,期望是有粘性的。因此,第二次调用turtle.GetX()时,最后(最新)EXPECT_CALL()语句将匹配,并立即导致“违反上限”错误

using ::testing::Return;
...
 for (int i = 1; i <= n; i++) {
  EXPECT_CALL(turtle, GetX())
      .WillOnce(Return(10*i));
}

正确做法如下:
正确的说法是turtle.GetX()将返回10,20,30,…就是明确地说期望是没有粘性的。换句话说,他们应该在饱和后立即退休:

using ::testing::Return;
...
 for (int i = 1; i <= n; i++) {
  EXPECT_CALL(turtle, GetX())
      .WillOnce(Return(10*i))
      .RetiresOnSaturation();
}

三、gMock Cookbook

1.Creating Mock Classes

class MyMock {
 public:
  MOCK_METHOD(ReturnType, MethodName, (Args...));
  MOCK_METHOD(ReturnType, MethodName, (Args...), (Specs...));
};

前3个参数只是方法声明,分为3部分。第四个参数接受限定符的封闭列表,该限定符将影响生成的方法:

const -使被模拟的方法为const方法。如果需要重写const方法。
override -用override标记方法。如果覆盖,建议使用虚方法。
noexcept -用noexcept标记方法。如果重写noexcept方法。
Calltype(…)-设置方法的调用类型(例如:为STDMETHODCALLTYPE),在Windows中很有用。
ref(…)-用引用限定符标记方法指定。如果重写具有引用的方法则必须资格。例如ref(&)或ref(&&)。

  • 参数里面有逗号,需要用()括起来
class MockFoo {
 public:
  MOCK_METHOD(std::pair<bool, int>, GetPair, ());  // Won't compile!
  MOCK_METHOD(bool, CheckMap, (std::map<int, double>, bool));  // Won't compile!
};

//OK
class MockFoo {
 public:
  MOCK_METHOD((std::pair<bool, int>), GetPair, ());
  MOCK_METHOD(bool, CheckMap, ((std::map<int, double>), bool));
};

//OK
class MockFoo {
 public:
  using BoolAndInt = std::pair<bool, int>;
  MOCK_METHOD(BoolAndInt, GetPair, ());
  using MapIntDouble = std::map<int, double>;
  MOCK_METHOD(bool, CheckMap, (MapIntDouble, bool));
};

2.Mocking Private or Protected Methods

你必须总是把一个模拟方法定义(MOCK_METHOD)放在模拟类的public:部分中,不管被模拟的方法是公共的,基类中的Protected或private。这允许ON_CALL和EXPECT_CALL从模拟类外部引用模拟函数。

(是的,c++允许子类改变基类中虚函数的访问级别。)

class Foo {
 public:
  ...
  virtual bool Transform(Gadget* g) = 0;

 protected:
  virtual void Resume();

 private:
  virtual int GetTimeOut();
};

class MockFoo : public Foo {
 public:
  ...
  MOCK_METHOD(bool, Transform, (Gadget* g), (override));

  // The following must be in the public section, even though the
  // methods are protected or private in the base class.
  MOCK_METHOD(void, Resume, (), (override));
  MOCK_METHOD(int, GetTimeOut, (), (override));
};

3.Mocking Overloaded Methods

class Foo {
  ...

  // Must be virtual as we'll inherit from Foo.
  virtual ~Foo();

  // Overloaded on the types and/or numbers of arguments.
  virtual int Add(Element x);
  virtual int Add(int times, Element x);

  // Overloaded on the const-ness of this object.
  virtual Bar& GetBar();
  virtual const Bar& GetBar() const;
};

class MockFoo : public Foo {
  ...
  MOCK_METHOD(int, Add, (Element x), (override));
  MOCK_METHOD(int, Add, (int times, Element x), (override));

  MOCK_METHOD(Bar&, GetBar, (), (override));
  MOCK_METHOD(const Bar&, GetBar, (), (const, override));
};

如果您没有模拟重载方法的所有版本,编译器将警告基类中的某些方法被隐藏。要解决这个问题,请使用using将它们引入范围:

class MockFoo : public Foo {
  ...
  using Foo::Add;
  MOCK_METHOD(int, Add, (Element x), (override));
  // We don't want to mock int Add(int times, Element x);
  ...
};

4.Mocking Class Templates

template <typename Elem>
class StackInterface {
  ...
  // Must be virtual as we'll inherit from StackInterface.
  virtual ~StackInterface();

  virtual int GetSize() const = 0;
  virtual void Push(const Elem& x) = 0;
};

template <typename Elem>
class MockStack : public StackInterface<Elem> {
  ...
  MOCK_METHOD(int, GetSize, (), (override));
  MOCK_METHOD(void, Push, (const Elem& x), (override));
};

5.Mocking Non-virtual Methods

在这种情况下,模拟类不会与真实类共享一个公共基类,而是与真实类无关,但包含具有相同签名的方法。模拟非虚方法的语法与模拟虚拟方法(只是不添加override):

// A simple packet stream class.  None of its members is virtual.
class ConcretePacketStream {
 public:
  void AppendPacket(Packet* new_packet);
  const Packet* GetPacket(size_t packet_number) const;
  size_t NumberOfPackets() const;
  ...
};

// A mock packet stream class.  It inherits from no other, but defines
// GetPacket() and NumberOfPackets().
class MockPacketStream {
 public:
  MOCK_METHOD(const Packet*, GetPacket, (size_t packet_number), (const));
  MOCK_METHOD(size_t, NumberOfPackets, (), (const));
  ...
};

一种方法是对需要使用包流的代码进行模板化。更具体地说,您将为您的代码提供一个数据包流类型的模板类型参数。在生产中,您将使用实例化模板ConcretePacketStream作为类型参数。在测试中,您将使用MockPacketStream实例化相同的模板。例如,你可以这样写:

template <class PacketStream>
void CreateConnection(PacketStream* stream) { ... }

template <class PacketStream>
class PacketReader {
 public:
  void ReadPackets(PacketStream* stream, size_t packet_num);
};

然后在生产代码中使用CreateConnection()和PacketReader,在生产代码中使用CreateConnection()和PacketReader测试。

 MockPacketStream mock_stream;
  EXPECT_CALL(mock_stream, ...)...;
  .. set more expectations on mock_stream ...
  PacketReader<MockPacketStream> reader(&mock_stream);
  ... exercise reader ...

依赖注入的eg:

源代码
class A{
public:
  int Funtion1(B& obj) {
    //do something
    std::string str = “mock non-virtual methods using templates”;
     auto rst = obj.Function2(str);
    //do something
  }
}
class B{
public:
    int Funtion2(std::string _str){ puts(_str.c_str()); }
}

当我们对类A的方法Function1进行UT防护的时候,不关心其中类B的方法Function2的执行结果,这时候该如何对其进行mock呢(Function2是非虚的)?
Gtest提供了依赖注入的方式

将类A修改为:
template <class T1 >
class  RefactorA{
public:
  int Funtion1(T1 & obj) {
    //do something
    std::string str = “mock non-virtual methods using templates”;
    auto rst = obj.Function2(str);
    //do something
  }
}

重构之后,类RefactorA变成了类模板,在实例化的时候把依赖的类B显式的“注入”进去,这时候进行UT的时候,就可以把“注入”的类B的方法Function2 进行mock,代码如下:

对类A进行UT测试

class  MockB
{
public:
	MOCK_METHOD(int , Funtion2, (std::string))
};

class RefactorA _UT : public :: testing::Test
{
protected:
  virtual void SetUp(){}
  virtual void TearDown(){}

  RefactorA < MockB> mockObjA;//实例化模板类
};
 
TEST_F(RefactorA _UT , Funtion1)
{
  //期望类B的方法Function2被调用至少一次,返回值为100,参数为任意字符串
  MockB mockObjB;
  EXPECT_CALL(mockObjB, Funtion2 (_))
  .Times(AtLeast(1))
  .WillOnce(Return(100));

  auto  rst  =  mockObjA.Function1( mockObjB );//注意这里传入的是mock出来的对象 

  EXPECT_TRUE( rst );
}

6.Mocking Free Functions

不可能直接模拟一个自由函数(即c风格的函数或静态方法)。如果需要,可以重写代码来使用接口(抽象类)。

class FileInterface {
 public:
  ...
  virtual bool Open(const char* path, const char* mode) = 0;
};

class File : public FileInterface {
 public:
  ...
  bool Open(const char* path, const char* mode) override {
     return OpenFile(path, mode);
  }
};

不要直接调用一个自由函数(比如OpenFile),而是为它引入一个接口,并有一个具体的子类来调用自由函数:
这可能看起来很麻烦,但在实践中,您经常有多个相关的函数可以放在同一个接口中,因此每个函数语法开销将大大降低。

7.Old-Style MOCK_METHODn Macros

Simple
OldMOCK_METHOD1(Foo, bool(int))
NewMOCK_METHOD(bool, Foo, (int))
Const Method
OldMOCK_CONST_METHOD1(Foo, bool(int))
NewMOCK_METHOD(bool, Foo, (int), (const))
Method in a Class Template
OldMOCK_METHOD1_T(Foo, bool(int))
NewMOCK_METHOD(bool, Foo, (int))
Const Method in a Class Template
OldMOCK_CONST_METHOD1_T(Foo, bool(int))
NewMOCK_METHOD(bool, Foo, (int), (const))
Method with Call Type
OldMOCK_METHOD1_WITH_CALLTYPE(STDMETHODCALLTYPE, Foo, bool(int))
NewMOCK_METHOD(bool, Foo, (int), (Calltype(STDMETHODCALLTYPE)))
Const Method with Call Type
OldMOCK_CONST_METHOD1_WITH_CALLTYPE(STDMETHODCALLTYPE, Foo, bool(int))
NewMOCK_METHOD(bool, Foo, (int), (const, Calltype(STDMETHODCALLTYPE)))
Method with Call Type in a Class Template
OldMOCK_METHOD1_T_WITH_CALLTYPE(STDMETHODCALLTYPE, Foo, bool(int))
NewMOCK_METHOD(bool, Foo, (int), (Calltype(STDMETHODCALLTYPE)))
Const Method with Call Type in a Class Template
OldMOCK_CONST_METHOD1_T_WITH_CALLTYPE(STDMETHODCALLTYPE, Foo, bool(int))
NewMOCK_METHOD(bool, Foo, (int), (const, Calltype(STDMETHODCALLTYPE)))

四、SaveArg与SaveArgPointee区别

参考:

struct SomeData { int a; };
class ISomeClass
{
public:
    virtual ~ISomeClass() = default;
    virtual void foo(SomeData*) = 0;
};

void someFunction(ISomeClass& a)
{
    SomeData b{1};
    a.foo(&b);
}

class SomeMock : public ISomeClass
{
public:
    MOCK_METHOD1(foo, void(SomeData*));
};

SaveArgPointee - 保存指向的数据 (PACKET_DATA *Data)

  • SaveArgPointee中Mock函数的接口入参必须是指针类型
  • 仅能获取接口中指针返回的值
TEST(TestSomeFoo, shallPassOne)
{
    SomeData actualData{};
    SomeMock aMock;
    EXPECT_CALL(aMock, foo(_)).WillOnce(::testing::SaveArgPointee<0>(&actualData));
    someFunction(aMock);
    ASSERT_EQ(1, actualData.a);
}

SaveArg - 您将只存储指向不再存在的局部变量的指针(更好用)

  • 如果Mock的接口入参是指针类型的,SaveArg也可以干。如果接口入参不是指针类型的只能用SaveArg。
  • 仅能获取接口中指针返回的值
TEST(TestSomeFoo, shallPassOne_DanglingPointer)
{
    SomeData* actualData;
    SomeMock aMock;
    EXPECT_CALL(aMock, foo(_)).WillOnce(::testing::SaveArg<0>(&actualData));
    someFunction(aMock);
    ASSERT_EQ(1, actualData->a);
}

测试:

wangji@script-wang:~/time/$ ./build/unit_test/xx_unittest --gtest_filter=TestSomeFoo.shallPassOne
Note: Google Test filter = TestSomeFoo.shallPassOne
[==========] Running 1 test from 1 test suite.
[----------] Global test environment set-up.
[----------] 1 test from TestSomeFoo
[ RUN      ] TestSomeFoo.shallPassOne
[       OK ] TestSomeFoo.shallPassOne (0 ms)
[----------] 1 test from TestSomeFoo (0 ms total)

[----------] Global test environment tear-down
[==========] 1 test from 1 test suite ran. (0 ms total)
[  PASSED  ] 1 test.

结论:

  • SaveArgPointee和SaveArg两者的实际使用的应用场景基本是一样的,既可以用指针来获取foo函数参数中指针返回的值,也可以用变量获取;

  • SaveArgPointee和SaveArg是无法改变foo函数中指针参数返回来的值的,仅是获取foo被调用后,指针参数返回的值的;

五、Gmock中Invoke的简单使用

#include <gtest/gtest.h>
#include <gmock/gmock.h>
#include <vector>
#include <memory>

class BaseMessage {
public:
   virtual std::vector<uint8_t> data() const noexcept = 0;
   virtual ~BaseMessage() = default;
};

class DerivedMessage : public BaseMessage {
public:
   std::vector<uint8_t> data() const noexcept override { return m_data; }
private:
   std::vector<uint8_t> m_data;
};


void stub_write(std::vector<uint8_t>)
{
        return;
}

// simplified
class Tcp {
        public:
                virtual void sendMessage(std::shared_ptr<BaseMessage> msg) { stub_write(msg->data());}
};

class SomeClass {
public:
   SomeClass(Tcp& tcp) : m_tcp(tcp) {}
   void writeDataToRemote(std::shared_ptr<DerivedMessage> derived)  {
      m_tcp.sendMessage(derived);}
private:
   Tcp m_tcp;
};

class MockTcp : public Tcp {
public:
MOCK_METHOD(void, sendMessage, (std::shared_ptr<BaseMessage> msg), (override));
};

TEST(SomeClassTest, writesMessageToSocket){

            MockTcp mockTcp;
    SomeClass sc(mockTcp);
    std::shared_ptr<BaseMessage> bm;
    EXPECT_CALL(mockTcp, sendMessage(_)).Times(1).WillOnce(::testing::SaveArg<0>(&bm));  // it will effectively do bm = arg;
    const auto derivedMsg = std::make_shared<DerivedMessage>();
    sc.writeDataToRemote(derivedMsg);
    // verify that the argument that you captured is indeed pointing to the same message
    std::cout << derivedMsg.get() << std::endl;
    std::cout << bm.get() << std::endl;

}

TEST(SomeClassTest, writesMessageToSocket_Invoke){
   MockTcp mockTcp;
    SomeClass sc(mockTcp);
    std::shared_ptr<BaseMessage> bm;
    EXPECT_CALL(mockTcp, sendMessage(_)).Times(1).WillOnce(::testing::Invoke([&bm](auto arg) { bm = arg; }));
    const auto derivedMsg2 = std::make_shared<DerivedMessage>();
    sc.writeDataToRemote(derivedMsg2);
    std::cout << derivedMsg2.get() << std::endl;
    std::cout << bm.get() << std::endl;

}

编译并运行

[  FAILED  ] TestSomeFoo.shallPassOne (0 ms)
[----------] 1 test from TestSomeFoo (0 ms total)

[----------] 2 tests from SomeClassTest
[ RUN      ] SomeClassTest.writesMessageToSocket
0x556aed826d60
0
g_test.cc:83: Failure
Actual function call count doesn't match EXPECT_CALL(mockTcp, sendMessage(_))...
         Expected: to be called once
           Actual: never called - unsatisfied and active
[  FAILED  ] SomeClassTest.writesMessageToSocket (0 ms)
[ RUN      ] SomeClassTest.writesMessageToSocket_Invoke
0x556aed826d60
0
g_test.cc:96: Failure
Actual function call count doesn't match EXPECT_CALL(mockTcp, sendMessage(_))...
         Expected: to be called once
           Actual: never called - unsatisfied and active
[  FAILED  ] SomeClassTest.writesMessageToSocket_Invoke (0 ms)
[----------] 2 tests from SomeClassTest (0 ms total)

[----------] Global test environment tear-down
[==========] 3 tests from 2 test suites ran. (0 ms total)
[  PASSED  ] 0 tests.
[  FAILED  ] 3 tests, listed below:
[  FAILED  ] TestSomeFoo.shallPassOne
[  FAILED  ] SomeClassTest.writesMessageToSocket
[  FAILED  ] SomeClassTest.writesMessageToSocket_Invoke

 3 FAILED TESTS

gtest例子:

using ::testing::_; using ::testing::Invoke;

class MockFoo : public Foo {
 public:
  MOCK_METHOD(int, Sum, (int x, int y), (override));
  MOCK_METHOD(bool, ComplexJob, (int x), (override));
};

int CalculateSum(int x, int y) { return x + y; }
int Sum3(int x, int y, int z) { return x + y + z; }

class Helper {
 public:
  bool ComplexJob(int x);
};

...
  MockFoo foo;
  Helper helper;
  EXPECT_CALL(foo, Sum(_, _))
      .WillOnce(&CalculateSum)
      .WillRepeatedly(Invoke(NewPermanentCallback(Sum3, 1)));
  EXPECT_CALL(foo, ComplexJob(_))
      .WillOnce(Invoke(&helper, &Helper::ComplexJob))
      .WillOnce([] { return true; })
      .WillRepeatedly([](int x) { return x > 0; });

  foo.Sum(5, 6);         // Invokes CalculateSum(5, 6).
  foo.Sum(2, 3);         // Invokes Sum3(1, 2, 3).
  foo.ComplexJob(10);    // Invokes helper.ComplexJob(10).
  foo.ComplexJob(-1);    // Invokes the inline lambda.

唯一的要求是函数的类型等必须 与mock函数的签名兼容,这意味着后者的参数(如果有的话)可以隐式转换为前者对应的参数,并且前者的返回类型可以隐式转换为后者的类型。

六、Gtest比较自定义类型(一般不会用的到)

GoogleTest.cpp

// GoogleTest.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include "Bar.h"
#include "BarTest.h"
#include "MockFoo.h"
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <iostream>

int main(int argc, char *argv[]) {
  testing::GTEST_FLAG(output) = "xml:./Report/";
  testing::InitGoogleTest(&argc, argv);

  RUN_ALL_TESTS();
  return 0;
}

自定义类型: Foo.h文件,定义自定义返回类型,并重载==运算符

#pragma once
class ResultData {
  friend bool operator==(const ResultData &lhs, const ResultData &rhs) {
    return lhs.m_data == rhs.m_data;
  }

public:
  void SetDataValue(int val) { m_data = val; }

private:
  int m_data = 100;

  bool operator==(ResultData &rhs) { return this->m_data == rhs.m_data; }
};

class Foo {
public:
  virtual int count() = 0;
  virtual ResultData GetResultData() = 0;
};

测试返回的自定义类型的函数

#pragma once

#include "Foo.h"

class Bar {
public:
  Bar(Foo *foo) : m_foo(foo) {}

  int count() { return m_foo->count(); }

  ResultData GetResultData() { return m_foo->GetResultData(); }

private:
  Foo *m_foo;
};

Mock:MockFoo.h

#pragma once

#include "Foo.h"
#include <gmock/gmock.h>
#include <gtest/gtest.h>

class MockFoo : public Foo {
public:
  MOCK_METHOD0(count, int());
  MOCK_METHOD0(GetResultData, ResultData());
};

test case:文件BarTest.cpp


#include "Bar.h"
#include "MockFoo.h"
#include <gtest/gtest.h>
//定义测试类Bar的固件类BarTest
class BarTest : public ::testing::Test {
protected:
  Bar *m_TestImpl;
  MockFoo *mockFoo;
  void SetUp() override {
    mockFoo = new MockFoo();
    m_TestImpl = new Bar(mockFoo);
  }
  void TearDown() override {
    if (m_TestImpl) {
      delete m_TestImpl;
      m_TestImpl = nullptr;
    }
  }
};

TEST_F(BarTest, count) {
  EXPECT_CALL(*mockFoo, count())
      .Times(3)
      .WillOnce(testing::Return(3))
      .WillOnce(testing::Return(6))
      .WillOnce(testing::Return(5));

  EXPECT_EQ(m_TestImpl->count(), 3);
  EXPECT_EQ(m_TestImpl->count(), 6);
  EXPECT_EQ(m_TestImpl->count(), 5);
}

TEST_F(BarTest, GetResultData) {
  ResultData rd;
  rd.SetDataValue(100);
  ResultData rd_exp;
  rd_exp.SetDataValue(100);

  EXPECT_CALL(*mockFoo, GetResultData()).Times(1).WillOnce(testing::Return(rd));
  EXPECT_EQ(m_TestImpl->GetResultData(), rd_exp);
}

编译及运行:

[returnCustomType]$ g++ *.cpp -lgmock -lgtest -o main
▶ [returnCustomType]$ ./main 
[==========] Running 2 tests from 1 test suite.
[----------] Global test environment set-up.
[----------] 2 tests from BarTest
[ RUN      ] BarTest.count
[       OK ] BarTest.count (0 ms)
[ RUN      ] BarTest.GetResultData
[       OK ] BarTest.GetResultData (0 ms)
[----------] 2 tests from BarTest (0 ms total)

[----------] Global test environment tear-down
[==========] 2 tests from 1 test suite ran. (0 ms total)
[  PASSED  ] 2 tests.

参考

ref:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

喜欢打篮球的普通人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值