带有不在标题中的函数的单元测试 C

Posted

技术标签:

【中文标题】带有不在标题中的函数的单元测试 C【英文标题】:Unit Testing C With Functions Not in Header 【发布时间】:2018-03-13 20:57:25 【问题描述】:

我开始进行单元测试,但我无法理解某些内容。我的斗争归结为我将如何测试仅在 .c 源代码中且未在 .h 标头中声明的函数。有些函数不需要在实现之外调用,因为它们只与该特定文件相关。由于它们对程序的其他部分不可见,这意味着我的单元测试用例文件看不到这些内部功能,因此使我无法测试它们。我已经通过在测试用例文件中使用前向声明解决了这个问题,但这似乎有点混乱,如果我修改函数参数,去更改会很痛苦。

这些功能难道不应该被单元测试覆盖吗?我已经读过关于 OOP 的内容,您不应该测试私有函数,因为它们是通过公共函数隐式测试的,但是不涵盖这些函数会让人感到不舒服(其中一些可能会变得相当复杂)。

【问题讨论】:

有一些头文件只用于单元测试吗? 有些功能根本无法通过单元测试直接测试。我特别想到那些有内部联系的人。一旦你掌握了这一点,也许你会更愿意只测试模块的外部接口。此外,就风格而言,旨在仅由同一翻译单元中的其他函数调用的函数应该使用内部链接声明,而所有其他函数都应该在头文件中声明,所以如果有任何可用于测试但未在标头中声明的函数,那么您可以认为这本身就是一个缺陷。 @JohnBollinger:我认为您在谈论黑盒测试,但是白盒测试对于许多项目来说非常重要,我们坚持在我工作的地方进行。对于我们的项目,我们为我们的外部接口创建了一个公共头文件(简单的 .h 后缀),并为我们的私有接口创建了一个私有头文件 (_pi.h),供少数需要访问私有接口的人使用:白盒测试、审计数据结构、内部配置和诊断以及调试工具。当然,_pi.h 后缀只是一种约定,但在实践中效果很好。 @ScottK,不,你描述的不是我要区分的。具有内部链接的函数不能直接从另一个翻译单元调用,例如包含单元测试的翻译单元,因为它们的标识符的范围是它们自己的翻译单元——这就是内部链接的含义,这就是不打算从其 TU 外部调用的函数应该如何声明。我确实断言应该在头文件中声明具有外部链接的函数,但这并不意味着它们都必须是 public 头文件。 维基参考 White-box testing 【参考方案1】:

黑盒测试是关于测试您的公开可见界面和用户之间的软件合同。为了对此进行测试,通常使用工具或单独的测试程序创建一组测试用例,#include 是您的头文件 .h,它定义了您的外部接口。听起来你已经有了这个。太好了!

缺少的是白盒测试的概念。对于电信、铁路、航空航天或任何其他需要高度保证高可用性和质量的行业,这与黑盒测试一样重要。

对于白盒测试,创建一个单独的“私有”接口,仅供白盒测试程序使用。请注意,在C 中,您可以为给定的 C 实现文件创建多个头文件。从编译器的角度来看,头文件的数量或其名称并没有真正的强制执行。最好遵守项目或团队规定的约定。

对于我们的项目,我们为我们的外部接口创建了一个公共标头(带有简单的 .h 后缀),并为我们的私有接口创建了一个私有标头 (_pi.h),旨在供少数需要访问私有接口的人使用,例如白盒测试、审计数据结构、内部配置和诊断以及调试工具。当然,_pi.h 后缀只是一种约定,但在实践中效果很好。

白盒测试对于测试您的内部功能和数据结构非常有用,并且可以远远超出黑盒测试的限制。例如,我们使用白盒测试用例来测试内部接口,并查看当我们的数据结构损坏时会发生什么,以及测试我们的代码在内部传递意外参数值时的行为等极端情况。

例如,假设我们有一个名为 foo.c 的文件,我们想要对其执行白盒测试。然后我们将为外部和内部用户创建两个头文件:foo.h 和 foo_pi.h。

文件 foo.h

#ifndef FOO_H
#define FOO_H

typedef int FooType;

// Public header for Foo
void Foo(FooType fooVal);
void Bar(void);

#endif

文件 foo_pi.h

#ifndef FOO_PI_H
#define FOO_PI_H

// PI should also include the public interface
#include "foo.h"

// Private header for Foo
// Called by White Box test tool 
void FooBar_Test1(FooType fooVal);
void Foo_Internal(void);
void Bar_Internal(void);

#endif

文件 foo.c

#include "foo.h"
#include "foo_pi.h"
// Notice you need to include both headers

// Define internal helpers here
static FooType myFooVal = 0; 
void FooBar_Test1(FooType fooVal) myFooVal = fooVal;

void Foo_Internal() Bar_Internal();
void Bar_Internal(void) myFooVal++;      


// Define external interfaces after the helpers
void Foo(FooType fooVal) myFooVal = fooVal; Foo_Internal();
void Bar(void)           Bar_Internal();

// Main() not typically included 
// if this is just one module of a bigger project!
int main(int argc, char** argv)

 Foo(argc);

如果您对所有 #ifndef/#define/#endif 的东西感到困惑,这些是 CPP 宏,并且这种用法在 C 中并未强制执行,但它是一种广泛使用的约定。更多详情请见https://***.com/a/42744341/6693299

虽然我没有在上面的示例中展示它,但 Foobar_test() 例程(以及任何其他内部测试方法,通常会放置在为内部测试功能保留的单独模块中。然后可以将它们打包出来产品。除了我不会在这里描述的一些花哨的 CPP 预处理之外,您还可以有条件地编译出私有头文件和测试函数,并使接口对生产负载安全(在完成白盒测试之后)。但是可能是太详细了!

【讨论】:

【参考方案2】:

@ScottK 对黑盒与白盒测试的描述很有用。我的白盒测试方法不同。

根据项目结构,我要么

将单元测试与被测单元放在同一个文件中,通常在文件末尾,全部位于#if UNIT_TEST ... #endif

#include单元测试代码中被测C文件;这提供了对所有内部函数的访问,而无需对原始源代码进行任何更改

【讨论】:

这也是一个很好的方法。我喜欢它的简单。

以上是关于带有不在标题中的函数的单元测试 C的主要内容,如果未能解决你的问题,请参考以下文章

数据库驱动的 CodeIgniter Web 应用程序中的单元测试

使用 OCMock 和 MagicalRecord 进行单元测试

在编译时重写或破坏 C 单元测试的函数链接

SonarQube 5.6 中的 C# 单元测试结果

Angular中的单元测试点击事件

Unity 单元测试 - 检查带有参数的函数调用是不是引发异常