我相信大多数人都在编写大量的自动化测试,而且在进行单元测试时你也遇到了一些常见的陷阱.
我的问题是你是否遵循任何编写测试的行为规则以避免将来出现问题?更具体一点:良好单元测试的属性是什么,或者您如何编写测试?
鼓励语言不可知的建议.
让我首先插入源代码 - 使用JUnit在Java中进行语用单元测试(还有一个带有C#-Nunit的版本......但是我有这个...它的大部分都是不可知的.推荐.)
好的测试应该是一个TRIP(首字母缩略词不够粘 - 我在书中打印出了cheatsheet,我不得不拔出以确保我做对了..)
自动:应自动调用测试以及检查通过/失败的结果
彻底:覆盖范围; 虽然错误往往聚集在代码中的某些区域,但请确保测试所有关键路径和方案.如果必须知道未经测试的区域,请使用工具
可重复:每次测试每次都会产生相同的结果.测试不应该依赖于无法控制的参数.
独立:非常重要.
测试应该一次只测试一件事.只要它们都测试一个功能/行为,多个断言就可以了.当测试失败时,它应该确定问题的位置.
测试不应该相互依赖 - 隔离.没有关于测试执行顺序的假设.通过适当使用设置/拆卸,在每次测试前确保"干净的平板"
专业:从长远来看,您将拥有与生产一样多的测试代码(如果不是更多),因此您的测试代码遵循相同的良好设计标准.精心设计的方法 - 具有意图揭示名称的类,无重复,具有良好名称的测试等.
好的测试也快速运行.任何需要超过半秒才能运行的测试......需要加以研究.测试套件运行所需的时间越长,运行的频率就越低.开发人员试图在两次运行之间潜行的变化越多......如果有什么破坏......需要更长的时间来确定哪个变化是罪魁祸首.
2010-08更新:
可读:这可以被认为是专业人士的一部分 - 但是它不能被强调.酸测试是找到一个不属于你的团队的人,并要求他/她在几分钟内弄清楚被测试的行为.测试需要像生产代码一样进行维护 - 因此即使需要付出更多努力也可以轻松阅读.测试应该是对称的(遵循模式)和简洁(一次测试一个行为).使用一致的命名约定(例如TestDox样式).避免使用"附带细节"使测试混乱.成为极简主义者.
除此之外,其他大多数都是减少低效益工作的准则:例如"不要测试你不拥有的代码"(例如第三方DLL).不要去测试getter和setter.密切关注成本效益比或缺陷概率.
不要写大量的测试.正如"单元测试"中的"单位"所暗示的那样,将每个单元视为原子并尽可能隔离.如果必须,请使用模拟对象创建前置条件,而不是手动重新创建过多的典型用户环境.
不要测试明显有用的东西.避免测试来自第三方供应商的类,尤其是提供您编码的框架的核心API的类.例如,不要测试将项添加到供应商的Hashtable类.
考虑使用代码覆盖工具(如NCover)来帮助发现尚未测试的边缘情况.
尝试在实现之前编写测试.将测试视为您的实现将遵循的更多规范.参看 行为驱动的开发,一个更具体的测试驱动开发分支.
始终如一.如果您只为某些代码编写测试,那么它几乎没用.如果你在一个团队中工作,而其他一些或所有人都没有编写测试,那么它也不是很有用.说服自己和其他人了解测试的重要性(以及节省时间的特性),或者不要打扰.
这里的大多数答案似乎都是针对单元测试的最佳实践(何时,何地,为什么以及什么),而不是实际编写测试本身(如何).由于问题在"如何"部分看起来非常具体,我想我会发布这个,取自我在公司进行的"棕色包"演示.
1.使用长的描述性测试方法名称.
- Map_DefaultConstructorShouldCreateEmptyGisMap() - ShouldAlwaysDelegateXMLCorrectlyToTheCustomHandlers() - Dog_Object_Should_Eat_Homework_Object_When_Hungry()
2.以Arrange/Act/Assert风格编写测试.
虽然这种组织策略已经存在了很长一段时间并且涉及很多事情,但最近引入"AAA"缩写是一种很好的方法来实现这一目标.使所有测试符合AAA风格,使其易于阅读和维护.
3.始终使用断言提供失败消息.
Assert.That(x == 2 && y == 2, "An incorrect number of begin/end element processing events was raised by the XElementSerializer");
一个简单而有益的练习,使你在跑步者应用程序中显而易见的失败.如果你没有提供消息,你通常会在失败输出中得到类似"预期真实,虚假"的内容,这使得你必须真正去阅读测试以找出错误.
4.评论测试的原因 - 业务假设是什么?
/// A layer cannot be constructed with a null gisLayer, as every function /// in the Layer class assumes that a valid gisLayer is present. [Test] public void ShouldNotAllowConstructionWithANullGisLayer() { }
这似乎是显而易见的,但是这种做法将保护测试的完整性,使其不受那些不了解测试背后原因的人的影响.我已经看到许多测试被删除或修改完全没问题,仅仅是因为该人不理解测试正在验证的假设.
如果测试是微不足道的,或者方法名称具有足够的描述性,则可以允许将注释保留为关闭状态.
5.每个测试必须始终恢复其接触的任何资源的状态
尽可能使用模拟以避免处理实际资源.
清理必须在测试级别完成.测试不得依赖执行顺序.
记住这些目标(改编自Meszaros的xUnit Test Patterns一书)
测试应该降低风险,而不是引入风险.
测试应该很容易运行.
随着系统的发展,测试应该易于维护
有些事情可以让这更容易:
只有一个原因,测试才会失败.
测试应该只测试一件事
最小化测试依赖性(不依赖于数据库,文件,ui等)
不要忘记您也可以使用xUnit框架进行集成测试,但要将集成测试和单元测试分开
伟大的单元测试的一些属性:
当测试失败时,应该立即明确问题所在.如果必须使用调试器来追踪问题,那么您的测试就不够精确.每次测试只有一个断言有助于此.
重构时,任何测试都不会失败.
测试应该运行得如此之快,以至于您可以毫不犹豫地运行它们.
所有测试都应该通过; 没有非确定性的结果.
单元测试应该是一个很好的因素,就像你的生产代码一样.
@Alotor:如果您建议库只应在其外部API上进行单元测试,我不同意.我想要为每个类进行单元测试,包括我不向外部调用者公开的类.(但是,如果我觉得需要为私有方法编写测试,那么我需要重构.)
编辑:有关于"每次测试一个断言"引起的重复的评论.具体来说,如果您有一些代码来设置场景,然后想要对其进行多次断言,但每次测试只有一个断言,则可能会在多个测试中复制设置.
我不接受这种方法.相反,我在每个场景中使用测试装置.这是一个粗略的例子:
[TestFixture] public class StackTests { [TestFixture] public class EmptyTests { Stack_stack; [TestSetup] public void TestSetup() { _stack = new Stack (); } [TestMethod] [ExpectedException (typeof(Exception))] public void PopFails() { _stack.Pop(); } [TestMethod] public void IsEmpty() { Assert(_stack.IsEmpty()); } } [TestFixture] public class PushedOneTests { Stack _stack; [TestSetup] public void TestSetup() { _stack = new Stack (); _stack.Push(7); } // Tests for one item on the stack... } }
测试应该是孤立的.一项测试不应该依赖于另一项测试.更进一步,测试不应该依赖外部系统.换句话说,测试你的代码,而不是代码的代码取决于on.You可以测试这些交互为您的集成测试或功能测试的一部分.
你所追求的是描述被测试阶级的行为.
验证预期的行为.
验证错误情况.
覆盖类中所有代码路径.
在班级中练习所有成员函数.
基本意图是增加您对班级行为的信心.
在查看重构代码时,这尤其有用.Martin Fowler 在他的网站上有一篇关于测试的有趣文章.
HTH.
干杯,
抢
测试最初应该失败.然后你应该编写使它们通过的代码,否则你冒着编写一个错误并且总是通过的测试的风险.
我喜欢上述实用单元测试书中的正确BICEP首字母缩略词:
右:结果对吗?
乙:是不是所有的b oundary条件是否正确?
我:我们可以查看我的反向关系吗?
Ç:我们能否Ç罗斯检查使用其他手段的结果吗?
Ë:我们可以强制Ë RROR情况发生呢?
P:是p范围内erformance特点?
我个人觉得你可以通过检查你得到正确的结果(1 + 1应该在一个加法函数中返回2)得到相当远,尝试你能想到的所有边界条件(例如使用两个数字的总和大于add函数中的整数最大值)并强制出现网络故障等错误情况.
良好的测试需要维护.
我还没有弄清楚如何在复杂环境中这样做.
随着您的代码库开始涉及数百个或数百万行代码,所有教科书都开始脱口而出.
团队互动爆炸
测试用例爆炸的数量
组件之间的交互爆炸.
构建所有单元测试的时间成为构建时间的重要部分
API更改可能会影响到数百个测试用例.即使生产代码变更很容易.
将进程排序到正确状态所需的事件数量增加,这反过来又增加了测试执行时间.
良好的架构可以控制一些交互爆炸,但随着系统变得越来越复杂,自动化测试系统随之增长.
这是您开始处理权衡的地方:
只测试外部API,否则重构内部会导致重大的测试用例返工.
每个测试的设置和拆除变得更加复杂,因为封装的子系统保留更多的状态.
每晚编译和自动化测试执行增长到几个小时.
增加编译和执行时间意味着设计人员不会或不会运行所有测试
为了减少测试执行时间,您需要考虑对测试进行测序以减少设置和拆卸
你还需要决定:
你在哪里存储代码库中的测试用例?
你如何记录你的测试用例?
可以重新使用测试夹具来保存测试用例维护吗?
当夜间测试用例执行失败时会发生什么?分流谁?
你如何维护模拟对象?如果您有20个模块都使用自己的模拟日志记录API,那么快速更改API会发出涟漪声.测试用例不仅会改变,而且20个模拟对象也会发生变化.这20个模块是由许多不同的团队在几年内编写的.这是一个经典的重用问题.
个人和他们的团队了解自动化测试的价值,他们只是不喜欢其他团队的工作方式.:-)
我可以永远继续下去,但我的观点是:
测试需要可维护.
我在这篇MSDN杂志文章中回顾了这些原则,我认为对任何开发人员都很重要.
我定义"好"单元测试的方式是,它们是否具有以下三个属性:
它们是可读的(命名,断言,变量,长度,复杂性......)
它们是可维护的(没有逻辑,没有指定,基于状态,重构......)
它们是值得信赖的(测试正确的东西,隔离,而不是集成测试..)