Begin with the operation
First step would be to identify what the test assertion needs. Is it reading a file? Compare contents? Counting items? This represents the operation itself, the core action taken. If this operation would be part of the implementation, and not the test assertion, we would maybe introduce an interface and error types as to not expose internal failures to the user. This differs a lot if the operation would run during the test assertion; as in that case, there is need for a lot of internal context – otherwise, the test assertion is not very useful.
🚩 Only if the source code is at the same authorization level as the test output, should the internal context be exposed.
Fire your life coach
One of the defensive code techniques is called ‘fire your life coach’. In short, it means that you should always prepare for a failure. In writing operations within test assertions, this is even more the case. If we take the example of reading a file contents during the test assertion, there are a lot of things that could go wrong: the file could be missing, the contents too big, the wrong extension, the wrong format, the wrong schema… Each of those verifications should be be checked, but moreover, each of those verifications should have a test assertion failure that includes enough internal context so that the failure makes sense. If the file is missing, you should include the file name and the directory where the system has searched; if the file is too big, you should show the maximum allowed contents and the actual contents length; if the file has the wrong extension, you should show the expected extension and the actual one.
It is not enough to have a yes/no test assertion verification, as you always need to know what went wrong and why it went wrong.
Expose your test-asserted operation
Once the operation is acting as an assertion, it is time to think about reusability. In almost all cases, the assertion is too big to be exposed directly to the test. Therefore, think about readable and usable ways of exposing the assertion to your test. If your system uses a test framework with a lot of Assert.XXX methods, then you could place your file assertion in an AssertFile.XXX one. Same goes if your system is using extensions on existing types for their assertions, you can create a new extension for your file assertion.
It is important that you treat your operation as an assertion all the way, because that is what it becomes: a test assertion.
Conclusion
This post talked about how functionality in implementation code differs from test code. When an operation is run as a test assertion, a lot can go wrong and all those failures need to be exposed to the tester in a humanly readable manner. Treat your operation as an assertion, because that is what it is. It even goes further. Every single line of code that gets executed after the interaction with the SUT (system-under-test) could be considered an assertion. It does not matter if it is considered ‘infrastructure code’, if it gets run in the test, it looses its roots and grows into a test assertion. And every test assertion needs good failure management and be exposed like any other assertion.
Thanks for reading!
Stijn
Subscribe to our RSS feed