C ++:将对象的创建与使用分开(用于测试目的)

时间:2014-10-21 05:48:30

标签: c++ unit-testing c++11 dependency-injection

假设我有如下代码。 http_client是我无法控制的外部依赖项(第三方API)。 transaction_handler是我控制的一个类,我想为它编写单元测试。

// 3rd party
class http_client
{
public:
    std::string get(std::string url)
    {
        // makes an HTTP request and returns response content as string
        // throws if status code is not 200
    }
};

//code to be tested
enum class transaction_kind { sell, buy };
enum class status { ok, error };

class transaction_handler
{
private:
    http_client client;
public:
    status issue_transaction(transaction_kind action)
    {
        try
        {
            auto response = 
                client.get(std::string("http://fake.uri/") + 
                   (action == transaction_kind::buy ? "buy" : "sell"));
            return response == "OK" ? status::ok : status::error;
        }
        catch (const std::exception &)
        {
            return status::error;
        }
    }
};

因为http_client进行网络调用,我希望能够在我的测试中使用模拟实现替换它,该模拟实现切断网络并允许测试不同的条件(ok / error / exception)。因为transaction_handler应该是内部的,我可以修改它以使其可测试但我不想越过边界(即如果可能的话我想避免指针或动态多态)。理想情况下,我想使用一种依赖注入,我的测试将注入一个模拟http_client。我不认为我可以/想要使用'穷人的DI',我会在调用者中创建http_client实现并将其传递给transaction_handler(通过const引用?std :: shared_ptr ?) - 因为我不控制http_client我必须提出一个接口和 - 在产品代码中 - 我必须将http_client包装在一个实现此接口的包装类中并将调用转发给实际/包装http_client实例。在测试代​​码中,我将创建该接口的模拟实现。接口必须是纯抽象方法,需要使用我想避免的运行时多态。另一种选择是使用模板。如果我将transaction_handler类更改为如下所示:

template <typename T = http_client>
class transaction_handler
{
private:
    T client;
public:

    transaction_handler(const std::function<T()> &create) : client(create())
    {}

    status issue_transaction(transaction_kind action)
    {
        // same as above, omitted for brevity
    }    
}

我现在可以创建一个模拟http_client类:

class http_client_mock
{
public:
    std::string get(std::string url)
    {
        return std::string("OK");
    }
};

并在我的测试中创建transaction_class对象,如下所示:

transaction_handler<http_client_mock> t(
    []() -> http_client_mock { return http_client_mock(); });

虽然我可以在产品代码中使用以下内容:

transaction_handler<> t1(
    []() -> http_client { return http_client(); });

虽然它似乎工作并满足了我的大部分要求(尽管我不喜欢实例化transaction_handler的代码需要知道http_client类型的事实 - 也许它可能是以某种方式隐藏为工厂类) - 它有意义吗?或者可能有更好的方法来做这种事情?我花了相当多的时间寻找一些简单的DI模式,使单元测试更容易,很难找到适合我需要的东西。另外,我的背景主要是C,所以也许我从错误的角度处理问题?

3 个答案:

答案 0 :(得分:1)

只需编写与您正在使用的http_client导出相匹配的测试例程。您的来源将优先链接到任何lib。

答案 1 :(得分:1)

我正在维护一个DI库,你的案例对我来说真的很有意思。

模拟是关于动态多态或编译时模拟(至少在使用C ++时)。您支付1个间接费用以获得注入所需内容的能力(仅依赖于1个接口)。

如果要使代码可测试,最好的方法是使用接口(C ++中没有接口的纯虚拟类)并仅通过构造函数注入依赖项。

如果你真的想避免多态(或者因为外部API而不能),你仍然可以接受一些不完全可测试的代码。


传统的做事方式

class ConcreteHttpClient : public virtual AbstractHttpClient { /*...*/}

class MockHttpClient : public virtual AbstractHttpClient{ /*...*/ }

你只需根据需要选择注入的内容(我故意使用“new”而不是在工作时显示一些DI框架。)

生产代码。

new TransactionHandler ( static_cast< AbstractService>( ConcreteService));

单元测试事务处理程序

new TransactionHandler ( static_cast< AbstractService>( MockService));

如果以后需要使用事务处理程序测试某个类,并且事务处理程序实现了接口

class TransactionHandler: public virtual AbstractTransactionHandler { /*...*/}

您只需要创建一个继承自AbstractTransactionHandler

的TransactionHandlerMock

当你使用接口时,优点是你可以使用依赖注入框架来避免穷人的注射。


编译时间模拟。

你提出的是一个可行的替代方案,基本上你可以通过模板

来假设一个“静态多态”

生产代码:

template <typename T = http_client>
class transaction_handler{/*...*/};

new transaction_handler( /*...*/ );

单元测试代码:

using transaction_handler_mocked = transaction_handler< http_client_mock>;

new transaction_handler_mocked( /*...*/ );

然而,问题很少:

  • 您在生产代码的每个部分都依赖“transaction_handler”类型,因此如果您更改它,则必须根据它重新编译每个文件。
  • 您不能将模拟处理程序注入模拟本身,除非您根据它更改所有类以成为接受处理程序的模板。
  • 第2点意味着基本上在复杂的项目中,每个类都依赖于彼此,增加编译时间并强制进行整个项目的重新编译,只需进行少量更改
  • 您没有使用接口(纯虚拟类),这意味着您仍然可能会意外地访问不打算访问的模板参数的字段或成员,从而使您的调试更难。

其他替代方案

  • 在链接时提供模拟,您不必模拟整个第三方库。只是你正在测试的类。大多数IDE都不会被认为是这样工作的,但可能你可以使用一些bash脚本或自定义makefile。 (例如,我这样做是为了测试依赖于C函数的代码,特别是OpenGL)

  • 在实现纯虚拟类的类后面包含您想要测试的库的有趣功能(不知道为什么要避免它)。您很有可能不需要包装所有方法,并且最终会使用较小的API来测试(只需要包装您需要使用的部分,不要在前面包装整个库)

使用模拟框架,可能还有依赖注入框架。

答案 2 :(得分:0)

我认为嘲笑它是一个好主意。由于http_client是外部依赖项,因此我选择Typemock Isolator++来处理它。请看下面的代码:

TEST_METHOD(FakeHttpClient) 
{
    //Arrange
    string okStr = "OK";
    http_client* mock_client = FAKE_ALL<http_client>();
    WHEN_CALLED(mock_client->get(ANY_VAL(std::string))).Return(&okStr);

    //Act
    transaction_handler my_handler;
    status result = my_handler.issue_transaction(transaction_kind::buy);

    //Assert
    Assert::AreEqual((int)status::ok, (int)result);
}

方法FAKE_ALL<>允许我设置所有http_client实例的行为,因此不需要注入。简单的API,代码看起来准确,您不需要更改生产代码。

希望它有所帮助!