Java module testing - proper isolation?

I would like to know if there was a wrong approach to unit testing:

My application has a boot process that initializes several components and provides services to various subsystems - let me call it a “controller”.

In many cases, for the unit test of these subsystems, I need access to the controller, since these subsystems can depend on it. My approach to this unit test is to initialize the system, and then provide the controller with any unit test that requires it. I achieve this through inheritance: I have a unit test base that initializes and tests the controller, then any unit test that requires a controller extends this base class and, therefore, has access to it.

My question is:

(1) Does this achieve proper isolation? It makes sense to me that unit tests should be performed in isolation so that they are repeatable and independent - is it normal that I provide a real initialized controller, and not mock it or try to mock the specific environment needed for each test?

(2) As best practice (assuming my previous approach is fine) - should I create a controller again and again for each unit test, or would it be enough to create it once (its state does not change).

+4
source share
3 answers

If we supply a “real” controller for testing another component, then, strictly speaking, we run an integration test, not a unit test. This is not necessarily bad, but consider the following points:

The cost of creating a controller

If the controller is a heavy object with significant construction costs, then each unit test will bear this cost. As the number of unit tests grows in quantity, this cost may begin to dominate the total test run time. It is always desirable that the battery life is minimal to ensure a quick turn after changing the code.

Controller dependencies

If the controller is a complex object, it may have its own dependencies that must be created to create the controller itself. For example, you might need access to a database file or a configuration file. Now, not only do you need to initialize the controller, but also those components. As the application develops over time, the controller may require more and more dependencies, just making this problem even worse over time.

Controller status

If the controller carries any state, performing a unit test can change that state. This, in turn, can change the behavior of subsequent unit tests. Such changes can lead to the apparently non-deterministic behavior of single tests, introducing the ability to mask errors. The fix for this problem is to re-create the controller for each test, which may be impractical if the creation is expensive (as mentioned above).

Combinatorial task

The number of combinations of possible inputs into the composite system of the device under test and the controller object can be much larger than the number of combinations for one device. This number may be too large for testing. Testing a block in isolation with a plug or a mock object instead of a controller makes it easier to control the number of combinations.

Object of god

If the controller is conveniently accessible for all components in each unit test, it will be a great temptation to turn the controller into a God Object that knows everything about every component of the system. Worse, these components can begin to interact with each other through this deity object. The end result is that the separation between application components begins to break down, and the system begins to become monolithic.

Technical debt

Even if the controller is inactive today and cheap to create an instance, this can change as the application develops. If this day comes after we have written a large number of unit tests, we may encounter a lot of refactoring of all these tests. Moreover, actual system code may also need refactoring to replace all references to controllers with lighter interfaces. There is a risk that the cost of refactoring is significant - perhaps even too high to behold, as a result of which the system "gets stuck" in an undesirable form.

Recommendation

To avoid these errors now and in the future, my recommendation is to not deliver the real controller to unit tests.

A full controller will probably be difficult to drown or mock you. This will cause (desired) pressure to express component dependencies as a “thin”, focused interface instead of the “thick”, “kitchen sink” that may be present in the controller. Why is this desirable? This is desirable because this practice promotes a better separation of problems between system components, which provides architectural benefits far beyond the unit test database.

For a lot of good practical tips on how to achieve separation of problems and generally write test code, see the Misko Hevery guide and.

+7
source
  • I think it is normal to provide a real controller. This will provide a good integration test for your system. At my company, we do a lot of what you do: a base test class that sets up the environment and actual test cases that inherit it.

  • Hmm ... I think I can create it once. This will also check your controller and make sure that its state does not change and can bear repeated calls.

+1
source

If you are looking for a rigorous unit test, why not use mock objects like EasyMock:

http://www.easymock.org/

In this way, you can provide a “layout” of behavior for the controller without creating it. Unitils also provides integration with EasyMock, so if you extend the UnitilsJUnit4 unit test class, you get the automatic creation and commissioning of a mock object. Unitils also provides DB / integration module testing, which may be too large for your software.

+1
source

Source: https://habr.com/ru/post/1369599/


All Articles