Why Should Unit Tests Feel Like Simulations?
Unit tests are designed to test units of software, but what exactly is a unit of software? It can be a function or a method, a class, or even an entire module.
If you're just starting with unit testing, chances are you're testing the implementation of a function or a method. Consequently, if the implementation changes, you must update your tests as well, which can render the entire process pointless. This is often the case with small pieces of code, particularly in embedded development, where there are many hardware dependencies. You mock dependencies and, in tests, check if a method on a dependency reference was called because that's all you can test.
Testing units of software in isolation just for the sake of testing can be pointless. What if instead of testing units of software, you test units of work?
Test behavior: what not how
Instead of testing the implementation of units of software, think about the units of work your system needs to execute to make a meaningful change. We are interested in what the system is doing, not how.
For example, if you have an embedded system with a keypad and a display, think about what happens when your system is in a defined state and a user presses the down button. What should your system do in this instance? What is the user expecting? Depending on the current state, a number on display may decrease, or if the number already has the lowest value, it may need to overflow to the highest one or stay the same.
Given that the FSM is in a set temperature state and the display shows 25 when the user presses the button down, then the display should show 24. Or, given that the FSM is in the set temperature state and the display shows 17 (the lowest possible temperature defined by requirements) when the user presses the button down, then the display should show 17 (again).
Tests like these will likely require multiple software components working together, such as an event dispatcher, Finite State Machine (FSM), display module, display driver, etc. Depending on the level of coupling, you will need to mock components that are hardware dependant to make the testing possible. And while you could call these integration tests, they actually add value by testing the units of work.
This idea is a part of Behavior-Driven Development (BDD), a methodology that builds upon Test-Driven Development (TDD). Whether you write your tests before or after implementation, what matters is what you are testing against. Choosing tests that make sense and add value to your development process is crucial.
Tests as simulation
Testing behavior feels like a small part of a simulation of your entire system. You put the system, or parts of it, in a known state, simulate inputs, and measure outputs. Writing them looks like writing a simulation but with a difference of verifying simulation outputs to those that are expected according to functional requirements.
By designing small simulations of a system in a given state, we can test what the system is doing without going into the implementation. We can optimize performance while keeping sure that system is still doing what it needs to. Tests will fail if the required functionality is broken, which allows us to change the implementation. To tests, the system is a black box, and they make sure it does what is required.
If you think about tests as simulations, your focus shifts to thinking about what the system is doing, not how it implements it internally. As a consequence, your tests are more robust as they don’t depend on the implementation, and they are more meaningful as they provide you with a stronger sense of confidence that your system is doing what it needs to do.
Behavior tests can have a different granularity depending on what exactly you are testing. They should keep you aware of the philosophy of testing what the system or part of it is doing, not how. Testing the how will cause your tests to change every time you change the implementation, which might make entire testing pointless.
Treat the system as a block box and verify what that black box is doing. Make small simulations and verify their results. Verify functionality. Happy testing!
To post reply to a comment, click on the 'reply' button attached to each comment. To post a new comment (not a reply to a comment) check out the 'Write a Comment' tab at the top of the comments.
Please login (on the right) if you already have an account on this platform.
Otherwise, please use this form to register (free) an join one of the largest online community for Electrical/Embedded/DSP/FPGA/ML engineers: