注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

Koala++'s blog

计算广告学 RTB

 
 
 

日志

 
 

Google Mock进阶篇 [2] (Google Mock Cookbook译文)  

2012-04-19 20:14:09|  分类: C++ |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |

Simplifying the Interface without Breaking Existing Code

    有时候一个函数有相当长的参数列表,那Mock的时候是相当无趣的,比如:

class LogSink {

 public:

  ...

  virtual void send(LogSeverity severity, const char* full_filename,

                    const char* base_filename, int line,

                    const struct tm* tm_time,

                    const char* message, size_t message_len) = 0;

};

    这个函数的参数列表很长且难用( 这么说吧,message参数甚至都不是以’\0’结尾的 )。如果我们执意要Mock它,那结果必是不雅的。然而如果我们试着简化这个接口,又需要将所有使用这个接口的代码全部改了,这通常是不可行的。

    技巧就是在Mock类中修改这个函数:

class ScopedMockLog : public LogSink {

 public:

  ...

  virtual void send(LogSeverity severity, const char* full_filename,

                    const char* base_filename, int line, const tm* tm_time,

                    const char* message, size_t message_len) {

    // We are only interested in the log severity, full file name, and

    // log message.

    Log(severity, full_filename, std::string(message, message_len));

  }

 

  // Implements the mock method:

  //

  //   void Log(LogSeverity severity,

  //            const string& file_path,

  //            const string& message);

  MOCK_METHOD3(Log, void(LogSeverity severity, const string& file_path,

                         const string& message));

};

    通过定义一个新有较少参数的Mock函数,我们让Mock类更易用。

Alternative to Mocking Concrete Classes

    你经常会发现你正在用一些没有针对接口实现的类。你为了可以用这种类( 且称为Concrete )来测试自己的代码,你可能会试着将Concrete的函数变为虚函数,然后再去Mock它。

    请不要这样做。

    将非虚函数改为虚函数是一个重大决定。这样做之后,子类会改变父类的行为。这样就会更难保持类的不变性,而从降低了你对类的控制力。你只应在一个合理的理由下将非虚函数变为虚函数。

    直接Mock具体的类会产生类和测试的高度耦合,任何对类的小的改动都会让你测试失效,这会让你陷入维护测试的痛苦中。

    为了避免这种痛苦,许多程序员开始了“针对接口”的实践:并不直接调用Concrete类,而是定义一个接口去调用Concrete类。然后你在Concrete类之上实现这个接口,即配接器。

    这种技术可能会带来一些负担:

l  你要为虚函数的调用买单( 通常不是问题 )

l  程序员需要掌握更多的抽象

但是,它同时也能巨大的好处,当然也有更好的可测性:

l  ConcreteAPI也许并不是很适合你的问题领域,因为你可能不是这个API唯一的调用方。通过设计你自己的接口,你有一个将这个类修改成自己所需的类的机会,你可加入一些特定功能,重命名接口函数,等等,你可以做的不是只是减少几个自己不使用的API。这可以让你自己以更自然的方式实现你的代码,因为它有更好的可读性,更好的可维护性,你也会有更高的编程效率。

l  如果Concrete的实现改变了,你不需要重写与改动相关的所有测试。相反你可以将改动在你自己的接口中隐藏,使你的调用代码和测试与Concrete改动绝缘。

有些人会担心如果每个人都在实践这个技术,将会产生大量的重复代码。这个担心是可以理解的。但是,有两个理由可以证明这种情况可能不会发生。

l  不同的工程可能会以不同的方式使用Concrete,所以最适合每个工程的接口是不同的。所以每个工程都有在Concrete之上的自己的领域相关的接口,这些接口是各不相同的。

l  如果有很多的工程用相同的接口,它们可以共用一个接口,就像它们共用Concrete一样。你可以在Concrete类的旁边提交接口和配接器的代码( 也许是在一个contrib子目录中 )并让许多工程使用它。

你需要仔细衡量针对你特定问题这种做法的优缺点,但我可以向你保证的是:Java世界的人已经实践这种方法很久了,并且它已经被证明在很广泛的领域中是一种有效的技术。

Delegating Calls to a Fake

    有时你已经有一个对某一接口的Fake实现了。比如:

class Foo {

 public:

  virtual ~Foo() {}

  virtual char DoThis(int n) = 0;

  virtual void DoThat(const char* s, int* p) = 0;

};

 

class FakeFoo : public Foo {

 public:

  virtual char DoThis(int n) {

    return (n > 0) ? '+' :

        (n < 0) ? '-' : '0';

  }

 

  virtual void DoThat(const char* s, int* p) {

    *p = strlen(s);

  }

};

    现在你想要Mock这个接口,比如你想在它上面设置期望。但是你还想用FakeFoo作为Mock类函数的默认行为,当然你可以选择将代码复制到Mock对象里,但是这会有很大的工作量。

    当你用Google Mock来定义Mock类,你可以代理对象的默认行为给你已经有的Fake类,用下面的方法:

using ::testing::_;

using ::testing::Invoke;

 

class MockFoo : public Foo {

 public:

  // Normal mock method definitions using Google Mock.

  MOCK_METHOD1(DoThis, char(int n));

  MOCK_METHOD2(DoThat, void(const char* s, int* p));

 

  // Delegates the default actions of the methods to a FakeFoo object.

  // This must be called *before* the custom ON_CALL() statements.

  void DelegateToFake() {

    ON_CALL(*this, DoThis(_))

        .WillByDefault(Invoke(&fake_, &FakeFoo::DoThis));

    ON_CALL(*this, DoThat(_, _))

        .WillByDefault(Invoke(&fake_, &FakeFoo::DoThat));

  }

 private:

  FakeFoo fake_;  // Keeps an instance of the fake in the mock.

};

    你现在可以像以前一样在你的测试中使用MockFoo。只是你要记得如果你没有明确地设置ON_CALL或是EXPECT_CALL()的行为,那Fake函数就会被调用:

using ::testing::_;

 

TEST(AbcTest, Xyz) {

  MockFoo foo;

  foo.DelegateToFake(); // Enables the fake for delegation.

 

  // Put your ON_CALL(foo, ...)s here, if any.

 

  // No action specified, meaning to use the default action.

  EXPECT_CALL(foo, DoThis(5));

  EXPECT_CALL(foo, DoThat(_, _));

 

  int n = 0;

  EXPECT_EQ('+', foo.DoThis(5));  // FakeFoo::DoThis() is invoked.

  foo.DoThat("Hi", &n);           // FakeFoo::DoThat() is invoked.

  EXPECT_EQ(2, n);

}

一些技巧:

l  如果你不想用FakeFoo中的函数,你仍然可以通过在ON_CALL或是在EXPECT_CALL中用.WillOnce() / .WillRepeated()覆盖默认行为。

l  DelegateToFake()中,你只需要代理那些你要用的函数的Fake实现。

l  这里所讲的技术对重载函数也是适用的,但你需要告诉编译器你是指重载函数中的哪一个。消除一个Mock函数的歧义( 即你在ON_CALL中指定的 ),参见“Selecting Between Overloaded Functions”一节,消除一个Fake函数的歧义( 即在Invoke中的 ),使用static_cast来指定函数的类型。

l  将一个Mock和一个Fake搅在一起通常是某种错误的信号。也许你还没有习惯基于交互方式的测试。或是你的接口融合了过多的角色,应该把这个接口分开。所以别滥用这个技术。我们建议这仅应该用做你重构你代码时的中间步骤。

再思考一个Mock和一个Fake混在一起的问题,这里有一个例子来说明为什么这是一个错误信号:假设你有一个System类,它实现了一些低层的系统操作。具体一些,它处理文件操作和I/O操作。假设你想测试你的代码是如何使用System来进行I/O操作,你只是想让文件操作工作正常就可以了。如果你想要Mock整个System类,你就必须提供一个关于文件操作的Fake实现,这表明System拥有了太多的角色。

相反,你可以定义一个FileOps和一个IOOps接口来拆分System的功能。然后你可以Mock IOOps而不用Mock FileOps

Delegating Calls to a Real Object

    当使用测试doubles( 替身的意思 mocks, fakes, stubs 等等 )时,有时它们的行为与真实对象的行为不一样。这种差别可能是有意为之( 比如模拟一个错误,假设你的代码中有错误处理逻辑 )或是无意的。如果你的Mock与真实对象的差别是错误造成的,你可能会得到能通过测试,却在正式代码中失败的代码。

    你可以使用delegating-to-real技术来保证你的Mock与真实对象有着相同的行为,并且拥有验证调用的能力。这个技术与delegating-to-fake技术很相似,区别在于我们使用真实对象而不是一个Fake。下面是一个例子:

using ::testing::_;

using ::testing::AtLeast;

using ::testing::Invoke;

 

class MockFoo : public Foo {

 public:

  MockFoo() {

    // By default, all calls are delegated to the real object.

    ON_CALL(*this, DoThis())

        .WillByDefault(Invoke(&real_, &Foo::DoThis));

    ON_CALL(*this, DoThat(_))

        .WillByDefault(Invoke(&real_, &Foo::DoThat));

    ...

  }

  MOCK_METHOD0(DoThis, ...);

  MOCK_METHOD1(DoThat, ...);

  ...

 private:

  Foo real_;

};

...

 

  MockFoo mock;

 

  EXPECT_CALL(mock, DoThis())

      .Times(3);

  EXPECT_CALL(mock, DoThat("Hi"))

      .Times(AtLeast(1));

  ... use mock in test ...

    用上面的代码,Google Mock会验证你的代码是否做了正确的调用( 有着正确的参数,以正确的顺序,有着正确的调用次数 ),并且真实的对象会处理这些调用( 所以行为将会和正式代码中表现一致 )。这会让你在两个世界都表现出色。

Delegating Calls to a Parent Class

    理想中,你应该针接口编程,并接口的函数都是虚函数。现实中,有时候你需要Mock一个非纯虚函数( 比如,它已经有了实现 )。比如:

class Foo {

   public:

    virtual ~Foo();

 

    virtual void Pure(int n) = 0;

    virtual int Concrete(const char* str) { ... }

  };

 

  class MockFoo : public Foo {

   public:

    // Mocking a pure method.

    MOCK_METHOD1(Pure, void(int n));

    // Mocking a concrete method.  Foo::Concrete() is shadowed.

    MOCK_METHOD1(Concrete, int(const char* str));

  };

    有时你想调用Foo::Concrete()而不是MockFoo::Concrete()。也许你想将它做为Stub行为的一部分,或是也许你的测试根本不需要Mock Concrete()  (当你不需要Mock一个新的Mock类的任何一个函数时候,而定义一个新的Mock类时,将会是出奇的痛苦)

    解决这个问题的技巧就是在你的Mock类中留下一个后门,可以通过它去访问基类中的真实函数:

class MockFoo : public Foo {

   public:

    // Mocking a pure method.

    MOCK_METHOD1(Pure, void(int n));

    // Mocking a concrete method.  Foo::Concrete() is shadowed.

    MOCK_METHOD1(Concrete, int(const char* str));

 

    // Use this to call Concrete() defined in Foo.

    int FooConcrete(const char* str) { return Foo::Concrete(str); }

  };

    现在你可以在一个动作中调用Foo::Concrete()

  using ::testing::_;

  using ::testing::Invoke;

  ...

    EXPECT_CALL(foo, Concrete(_))

        .WillOnce(Invoke(&foo, &MockFoo::FooConcrete));

    或是告诉Mock对象你不想MockConcrete()

using ::testing::Invoke;

  ...

    ON_CALL(foo, Concrete(_))

        .WillByDefault(Invoke(&foo, &MockFoo::FooConcrete));

    ( 为什么我们不只写Invoke(&foo, &Foo::Concrete)?如果你这样做,MockFoo::Concrete会被调用( 从而导致无穷递归),因为Foo::Concrete()是虚函数。这就是C++的工作方式 )

  评论这张
 
阅读(2974)| 评论(1)
推荐 转载

历史上的今天

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2017