假设我有如下代码。 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,所以也许我从错误的角度处理问题?
答案 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( /*...*/ );
然而,问题很少:
其他替代方案
在链接时提供模拟,您不必模拟整个第三方库。只是你正在测试的类。大多数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,代码看起来准确,您不需要更改生产代码。
希望它有所帮助!