Published July 16, 2023
by Doug Klugh
Keep Tests Well Organized
Organize your xUnit test packages, classes, and methods in a way that makes it easy to find, understand, and maintain your tests. While this is relatively easy with a small number of tests, effectively managing a lot of them takes some planning. Although there is no single best practice, there are organizational strategies that will go a long way to help keep your tests and test fixtures well organized. These strategies will also help to promote test code reuse, simplify running test subsets, manage the lifetime of test fixtures, and optimize test execution by managing resources that are expensive to allocate.
How you organize your test methods will have a big impact on the efficiency of your testing.
The simplest approach to test organization is to simply create a test class for each production class. This test class will contain all the test methods used to verify the behavior of a particular SUT class. While this makes it easy to locate all the tests associated with that class, it may require creating a significant number of test fixtures.
Another approach is to put all test methods that verify a particular feature or capability of the SUT into a single test class. Not only does this make it easy to locate all the tests associated with a feature, but it is also easy to run that subset of tests that verify (and possibly validate) specific functionality. The downside is that it may result in duplicate test fixtures (across different features).
One more approach is to group all test methods that require the same test fixture(s) into the same test class. This will minimize the number of test fixtures but your tests for each feature will likely be scattered across numerous test classes — which may inhibit your ability to locate specific tests or to run a certain subset of tests.
A Test Fixture is code that defines the context (pre-conditions) that is needed to exercise (and test) the SUT. This, of course, would include an instance of the class whose method we are testing. It may also include other objects upon which the SUT depends. While we certainly would not create SUT dependencies for unit tests, they may likely be needed for other types of tests, such as component, acceptance, and integration tests.
Following are a few strategies for optimizing test fixtures:
A Transient Fresh Fixture is one that is created and destroyed within every test. This type of fixture is particularly easy to setup in JUnit using either a constructor or setup method (using the @Before attribute). JUnit will invoke either one (or both) before each test method. This type of fixture is transient because the lifetime of the fixture persists only for the duration of the test method. And it is fresh because the fixture is initialized prior to each method.
Transient Fresh Fixtures ensure that your tests are independent, can execute concurrently, and can execute in any order. They also ensure that you never need a tear-down method because nothing persists beyond the fixture — it's all destroyed at the end of the test. If you need a tear-down method, that's an indicator that there is something about the fixture that is not transient.
A Persistent Fresh Fixture is one that otherwise persists from test to test and needs to be torn down within each test method to make it fresh. Examples include fixtures that create resources such as files, sockets, semaphores, database connections, etc. The goal of the tear-down method is to delete or reset any persistent parts of the fixture — making them fresh again for the next test.
Users of NUnit or x-Unit should note that these frameworks do not create a fresh instance between test methods — which means that all tests written in those frameworks are persistent. This will require tear-down methods to freshen them up.
A Persistent Shared Fixture is one that allows some state (or resource) to persist from test to test. Since some resources can be very expensive to create and destroy within every test method (such as database connections), it is usually best to share that resource across a suite of tests. In JUnit, this can be accomplished using the @BeforeClass attribute to designate suite setups and the @AfterClass attribute to designate suite teardowns.
Related Posts
assistant Development Tip
Fresh Fixture
Define the state of your test environment by designing and building a test fixture that supports a single run of a single test. Build a Transient Fixture when its lifetime should persist no longer than the duration of the test method and is initialized prior to each method. Building a Persistent Fixture that survives the life of the test must be torn down within the test method to avoid side effects and... Read More
assistant Development Tip
Shared Fixture
Define the state of your test environment by designing and building a Persistent Test Fixture to share system resources across a suite of tests. This will help optimize test execution by managing resources that are expensive to allocate (such as database connections) that can be setup once and shared across multiple tests. Be sure not to persist resources with side effects as this will create... Read More
assistant Development Tip
Repeatable Tests
Write tests such that each produces the same result from a given initial state without any manual intervention between runs. Unit tests must verify Single Test Conditions by executing a single code path through the System Under Test (SUT) and executing the same, exact code path each time it runs. Verifying one condition for each test helps to minimize Test Overlap and ensures we have... Read More
assistant Development Tip
Robust Tests
Build tests in such a way that the number of tests impacted by any change is small. This is achieved by minimizing overlap and dependencies between tests and ensuring changes to the test environment have no affect on our tests. Isolate the System Under Test (SUT) and verify just one condition for each test. Changes that affect test fixtures should be encapsulated behind test utility methods. Read More
assistant Development Tip
Slow Tests
Build your software and your tests in ways that minimize test execution time. Common causes for long running tests include over-engineered test fixtures, asynchronous code, components with high latency, Test Overlap, and too many tests due to a tightly coupled architecture. This causes bottlenecks in Continuous Integration, inhibits rapid feedback facilitated by... Read More
assistant Development Tip
Obscure Tests
Avoid this test smell by writing tests from Simple Designs and refactor as needed to keep your test code clean. Good tests are understood at a glance and clarify the behavior they verify. Structure Matters as much for tests as it does production code. Apply design principles and patterns to enhance the structural quality of tests — as they are every bit as important as production code and should be... Read More