简体   繁体   English

Java单元测试 - 适当的隔离?

[英]Java unit testing - proper isolation?

I'd like to know if my approach to unit testing has been wrong: 我想知道我的单元测试方法是否错误:

My application has a boot strap process that initializes a several components and provides services to various sub-systems - let's call it the "controller". 我的应用程序有一个引导程序进程,初始化几个组件并为各种子系统提供服务 - 让我们称之为“控制器”。

In many cases, in order to unit test these sub-systems, I would need access to the controller as these sub-systems may depend on it. 在许多情况下,为了对这些子系统进行单元测试,我需要访问控制器,因为这些子系统可能依赖于它。 My approach to doing this unit test would be to initialize the system, and then provide the controller to any unit test that requires it. 我进行单元测试的方法是初始化系统,然后将控制器提供给需要它的任何单元测试。 I achieve this via inheritance: I have a base unit test that initializes and tests the controller, then any unit test requiring the controller would extend this base class, and hence, have access to it. 我通过继承来实现这一点:我有一个初始化和测试控制器的基本单元测试,然后任何需要控制器的单元测试都会扩展这个基类,因此可以访问它。

My question is this: 我的问题是:

(1) Is this achieving proper isolation? (1)这是否实现了适当的隔离? It makes sense to me that unit tests should be done in isolation so that they are repeatable and independent - is it ok that I am providing a real initialized controller rather than mocking it or attempting to mock the specific environment required by each test? 对我来说,单元测试应该是孤立完成的,这样它们是可重复和独立的 - 我可以提供一个真正的初始化控制器而不是模拟它或试图模拟每个测试所需的特定环境吗?

(2) As a best practice (assuming that my previous approach is OK) - should I be creating the controller over and over for each unit test, or would it suffice to create it once (its state is not changing). (2)作为一种最佳实践(假设我以前的方法没问题) - 我应该为每个单元测试反复创建控制器,还是只需创建一次(其状态不会改变)。

If we are supplying a "real" controller to test another component, then strictly speaking we are performing an integration test rather than a unit test. 如果我们提供一个“真正的”控制器来测试另一个组件,那么严格来说我们正在执行集成测试而不是单元测试。 This is not necessarily a bad thing, but consider the following points: 这不一定是坏事,但请考虑以下几点:

Cost of Creating the Controller 创建控制器的成本

If the controller is a heavyweight object with a considerable cost to construct, then every unit test will incur this cost. 如果控制器是重量级的物体,构造成本相当高,那么每个单元测试都会产生这个成本。 As the number of unit tests grows in number, that cost may begin to dominate the overall test execution time. 随着单元测试数量的增加,该成本可能开始占据整个测试执行时间。 It is always desirable to keep the runtime of unit tests as small as possible to allow quick turnaround after code changes. 始终希望保持单元测试的运行时尽可能小,以便在代码更改后快速周转。

Controller Dependencies 控制器依赖性

If the controller is a complex object, it may have dependencies of its own that need to be instantiated in order to construct the controller itself. 如果控制器是一个复杂的对象,它可能具有自己的依赖关系,需要实例化以构建控制器本身。 For example, it may need to access a database or configuration file of some kind. 例如,它可能需要访问某种数据库或配置文件。 Now, not only does the controller need to be initialized, but also those components. 现在,不仅需要初始化控制器,还需要初始化控制器。 As the application evolves over time, the controller may require more and more dependencies, just making this problem worse as time goes on. 随着应用程序随着时间的推移而发展,控制器可能需要越来越多的依赖关系,只是随着时间的推移使这个问题变得更糟。

Controller State 控制器状态

If the controller carries any state, the execution of a unit test may change that state. 如果控制器携带任何状态,则单元测试的执行可以改变该状态。 This, in turn, may change the behaviour of subsequent unit tests. 反过来,这可能会改变后续单元测试的行为。 Such changes may result in apparently non-deterministic behaviour of the unit tests, introducing the possibility of masking bugs. 这些变化可能导致单元测试的明显不确定性行为,引入掩盖错误的可能性。 The cure for this problem is to create the controller anew for each test, which may be impractical if that creation is expensive (as noted above). 解决这个问题的方法是为每次测试重新创建控制器,如果创建成本很高(如上所述),这可能是不切实际的。

Combinatorial Problem 组合问题

The number of combinations of possible inputs to the composite system of the unit under test and the controller object may be much larger than the number of combinations for the unit alone. 被测单元的复合系统和控制器对象的可能输入的组合的数量可以远大于单元的组合的数量。 That number might be too large to test practically. 这个数字可能太大而无法进行实际测试。 By testing the unit in isolation with a stub or mock object in place of the controller, it is easier to keep the number of combinations under control. 通过使用存根或模拟对象代替控制器单独测试单元,可以更容易地控制组合的数量。

God Object 上帝对象

If the controller is conveniently accessible to all components in every unit test, there will be a great temptation to turn the controller into a God Object that knows everything about every component in the system. 如果每个单元测试中的所有组件都可以方便地访问控制器,那么很有可能将控制器转换为神对象 ,该对象知道系统中每个组件的所有内容。 Even worse, those components may begin to interact with one another through that god object. 更糟糕的是,这些组件可能开始通过该上帝对象彼此交互。 The end result is that the separation between application components begins to erode and system starts to become monolithic. 最终结果是应用程序组件之间的分离开始腐蚀,系统开始变得单一。

Technical Debt 技术债务

Even if the controller is stateless and cheap to instantiate today, that may change as the application evolves. 即使控制器是无状态且今天实例化的便宜,也可能随着应用程序的发展而改变。 If that day arrives after we have written a large number of unit tests, we might be faced with a large refactoring exercise of all of those tests. 如果在我们编写了大量单元测试后的那一天到来,我们可能会面临所有这些测试的大量重构练习。 Furthermore, the actual system code might also need refactoring to replace all of the controller references with lighter weight interfaces. 此外,实际的系统代码可能还需要重构以用较轻的接口替换所有控制器引用。 There is a risk that the refactoring cost is significant -- possibly even too high to contemplate, resulting in a system is "stuck" in an undesirable form. 存在重构成本显着的风险 - 可能甚至太高而无法考虑,导致系统被“卡住”在不期望的形式中。

Recommendation 建议

In order to avoid these pitfalls now and in the future, my recommendation is to avoid supplying the real controller to the unit tests. 为了避免现在和将来出现这些陷阱,我的建议是避免将真实控制器提供给单元测试。

The full controller is likely to be difficult to stub or mock effectively. 完整的控制器可能难以有效地存根或模拟。 This will induce (desirable) pressure to express a component's dependencies as a "thin", focused interface in place of the "thick", "kitchen sink" interface that the controller is likely to present. 这将导致(期望的)压力以将组件的依赖性表达为“薄的”聚焦界面,代替控制器可能呈现的“厚”,“厨房水槽”界面。 Why is this desirable? 为什么这是可取的? It is desirable because this practice promotes better separation of concerns between system components, yielding architectural benefits far beyond the unit test code base. 这是可取的,因为这种做法促进了系统组件之间关注点的更好分离,从而产生远远超出单元测试代码库的架构优势。

For lots of good practical advice about how to achieve separation of concerns and generally write testable code, see Misko Hevery's guide and talks . 有关如何实现关注点分离以及通常编写可测试代码的大量实用建议,请参阅Misko Hevery的指南会谈

  1. I think it's OK to provide a real controller. 我认为提供一个真正的控制器是可以的。 That will provide for a good integration test of your system. 这将为您的系统提供良好的集成测试。 At my company we do a lot of what you're doing: a base test class that sets up the environment and the actual test cases that inherit it. 在我的公司,我们做了很多你正在做的事情:一个用于设置环境的基础测试类和继承它的实际测试用例。

  2. Hrm ... I think I might create it once. 嗯......我想我可能会创造一次。 That'll test your controller also and make sure its state isn't changing and can bear repeated invocations. 那也将测试你的控制器,并确保它的状态不会改变,并且可以承受重复的调用。

If you're looking for a strict unit test, why not use mock objects, like EasyMock: 如果您正在寻找严格的单元测试,为什么不使用像EasyMock这样的模拟对象:

http://www.easymock.org/ http://www.easymock.org/

That way you can provide "mock" behavior for the controller without ever instantiating it. 这样,您可以为控制器提供“模拟”行为,而无需实例化它。 Unitils also provides integration with EasyMock, such that if you extend the UnitilsJUnit4 unit test class, you get automatic mock object creation and injection. Unitils还提供与EasyMock的集成,这样,如果扩展UnitilsJUnit4单元测试类,您将获得自动模拟对象创建和注入。 Unitils also provides for DB unit/integration testing, which might be overkill for your software, though. Unitils还提供数据库单元/集成测试,但这可能对您的软件来说太过分了。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM