我正在寻找一个在C中进行DI的良好技术解决方案。
我已经看到了一些DI问题,但我没有看到任何实际的例子或具体的实施建议。
所以,假设我们有以下情况:
我们在c中有一组模块;我们想重构这些模块,以便我们可以使用DI来运行单元测试等等。
每个模块实际上由一组c函数组成:
module_function(...);
模块相互依赖。 IE浏览器。通常,您可能会打电话,如:
int module1_doit(int x) {
int y = module2_dosomethingelse(x);
y += 2;
return(y);
}
DI的正确方法是什么?
可能的解决方案似乎是:
(1)为所有模块函数使用函数指针,并在调用函数时执行此(或类似):
int y = modules-> module2-> dosomethingelse(x);
(2)使用相同的符号编译多个库(mock,std等),并在正确的实现中动态链接。
(2)似乎是正确的方法,但是很难配置并且烦人地迫使你为每个单元测试构建多个二进制文件。
(1)似乎它可能会起作用,但在某些时候你的DI控制器会陷入需要动态调用通用工厂函数的情况(void ( factory)( ...)说)还有许多其他需要在运行时注入的模块吗?
在c中还有另一种更好的方法吗?
这种“正确”的做法是什么?
答案 0 :(得分:9)
我的结论是,在C语言中没有“正确”的方法。与其他语言相比,它总是比较困难和繁琐。但是,我认为重要的是,不要为了单元测试而混淆代码。将所有内容都设置为C中的函数指针可能听起来不错,但我认为它最终会让代码变得可怕。
我最近的方法是保持简单。除了文件顶部的小#ifdef UNIT_TESTING
之外,我不会更改C模块中的任何代码以进行外部和内存分配跟踪。然后我接受模块并编译它,删除所有依赖项,以便它失败链接。一旦我查看了未解析的符号以确保它们是我想要的,我运行一个脚本来解析这些依赖项并为所有符号生成存根原型。这些都被转储到单元测试文件中。 YMMV取决于外部依赖项的复杂程度。
如果我需要在一个实例中模拟一个依赖项,在另一个实例中使用真实的,或者在另一个实例中使用它,那么我最终会为一个被测模块提供三个单元测试模块。拥有多个二进制文件可能并不理想,但它是C的唯一真正选项。但它们都可以同时运行,所以对我来说这不是一个真正的问题。
答案 1 :(得分:9)
这是Ceedling的完美用例。
Ceedling是一个排序伞项目,它将Unity和CMock(以及其他东西)结合在一起,它们可以自动完成你所描述的大量工作。
一般来说,Ceedling / Unity / CMock是一组ruby脚本,它们扫描你的代码并根据你的模块头文件自动生成模拟,以及测试运行器,它们可以找到所有的测试并生成运行它们的运行器
为每个测试套件生成一个单独的测试运行二进制文件,在您的测试套件实现中按照您的请求链接相应的模拟和实际实现。
我最初犹豫是否将ruby作为依赖我们的构建系统进行测试,它似乎有很多复杂性和魔力,但在尝试完并使用自动生成的模拟代码编写一些测试之后我就是钩状。
答案 2 :(得分:9)
我认为在C中使用DI没有任何问题。请参阅:
http://devmethodologies.blogspot.com/2012/07/dependency-injection.html
答案 3 :(得分:2)
在这方面有点晚了,但这是我工作的最近话题。
我已经看到它完成的两个主要方法是使用函数指针,或将所有依赖项移动到特定的C文件。
后者的一个很好的例子是FATFS。 http://elm-chan.org/fsw/ff/en/appnote.html
fatfs的作者提供了大量的库函数,并为库的用户编写了某些特定的依赖项(例如串行外设接口函数)。
函数指针是另一个有用的工具,使用typedef帮助防止代码过于丑陋。
以下是我的模数转换器(ADC)代码的一些简化片段:
typedef void (*adc_callback_t)(void);
bool ADC_CallBackSet(adc_callback_t callBack)
{
bool err = false;
if (NULL == ADC_callBack)
{
ADC_callBack = callBack;
}
else
{
err = true;
}
return err;
}
// When the ADC data is ready, this interrupt gets called
bool ADC_ISR(void)
{
// Clear the ADC interrupt flag
ADIF = 0;
// Call the callback function if set
if (NULL != ADC_callBack)
{
ADC_callBack();
}
return true; // handled
}
// Elsewhere
void FOO_Initialize(void)
{
ADC_CallBackSet(FOO_AdcCallback);
// Initialize other FOO stuff
}
void FOO_AdcCallback(void)
{
ADC_RESULT_T rawSample = ADC_ResultGet();
FOO_globalVar += rawSample;
}
Foo的中断行为现已注入ADC的中断服务程序。
您可以更进一步,将函数指针传递给FOO_Initialize,以便所有依赖性问题都由应用程序管理。
//dependency_injection.h
typedef void (*DI_Callback)(void)
typedef bool (*DI_CallbackSetter)(DI_Callback)
// foo.c
bool FOO_Initialize(DI_CallbackSetter CallbackSet)
{
bool err = CallbackSet(FOO_AdcCallback);
// Initialize other FOO stuff
return err;
}
答案 4 :(得分:1)
您可以使用两种方法。无论你是否真的想要,正如Rafe指出的那样,取决于你。
首先:在静态库中创建“动态”注入方法。链接到库并在测试期间简单地替换它。瞧,这个方法被取代了。
第二:简单地提供基于预处理的编译时替换:
#ifndef YOUR_FLAG
/* normal method versions */
#else
/* changed method versions */
#endif
/* methods that have no substitute */