模拟具体类:模板并避免条件编译

发布于 2024-08-28 19:25:15 字数 2069 浏览 2 评论 0原文

我正在尝试测试具有这种结构的具体对象。

class Database {
 public:
  Database(Server server) : server_(server) {}
  int Query(const char* expression) {
    server_.Connect();
    return server_.ExecuteQuery();
  }

 private:
  Server server_;
};

即它没有虚函数,更不用说定义良好的接口了。

我想要一个调用模拟服务进行测试的假数据库。更糟糕的是,我希望针对真实版本或假版本构建相同的代码,以便相同的测试代码可以:

  • 测试真实的数据库实现 - 用于集成测试
  • 测试假实现,这调用模拟服务

为了解决这个问题,我使用了一个模板化的假数据库,如下所示:

#ifndef INTEGRATION_TESTS
class FakeDatabase {
 public:
  FakeDatabase() : realDb_(mockServer_) {}
  int Query(const char* expression) {
    MOCK_EXPECT_CALL(mockServer_, Query, 3);
    return realDb_.Query();
  }
private:
  // in non-INTEGRATION_TESTS builds, Server is a mock Server with
  // extra testing methods that allows mocking
  Server mockServer_;
  Database realDb_;
};
#endif

template <class T>
class TestDatabaseContainer {
 public:
  int Query(const char* expression) {
    int result = database_.Query(expression);
    std::cout << "LOG: " << result << endl;
    return result; 
  }
private:
  T database_;
};

编辑: 请注意,假数据库必须调用真实数据库(但使用模拟服务器)。

现在,为了在它们之间进行切换,我计划使用以下测试框架:

class DatabaseTests {
 public:
#ifdef INTEGRATION_TESTS
  typedef TestDatabaseContainer<Database> TestDatabase ;
#else
  typedef TestDatabaseContainer<FakeDatabase> TestDatabase ;
#endif

  TestDatabase& GetDb() { return _testDatabase; }

 private: 
  TestDatabase _testDatabase;
};

class QueryTestCase : public DatabaseTests {
 public:
  void TestStep1() {
    ASSERT(GetDb().Query(static_cast<const char *>("")) == 3);
    return;
  }
};

我不太喜欢在真假之间进行编译时切换。

所以,我的问题是:

  1. 是否有更好的方法在数据库和 FakeDatabase 之间切换?例如,是否可以在运行时以干净的方式完成此操作?我喜欢避免#ifdefs。
  2. 另外,如果有人有更好的方法来制作模仿具体课程的假课程,我将不胜感激。

我不想在实际测试代码(QueryTestCase 类)中使用模板化代码。

也可以随意批评代码风格本身。您可以在键盘上看到此代码的编译版本

I'm trying to testing a concrete object with this sort of structure.

class Database {
 public:
  Database(Server server) : server_(server) {}
  int Query(const char* expression) {
    server_.Connect();
    return server_.ExecuteQuery();
  }

 private:
  Server server_;
};

i.e. it has no virtual functions, let alone a well-defined interface.

I want to a fake database which calls mock services for testing. Even worse, I want the same code to be either built against the real version or the fake so that the same testing code can both:

  • Test the real Database implementation - for integration tests
  • Test the fake implementation, which calls mock services

To solve this, I'm using a templated fake, like this:

#ifndef INTEGRATION_TESTS
class FakeDatabase {
 public:
  FakeDatabase() : realDb_(mockServer_) {}
  int Query(const char* expression) {
    MOCK_EXPECT_CALL(mockServer_, Query, 3);
    return realDb_.Query();
  }
private:
  // in non-INTEGRATION_TESTS builds, Server is a mock Server with
  // extra testing methods that allows mocking
  Server mockServer_;
  Database realDb_;
};
#endif

template <class T>
class TestDatabaseContainer {
 public:
  int Query(const char* expression) {
    int result = database_.Query(expression);
    std::cout << "LOG: " << result << endl;
    return result; 
  }
private:
  T database_;
};

Edit: Note the fake Database must call the real Database (but with a mock Server).

Now to switch between them I'm planning the following test framework:

class DatabaseTests {
 public:
#ifdef INTEGRATION_TESTS
  typedef TestDatabaseContainer<Database> TestDatabase ;
#else
  typedef TestDatabaseContainer<FakeDatabase> TestDatabase ;
#endif

  TestDatabase& GetDb() { return _testDatabase; }

 private: 
  TestDatabase _testDatabase;
};

class QueryTestCase : public DatabaseTests {
 public:
  void TestStep1() {
    ASSERT(GetDb().Query(static_cast<const char *>("")) == 3);
    return;
  }
};

I'm not a big fan of that compile-time switching between the real and the fake.

So, my question is:

  1. Whether there's a better way of switching between Database and FakeDatabase? For instance, is it possible to do it at runtime in a clean fashion? I like to avoid #ifdefs.
  2. Also, if anyone has a better way of making a fake class that mimics a concrete class, I'd appreciate it.

I don't want to have templated code all over the actual test code (QueryTestCase class).

Feel free to critique the code style itself, too. You can see a compiled version of this code on codepad.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

心凉 2024-09-04 19:25:15

免责声明 我在 Typemock 工作。

在以下示例中,代码是根据真实版本构建的,生产代码没有任何更改。

正在测试:

class Server
{
    public:
    void Connect(){/* real implementation*/}
    int ExecuteQuery() { return 0; // real implementation }
};

class Database{

    public:
    Database(Server server) : server_(server) {}

    int Query(const char* expression) {
           server_.Connect();
           return server_.ExecuteQuery();
    }

    private:
           Server server_;
};

测试类:

TEST_CLASS(GlobalCMethods)
{
public:

测试真实的数据库实现 - 用于集成测试:
测试真实的数据库实现意味着像在真实代码中一样使用它。下面的例子就是这样做的。但是,您可以使用模拟来更改某些逻辑,而无需更改数据库逻辑本身。
例如,如果查询执行是有条件的,您可以模拟该条件(不在本例中)并仍然执行真正的查询。

     TEST_METHOD(IntegrationTest)
     {
            Server server;
            Database db(server);
            int QueryResult = db.Query("dummy expression");
            Assert::AreEqual(0, QueryResult);
     }

测试假实现,它调用模拟服务:
由于您的数据库使用 Server 类来执行查询,因此模拟 Server 基本上会将真正的连接带出游戏。
我使用 Typemock Isolator++ 进行模拟。我使用 FAKE_ALL 来模拟服务器的实例化,因为您的示例服务器是按值传递的(不是一个好主意),并且在传递时正在创建和复制它。
使用 FAKE_ALL 时,您将获得一个对象的代理 (fakeServer),该对象替换代码中的服务器。代理上设置的所有行为都适用于代码中的对象 (server_)。

     TEST_METHOD(UnitTest)
     {
            Server* fakeServer = FAKE_ALL<Server>();
            WHEN_CALLED(fakeServer->ExecuteQuery()).Return(3);
            Database db(*fakeServer);

            int QueryResult = db.Query("dummy expression");

            Assert::AreEqual(3, QueryResult);
     }
};

Disclaimer I work at Typemock.

In the following example, the code is built against the real version, no changes in the production code.

Under Test:

class Server
{
    public:
    void Connect(){/* real implementation*/}
    int ExecuteQuery() { return 0; // real implementation }
};

class Database{

    public:
    Database(Server server) : server_(server) {}

    int Query(const char* expression) {
           server_.Connect();
           return server_.ExecuteQuery();
    }

    private:
           Server server_;
};

Test Class:

TEST_CLASS(GlobalCMethods)
{
public:

Test the real Database implementation - for integration tests:
Testing real database implementation means using it as you would in your real code. The following example does just that. You can however alter some of the logic using mocking without changing the database logic itself.
For instance, if the query execution was conditional, you could mock that condition (not in this example) and still execute the real query.

     TEST_METHOD(IntegrationTest)
     {
            Server server;
            Database db(server);
            int QueryResult = db.Query("dummy expression");
            Assert::AreEqual(0, QueryResult);
     }

Test the fake implementation, which calls mock services:
As your database uses the Server class to do queries, mocking the Server basically takes the real connection out of the game.
I used Typemock Isolator++ for the mocking. I used FAKE_ALL to mock the Instantiation of Server since in your example Server is passed by value (not a good idea) and it's being created and duplicated as it's passed.
When using FAKE_ALL you get a proxy (fakeServer) to the object that replaces the server in your code. all behaviors set on the proxy apply to the object in your code (server_).

     TEST_METHOD(UnitTest)
     {
            Server* fakeServer = FAKE_ALL<Server>();
            WHEN_CALLED(fakeServer->ExecuteQuery()).Return(3);
            Database db(*fakeServer);

            int QueryResult = db.Query("dummy expression");

            Assert::AreEqual(3, QueryResult);
     }
};
柳若烟 2024-09-04 19:25:15

您可以将测试的代码和模拟/伪造的代码放在它们自己的源文件中。然后您可以链接到您想要使用的任何版本。

我个人不会将测试代码与生产代码放在一起。这是一个单独的项目。

You could put the tested code and the mock/fake in their own source files. You could then link with whichever version you wanted to use.

I personally don't put my testing code in with my production code. It's a separate project.

相权↑美人 2024-09-04 19:25:15

如果您想在运行时期间在数据库实现之间进行选择,那么显然您需要运行时多态性而不是静态多态性。因此,将依赖于“Database”概念的模板类 TestDatabaseContainer 更改为依赖于“Database”接口的常规类,并让 FakeDatabase 和真正的 Database 继承该接口:(

class IDatabase
{
public:
    int Query(const char* expression) = 0;
};
class TestDatabaseContainer 
{
 public:
  TestDatabaseContainer(IDatabase& d) : database_(d) {}
  int Query(const char* expression) {
    int result = database_.Query(expression);
    std::cout << "LOG: " << result << endl;
    return result; 
  }
private:
  IDatabase& database_;
};
class DatabaseTests 
{
 public:
  TestDatabaseContainer GetDb() 
  { 
      if(IntegrationTests)
          return TestDatabaseContainer(*(new Database));
      else 
          return TestDatabaseContainer(*(new FakeDatabase)); 
  }
};

我对任何内存泄漏不承担任何责任:)所以修改适当地)

If you want to choose between implementation of database during runtime, then apparently you need runtime polymorphism instead of static polymorphism. So change template class TestDatabaseContainer that relies on the concept "Database" to regular class that relies on the interface "Database" and let FakeDatabase and real Database inherit that interface:

class IDatabase
{
public:
    int Query(const char* expression) = 0;
};
class TestDatabaseContainer 
{
 public:
  TestDatabaseContainer(IDatabase& d) : database_(d) {}
  int Query(const char* expression) {
    int result = database_.Query(expression);
    std::cout << "LOG: " << result << endl;
    return result; 
  }
private:
  IDatabase& database_;
};
class DatabaseTests 
{
 public:
  TestDatabaseContainer GetDb() 
  { 
      if(IntegrationTests)
          return TestDatabaseContainer(*(new Database));
      else 
          return TestDatabaseContainer(*(new FakeDatabase)); 
  }
};

(I'm in no way responsible for any memory leaked :) so modify it appropriately)

小梨窩很甜 2024-09-04 19:25:15

如果数据库没有虚函数:

//ancestor of database containers
class TestDatabase
{
public:
  virtual int Query(const char* expression) = 0;
}; 
//TestDatabaseContainer is the same as in the example, but
//inherits TestDatabase
template <class T>
class TestDatabaseContainer: public TestDatabase
{
//...
};
class DatabaseTests 
{
 public:
  TestDatabase* GetDb() 
  { 
      if(IntegrationTests)
          return new TestDatabaseContainer<Database>();
      else 
          return new TestDatabaseContainer<FakeDatabase>();
  }
  //...
};

In case the database has no virtual functions:

//ancestor of database containers
class TestDatabase
{
public:
  virtual int Query(const char* expression) = 0;
}; 
//TestDatabaseContainer is the same as in the example, but
//inherits TestDatabase
template <class T>
class TestDatabaseContainer: public TestDatabase
{
//...
};
class DatabaseTests 
{
 public:
  TestDatabase* GetDb() 
  { 
      if(IntegrationTests)
          return new TestDatabaseContainer<Database>();
      else 
          return new TestDatabaseContainer<FakeDatabase>();
  }
  //...
};
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文