我已经了解了一些有关单元测试的内容,想知道您是如何进行单元测试的。显然,单元测试应该将程序分解为非常小的“单元”,然后从那里测试功能。
但我想知道,仅对一个类进行单元测试是否足够?还是您进一步对算法、公式等进行单元测试?或者扩大范围进行asp页面/功能的单元测试?或者您根本不进行单元测试?
我已经了解了一些有关单元测试的内容,想知道您是如何进行单元测试的。显然,单元测试应该将程序分解为非常小的“单元”,然后从那里测试功能。
但我想知道,仅对一个类进行单元测试是否足够?还是您进一步对算法、公式等进行单元测试?或者扩大范围进行asp页面/功能的单元测试?或者您根本不进行单元测试?
我将单元测试作为一种工具来衡量代码变更(例如重构,修复错误,添加功能)后是否仍然能够正常工作。 由于我使用Java,因此主要使用JUnit编写自动化单元测试。只需调用一条命令行脚本,便可运行数百个测试用例,以验证代码是否存在问题。
我单元测试的是功能,而不是个别方法或类。编写和维护单元测试的开销并不小,一般来说我不建议为每一点代码编写单元测试。然而,对于功能进行单元测试是值得的,因为客户为此付费。
以下是我认为在单元测试中有用的通用指导原则:
1)识别边界对象(Win / WebForms,CustomControls等)。
2)识别控制对象(业务层对象)。
3)确保至少编写单元测试以测试由边界对象调用的控制对象公共方法。
这样,您就可以确保覆盖应用程序的主要功能方面,并且不会冒微观测试的风险(除非您想这样做)。
我们通常使用Java库为机器编写程序。一个程序通常由20多个库组成,因此我们的做法是对每个库进行单元测试。这并不是一项容易的任务,因为很多时候库之间的耦合非常紧密,这种情况下就不太可能进行单元测试。
我们的代码并不像我们希望的那样模块化,但出于兼容性问题,我们必须接受它,并且在许多情况下,打破耦合意味着打破兼容性。
我尽可能地测试公共接口(我使用的是C++,但语言并不重要)。最重要的方面是在编写代码时编写测试(紧接着或之后)。从经验上来看,以这种方式开发将导致更可靠的代码,并使其更易于维护(因为破坏测试的更改将立即显而易见)。
对于所有项目,我建议您从一开始就考虑测试-如果您编写了一个依赖于另一个复杂类的类,则使用接口,以便在测试时“模拟”更复杂的对象(数据库访问,网络访问等)。
编写大量测试似乎会减慢您的速度,但实际上,在项目的整个生命周期内,您将花费更少的时间修复错误。
经常进行测试-如果它可以破坏,那么它就会破坏-最好在测试时破坏它,而不是在客户尝试使用它时破坏它。
仅对一个类进行单元测试是不够的。类相互配合,这也必须进行测试。
除了类之外,还有更多的单元:
当然,还有不同形式的测试,例如集成和验收。
我测试那些我认为困难的事情,那些我认为可能会改变的事情,接口以及我必须修复的事情。而且我大多数时候都从测试开始,试图确保我理解我要解决的问题。
仅仅因为编译通过并不意味着它能运行!这就是单元测试的本质。试试代码,确保它正在做你认为它在做的事情。
让我们面对现实吧,如果你从Matlab中带来一个矩阵变换,很容易在某个地方搞错加号或减号。那种事情很难发现。如果不试一下,你就不知道它是否能正确工作。调试100行代码比调试100,000行代码要容易得多。
有些人会走向极端,他们试图测试每一个可以想象的东西。测试变成了目的本身。
这在后期维护阶段可能很有用。您可以快速检查以确保更新没有破坏任何内容。
但是,所涉及的开销可能会瘫痪产品开发!而未来更改可能涉及大量的测试更新开销,以更改功能。
(在多线程和任意执行顺序方面也可能变得混乱。)
通常情况下,除非另有指示,我的测试会尝试达到中间地带。
我会尝试进行更大粒度的测试,以验证基本的一般功能。我不会过于担心每种可能的边界情况。(这就是ASSERT宏的作用所在。)
例如:当我编写代码以通过UDP发送/接收消息时,我会快速地编写一个测试,使用该类通过环回接口发送/接收数据。没有什么花哨的东西。快速、简单、脏的代码。我只是想试试它。确保在构建其上之前它实际上是有效的。
另一个例子:从Firewire相机读取相机图像。我编写了一个快速而简单的GTK应用程序来读取图像、处理它们并实时显示它们。其他人称之为集成测试。但我可以用它来验证我的Firewire接口、我的Image类、我的Bayer RGGB->RGB变换、我的图像方向和对齐,甚至是否再次倒置相机。只有在这被证明不足时才需要进行更详细的测试。
另一方面,即使对于像这样简单的事情:
template<class TYPE> inline TYPE MIN(const TYPE & x, const TYPE & y) { return x > y ? y : x; }
template<class TYPE> inline TYPE MAX(const TYPE & x, const TYPE & y) { return x < y ? y : x; }
我写了一个一行的SHOW宏来确保我没有搞错符号:
SHOW(MIN(3,4)); SHOW(MAX(3,4));
就工具而言,有很多单元测试的东西。如果它对你有帮助,那就更好了。如果没有,那么你不需要太过花哨。
我经常编写一个测试程序,将数据转储到类中,然后使用 SHOW 宏将其全部打印出来:
#define SHOW(X) std::cout << # X " = " << (X) << std::endl
(或者,我的许多类可以使用内置的operator<<(ostream&)方法进行自我打印。这是一种非常有用的调试技术和测试技术!)
Makefile 可以轻松扩展,以自动从测试程序生成输出文件,并自动将这些输出文件与先前已知的(经过审核的)结果进行比较(diff)。
也许不太花哨,可能不太优雅,但作为技术而言,这非常有效,实现速度快,开销非常低。(当你的经理反对在测试上浪费时间时,这有其优势。)
最后我想留给你一个思考。 这会让我被扣分,所以不要这样做!
有一段时间我需要一个测试程序。这是必须交付的内容。该程序本身必须验证另一个类是否正常工作。但它不能访问外部数据文件。(我们不能依赖程序相对于其他任何东西的位置。也没有绝对路径。)项目的单元测试框架与我必须使用的编译器不兼容。它还必须在一个文件中。项目的makefile系统不支持将多个文件链接在一起以进行低级别的测试程序。(应用程序可以使用库。但每个测试程序只能使用一个文件。)
所以,上帝原谅我,我“打破了规则”...
<尴尬>
我使用了宏定义。当设置一个 #define 宏时,数据被写入第二个 .c 文件作为结构体数组的初始化器。随后,在软件重新编译和那个带有结构体数组的第二个 .c 文件 #included 时,若 #define 宏没有设置,则将新结果与先前存储的数据进行比较。是的,我 #included 了一个 .c 文件。真是太尴尬了。
</尴尬>
但这是可行的……