使用CRTP模拟类

时间:2015-04-15 08:31:27

标签: c++ unit-testing mocking crtp

我正在尝试模拟没有任何虚函数的类。我曾经读过Curiously_recurring_template_pattern(CRTP)可以帮助实现这一目标。

这是代码。我正在尝试对单元测试函数getMyClassValue

// file myclass.h
template<typename T>
struct MyClass_t {
    int hello() {
        return (static_cast<T*>(this))->hello_impl();
    }
};

/*
 Earlier MyClassImpl was just simple class like
 struct MyClassImpl {
    int hello() {
       return 110;
     }
 };

 // I changed it to below for making it mockable. Using CRTP.
*/

struct MyClassImpl : public MyClass_t<MyClassImpl> {
    int hello_impl() {
        return 110;
    }
};

typedef MyClassImpl *MyClass;

int getMyClassValue(MyClass doc) { 
    return doc->hello(); 
}

// file main.cpp
#include <iostream>
/*
int main() {
    MyClass myclass = new MyClassImpl();
    std::cout << getMyClassValue(myclass);
    delete myclass;
    return 0;
}
*/

// file test.cpp
struct MyClassImplTest : public MyClass_t<MyClassImplTest>, 
                         public virtual MyClassImpl {
    int hello_impl() {
        return 2;
    }
};

int main() {
    auto myclass = new MyClassImplTest();
    std::cout << getMyClassValue(myclass);
    delete myclass;
    return 0;
}

我在控制台中收到110而不是2。 为什么会这样?由于我使用指针,切片不应该发生。

我如何实现模拟?

2 个答案:

答案 0 :(得分:1)

实际上,这与切片无关。真正发生的事情非常简单:

  • getMyClassValue接受MyClassImpl*类型的指针并调用hello
  • 调用解析为hello的父MyClassImpl中的MyClass_t<MyClassImpl> <{1}}
  • hello static将指针T*投射到MyClassImpl*并调用hello_impl
  • MyClassImpl::hello_impl返回110

hello的调用无法解析为MyClass_t<MyClassImplTest>的{​​{1}}父级,因为它在类型为MyClassImplTest的指针上调用,而不是在{{1}类型的指针上调用}}。尝试在MyClassImpl*上调用MyClassImplTest将无效,除非您指定要使用哪个父项,因为调用不明确。

使用虚函数实现模拟很简单。没有,没有那么多。

答案 1 :(得分:0)

MyClass是指向MyClassImpl的指针。在其上调用hello会调用MyClass_t<MyClassImpl>::hello,将this投射到MyClassImpl*,从而调用MyClassImpl::hello_impl

如果您正在使用编译时“多态”(CRTP),则所有类型在编译时都必须正确。

我认为实现这项工作的唯一方法是让所有成为模板。您只需将实现类型作为模板参数/参数传递。 getMyClassValue的一个例子是:

template <class T_MyClass>
int getMyClassValue(MyClass_t<T_MyClass> *doc)
{
  return doc->hello();
}

struct MyClassImplTest : public MyClass_t<MyClassImplTest>
{
    int hello_impl() {
        return 2;
    }
};

int main()
{
    auto myclass = new MyClassImplTest();
    std::cout << getMyClassValue(myclass);
    delete myclass;
    return 0;
}

这必须适用于所有生产代码和所有测试代码。通过将所有类型收集到“类型上下文”中可以使其更加可行,该类型上下文将存储每个接口的实现类型。然后将明确地传递它。

struct ProductionContext
{
  typedef MyClassImpl MyClass;
  typedef MyClass2Impl MyClass2;
};

struct Mock1Context
{
  typedef MyClassImplTest MyClass;
  typedef MyClass2Impl MyClass2;
};

struct Mock2Context
{
  typedef MyClassImpl MyClass;
  typedef MyClass2ImplTest MyClass2;
};

template <class T_Context>
int getMyClassValue(typename T_Context::MyClass *doc)
{
  return doc->hello();
}

int main()
{
    auto myclass = new MyClassImplTest();
    std::cout << getMyClassValue<Mock1Context>(myclass);
    delete myclass;
    return 0;
}

同样,生产和测试代码的每一点都必须由T_Context模板化。为了防止只需要包含头文件的所有内容,您可以使用您打算使用的所有上下文显式实例化模板。

对我来说,这会非常笨拙,但它的解决方案。


旁注:请不要隐藏typedef后面的指针。 typedef MyClassImpl *MyClass;是纯粹的混淆,永远不会通过代码审核。