我相信大多数人都在编写大量自动化测试,并且在单元测试时也遇到了一些常见的陷阱。
我的问题是,为了避免未来出现问题,你是否遵循编写测试的任何行为规范? 更具体地说:好的单元测试具有哪些属性或者你如何编写你的测试用例?
鼓励提供与编程语言无关的建议。
我相信大多数人都在编写大量自动化测试,并且在单元测试时也遇到了一些常见的陷阱。
我的问题是,为了避免未来出现问题,你是否遵循编写测试的任何行为规范? 更具体地说:好的单元测试具有哪些属性或者你如何编写你的测试用例?
鼓励提供与编程语言无关的建议。
让我首先推荐一些资源 - 《Java单元测试实践》(也有用C#-NUnit的版本..但我只有这个版本..在大部分情况下,它是中立的。推荐阅读)
好的测试应该是一个 TRIP(缩写不够粘性 - 我有一份书中的备忘单打印出来,以确保我没记错..)
专业化:从长远来看,你的测试代码将和生产代码一样多(如果不是更多),因此遵循同样的良好设计标准来编写测试代码。方法类应该被反映出其意图的名称分解得很好,没有重复,测试用例的命名良好等。
好的测试也要运行快。任何运行时间超过半秒的测试都需要加以改进。测试套件运行所需时间越长,就会越不经常运行。开发人员在测试之间尝试悄悄进行更改,如果出现问题,将需要更长时间才能找出是哪个更改造成了问题。
更新于2010年08月:
除此之外,大多数其他指南都可以削减低效的工作:例如,“不要测试您不拥有的代码”(例如第三方 DLL)。不要测试 getter 和 setter。关注成本效益比或缺陷概率。
这里大多数答案似乎是涉及单元测试最佳实践的一般性问题(何时、何地、为什么和什么),而不是实际撰写测试本身的方法(如何)。由于问题似乎非常专注于“如何”部分,因此我想发布这篇文章,它来自于我在公司进行的“brown bag”演示。
1. 使用长而具有描述性的测试方法名称。
- Map_DefaultConstructorShouldCreateEmptyGisMap()
- ShouldAlwaysDelegateXMLCorrectlyToTheCustomHandlers()
- Dog_Object_Should_Eat_Homework_Object_When_Hungry()
2. 以安排/操作/断言的方式编写你的测试。
3. 在你的Asserts中始终提供失败消息。
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)
为了使这更容易实现:
不要忘记你也可以使用xUnit框架进行集成测试但要保持集成测试和单元测试分离
当测试失败时,应立即明确问题所在。如果必须使用调试器来跟踪问题,则您的测试不够细粒度。每个测试只有一个断言可以帮助解决这个问题。
重构时,不应该存在测试失败的情况。
测试运行速度应非常快,以至于您从未犹豫过是否运行它们。
所有测试始终都应该通过,没有非确定性结果。
单元测试应该像你的生产代码一样被很好地重构。
@Alotor:如果您建议库只在其外部API上进行单元测试,我不同意。我希望为每个类编写单元测试,包括我不向外部调用者公开的类。(然而,如果我感觉需要为私有方法编写测试,则需要重构。)
编辑:关于“每个测试只有一个断言”可能引起重复的评论。具体来说,如果您有一些设置场景的代码,然后想对其进行多个断言,但每个测试只能有一个断言,那么您可能会在多个测试中重复设置。
我不采取这种方法。相反,我使用每个场景的测试装置。这是一个粗略的例子:
[TestFixture]
public class StackTests
{
[TestFixture]
public class EmptyTests
{
Stack<int> _stack;
[TestSetup]
public void TestSetup()
{
_stack = new Stack<int>();
}
[TestMethod]
[ExpectedException (typeof(Exception))]
public void PopFails()
{
_stack.Pop();
}
[TestMethod]
public void IsEmpty()
{
Assert(_stack.IsEmpty());
}
}
[TestFixture]
public class PushedOneTests
{
Stack<int> _stack;
[TestSetup]
public void TestSetup()
{
_stack = new Stack<int>();
_stack.Push(7);
}
// Tests for one item on the stack...
}
}
测试应该是隔离的。一个测试不应依赖于另一个测试。更进一步,测试不应该依赖于外部系统。换句话说,测试你的代码,而不是你的代码所依赖的代码。你可以将这些交互作为集成或功能测试的一部分进行测试。
测试应当一开始失败。接着,你应该编写代码使它们通过,否则你就有可能编写一个存在缺陷且总是通过的测试。
好的测试需要易于维护。
对于复杂的环境,我还没有完全弄清楚如何做到这一点。
所有的教科书在你的代码库开始达到数十万或数百万行代码时都开始出现问题。
良好的架构可以控制某些交互爆炸,但随着系统变得越来越复杂,自动化测试系统也随之增长。
这是你开始处理权衡的地方:
你还需要决定:
在代码库中存储测试用例的位置?
我可以永远地继续下去,但我的观点是:
测试需要易于维护。
我喜欢前面提到的《使用 NUnit 进行实用单元测试》一书中的“Right BICEP”缩写:
个人认为,通过检查是否获得正确的结果(例如,在加法函数中,1+1应该返回2),尝试使用您可以想到的所有边界条件(例如使用两个其总和大于整数最大值的数字进行加法)并强制发生网络故障等错误条件,您可以取得相当大的进展。