“Encourages simple designs and inspires confidence” — Kent Beck, 2003
Following on from the What is Test-Driven Development article, I thought I’d share more detail on how I structure Unit and Acceptance Tests (System / Integration tests follow the same basic structure as Unit Tests, they just take longer to run). Over the past 10+ years, I’ve tried many different naming conventions and test structures but, overall, I’ve found this style suits my approach best.
I’ll show a simple, but real, refactoring that a good suite of tests will allow me to make in the knowledge that nothing will break (or I will know if it does).
An Example of My Testing Structure
Below is an example of how I structure my unit test classes and folders along with a little information on why I do certain things.
- The Classes under test — as this is a demo and not real code, there is only one production class.
- The test classes — these use the same folder / naming as the production classes but are suffixed with Should.
- Creating the systemUnderTest during the Setup of the Test Fixture — for the Xunit framework, this occurs in the constructor rather than a dedicated Setup method. I always use the name systemUnderTest for the object being tested as it keeps the focus on the object of real interest (and is consistent). In the above, there is almost no set-up but, for more complex objects, this consistency ensures easy identification of the object of interest.
- The actual test. Note how the name extends the test class and becomes a pseudo-sentence (when appropriate spaces are added).
Using this structure (which I appreciate is not the most common, but I feel shows the intent of the test), our class name is:
ResourceControllerShould > ReturnTheExpectedNumberOfApis
or, with the correct spacing, it becomes:
Resource controller should return the expected number of APIs
Verbosity in test names is a GOOD thing — as with the production code, we’re expressing the intent (the what if you like) with our Test name.
A Closer Look at The Structure of a Unit Test
The above capture may have been a little hard to read so, below is a closer look at the same test — highlighting the main sections along with an answer to the why where appropriate.
- As the value will never change and is within the test, I make it explicit that this is the only acceptable expectation by marking it const. If the expected outcome cannot be marked as const, there may be something wrong with the test. At a unit-test level, I have rarely found a scenario that cannot be const.
- Magic values (whether string or integer etc.) are bad, bad, and did I mention bad? I appreciate that this is very subjective but… Here you could argue that simply using the value would still be clear, but I prefer to be explicit even for local values.
- Using FluentAssertions, the expected result resembles a sentence — C# idioms aside.
The Result? Whilst a simple (but real) test, it is clear exactly what the scenario is — from the test name — and what the expectation is from a reasonable representation of an English sentence.
With the structure in place, the full sentence would, as previously mentioned, become:
Resource Controller should contain the expected number of APIs.
This repeat is deliberate: whilst reasonably long at 9 words, I have known a test name to be two or three times longer. There may be a good reason for such extreme verbosity, but it may be a sign that the method is doing too much or, at least, the test is testing too much. If you find yourself writing a test name that fills the screen, please, for the sake of your future self, consider whether it is testing too much and should be broken into several, more focused tests; whether the name is simply too verbose or, as may be the case, it is just a verbose test name!
A Closer Look at The Structure of An Acceptance Test
Whilst the example below is the default Calculator example from the SpecFlow template, I have updated it to reflect the approach used on the projects I work on.
- The file name should be basically the same as the Feature — although, sometimes it may be abbreviated.
- The feature description in the User Story nnnn — Description format. The example is clearly for User Story 1234, which we can see is related to the Calculator Demo feature. If we need more information, we can see clearly, where to go. The use of the story number in the description also aids linking the tests in Azure DevOps — I wont cover how to link the tests in ADO as this will become an even longer post.
- When applicable, we add a tag to the feature scenario marking it special — in the example, the first test is special as it forms part of our SIT (System Integration Testing) suite.
- The Scenario the test is expected to prove. Each feature can have multiple scenarios and this line helps us focus on whether this is the scenario we are interested in. You can view the Scenario as akin to the Test Name from the above Unit Test example.
- Finally, the actual test in GWT-format (Given…When…Then…). Ideally, the core steps can be copied from the original User Story and will not require significant deviation / modification. This statement should be caveat-ed with suitable backlog refinement sessions.
Suitable tests give confidence for refactoring
We’ve spent time writing our test suite and production code, but is it the best production code we could have? Probably not! Don’t worry, this is where the benefit of TDD begins to pay back your efforts — we already know the code does what we want and we can now safely refactor to improve the design, separation, abstractions or whatever, without breaking anything.
I will use a simple (but real) example — the names have been obfuscated to protect the innocent…
What’s wrong with the below code?
Taken out of context, we do not know whether the someImportantResults object will only ever contain a few items or whether 10,000s or 1,000,000s of items is possible. What we do know is we only care about having more than 0 items. Whilst the above will work no matter how many items are in the collection, the below will be exponentially more performant as the number of rows increases:
With a suitable test for 0 items, 1 item and almost any other number imaginable / logical, I can safely change (refactor) the first piece of code to the second in the sure and certain knowledge that any mistake in the refactoring will cause at least one test to fail.
The above example is almost contrived to the point of being trite, but it IS a refactoring I have performed several times recently as part of our code review process.
Is the above the same code? Not quite, but whilst the difference may be hard to spot the impact is very significant — and one that the tests would catch. I don’t believe I’ve ever accidentally added an “!” but I have moved lines that appeared more logical in their new location and, thanks to the failing tests, found that I’d broken the intended flow in a way I hadn’t expected.
A meaningful suite of tests — at all levels — gives confidence to improve the code we have without affecting the results.
Whilst the refactoring above was very minor and can be viewed as a change supported by unit testing rather than Acceptance Testing, a suitable Acceptance test would still highlight the change in functionality if, for any reason, I did include the ! from the third example as the functionality would become the exact opposite of the expected functionality.
Wrap Up / Recommendations
I cannot recommend Test-Driven Development enough. A good suite of tests, covering all levels, aids confidence — whether to refactor a particular method or simply that it will work when released.
There are too many books for a comprehensive list of recommendations, but below are a couple that I strongly recommend:
- The Art of Unit Testing with Examples in .NET — Roy Osherove
- Improving the Design of Existing Code — Martin Fowler (with Kent Beck)
PluralSight Training Courses
PluralSight is updating so regularly that the below may be out of date before this article is published, but there should be something for everyone / every level of developer:
Join the Capgemini Microsoft team, open roles here.