我想在我的应用程序中开始进行更多的单元测试,但在我看来,我所做的大部分工作都不适合进行单元测试。我知道单元测试应该如何在教科书示例中起作用,但在现实世界的应用程序中它们似乎并没有多大用处。
我编写的一些应用程序具有非常简单的逻辑和与我无法控制的事物的复杂交互。例如,我想编写一个守护进程来响应某些应用程序发送的信号,并更改操作系统中的一些用户设置。我可以看到三个困难:
所有这些都可能是微妙的:我将不得不浏览可能复杂的API,我可能会引入错误,比如错误解释一些参数。单元测试能为我做什么?我可以模拟外部应用程序和操作系统,并检查给定来自应用程序的信号,我将在操作系统上调用相应的API方法。这是......嗯,应用程序的简单部分。
实际上我做的大多数事情都涉及与数据库,文件系统或其他应用程序的交互,而这些是最微妙的部分。
再看一下my build tool PHPmake的例子。我想重构它,因为它写得不是很好,但我担心这样做,因为我没有测试。所以我想补充一些。重点是,通过重构可能会破坏的东西可能无法被单元测试捕获:
glob
,它与文件系统一起使用。如果我只是模拟一棵树来代替实际的文件系统,glob
将不起作用。我可以继续更多的例子,但重点是以下几点。除非我有一些精巧的算法,否则我所做的大部分工作都涉及与外部资源的交互,这不适合单元测试。更重要的是,通常这种互动实际上是非平凡的部分。仍有许多人将单元测试视为基本工具。我错过了什么?我怎样才能学会成为更好的测试人员?
答案 0 :(得分:9)
我认为你在问题中提出了许多问题。
首先,当您的应用程序与外部环境(如操作系统,其他线程等)集成时,您必须将(1)与外部环境相关的逻辑与(2)您的业务代码分开。是,您的应用程序所做的事情。这与在应用程序(或Web应用程序)中分离GUI和SERVER的方式没有什么不同。
其次,你问你是否应该测试简单的逻辑。我会说,这取决于。通常,简单的提取/存储功能很适合进行测试。这就像你的应用程序的基础..因此它很重要。在你的基础上构建的其他商业资料非常简单,你可能很容易发现自己都觉得自己在浪费时间,而且大多数情况下你是: - )
第三,重构现有程序并在现有状态下对其进行测试可能是个问题。如果您的PHP程序根据某些输入生成一组文件,那么,也许这就是您测试的入口点。当然测试可能是高级的,但这是确保在重构之后,程序产生相同输出的简单方法。因此,在重构工作的开始阶段,针对那种情况下的更高级别的测试。
我想推荐一些文献,但我只想出一个标题。 “有效地使用传统代码”作者:Micheal Feathers。这是一个好的开始。另一个是Gerard Meszaros的“xUnit测试模式:重构测试代码”(虽然那本书更加草率和完整的复制粘贴文本)。
答案 1 :(得分:4)
关于您希望开始重构的测试目前尚未涵盖的现有代码库的问题,我建议您阅读:
Working Effectively with Legacy Code作者:Micheal Feathers。
该书为您提供了如何处理PHPMake可能遇到的问题的技巧。它提供了引入接缝进行测试的方法,以前没有任何接缝。
此外,使用触及文件系统的代码,您可以使用适配器模式在瘦包装器后面抽象文件系统调用。单元测试将反对包装类实现的抽象接口的伪实现。
在某些时候,你会达到足够低的水平,因为单元测试无法隔离代码单元,因为这些代码依赖于库或API调用(例如在包装器的生产实现中)。一旦发生这种情况,集成测试实际上是您可以编写的唯一自动化开发人员测试。
答案 2 :(得分:2)
“单元测试”测试一个代码单元。不应涉及任何外部工具。对于你的第一个应用程序来说这似乎很复杂(不知道太多关于它;))但是phpMake是单元可测试的 - 我确定...因为蚂蚁,gradle和maven也可以进行单元测试;)!
但当然您也可以自动测试您的第一个应用程序。有several different layers可以测试应用程序。
因此,您的任务是找到一种自动测试应用程序的方法 - 无论是集成测试还是其他任何方式。
E.g。你可以编写shell脚本,它会声明一些输出!这样你可以确保你的应用程序正常运行......
答案 3 :(得分:2)
我推荐这个google tech-talk on unit testing。
视频归结为
foo()
的课程,testFoo()
没有信息。他们实际上建议使用itShouldCloseConnectionEvenWhenExceptionThrown()
等测试名称。理想情况下,您的故事应该涵盖足够的功能,您可以从故事中重建规范。注意:视频和本文以Java为例;然而,主要观点代表任何语言。
答案 4 :(得分:1)
与外部资源交互的测试是集成测试,而不是单元测试。
如果发生特定的外部交互,您的代码测试可以看出它的行为方式可以是单元测试。这些应该通过编写代码来使用依赖注入来完成,然后在单元测试中将模拟对象注入依赖项。
例如,考虑一段代码,将一个服务的调用结果添加到另一个服务的调用结果中:
public int AddResults(IService1 svc1, IService2 svc2, int parameter)
{
return svc1.Call(parameter) + svc2.Call(parameter);
}
您可以通过传递两个服务的模拟对象来测试它:
private class Service1Returns1 : IService1
{
public int Call(int parameter){return 1;}
}
private class Service2Returns1 : IService2
{
public int Call(int parameter){return 1;}
}
public void Test1And1()
{
Assert.AreEqual(2, AddResults(new Service1Returns1(), new Service2Returns1(), 0));
}
答案 5 :(得分:1)
首先,如果单元测试似乎不适合您的应用程序,为什么还要开始做更多的事情?是什么促使你关心它?如果a)你第一次完成所有事情并且没有任何改变或b)你认为这是浪费时间并且做得不好,那绝对是浪费时间。
如果您确实认为您确实想要进行单元测试,那么您的问题的答案都是一样的:封装。在您的守护程序示例中,您可以使用非常窄的接口创建ApplcationEventObeservationProxy,该接口仅实现传递方法。这个类的目的是做 nothing ,但完全封装了第三方事件观察库中的其余代码(没有任何意义 - 这里没有逻辑)。对OS设置执行相同的操作。然后,您可以完全单元测试基于事件执行操作的类。我建议有一个单独的守护进程类,它只包装你的主类 - 它将使测试更容易。
在单元测试之外,这种方法有几个好处。一个是如果您封装了与操作系统直接交互的代码,则更容易将其切换出来。这种代码特别容易在您的控件之外破坏(即MS补丁集)。您可能还希望支持多个操作系统,如果操作系统特定逻辑与其余逻辑不相混淆,则会更容易。另一个好处是,您将被迫意识到您的应用程序中存在比您想象的更多的业务逻辑。 :)
最后,不要忘记单元测试是优质产品的基础,但不是唯一的成分。有一组测试可以探索和验证您将使用的OS API调用,这是解决此问题“硬”部分的一个很好的策略。您还应该进行端到端测试,以确保应用程序中的事件导致操作系统设置更改。
答案 6 :(得分:0)
正如其他答案建议有效地使用遗产代码作者:Micheal Feathers是一本很好的读物。如果您必须处理遗留代码,并且希望确保系统交互按预期工作,请尝试首先编写集成测试。然后,编写单元测试以测试从需求角度评估的方法的行为更为合适。 You Tests服务的目的与集成测试完全不同。单元测试更有可能改善系统的设计,而不是测试所有东西是如何挂起来收集的。