Testing is one of the few ideas that really changed my perception about software. It has become part of my everyday coding routine and is something that once you learn using it, it affects the way you write software.
Developing a software without testing is the same as developing software without a version control system or having a database in production without taking backups. It’s really scary!
Creating good tests is the programmer’s responsibility. Tests are production code. Personally, to me, it is more important to have good tests than good code. I can always refactor bad code if I have a good test suite that I trust. Refactoring is cheap when you have a good test suite.
Tests have many properties, but today I’ll talk only about the FIRST principles.
I need my test to be really fast! I want a fast feedback loop. I want to be able to run the tests on every line of code that I write. I want to know if the last line that I’ve written broke the behavior of the program. This way I can easily identify the line of code that doesn’t work.
I want my tests to be like my IDE.
When I make a syntax error, the IDE immediately highlights the line that has the syntax error. Imagine if it took the IDE 5 minutes to give you feedback when a syntax error occurred. Would you wait for 5 minutes before you write your next line or you would write the code and later “fix” the errors? If your tests aren’t fast, you won’t run them often. If you don’t run them often, then you will not pay attention to them. If you don’t pay attention to your test, then you have no feedback loop about your progress. Do you want your IDE to tell you about a syntax error 5 minutes later?
I follow the 3A rule. Arrange, Act, Assert.
This step builds the necessary state for your test case. I usually spend time thinking good names for variables and helper functions that build the state. They help to better understand the situation that the test case describes.
Invoke the method under test only.
Assert the logical outcome/expectation. Logical assertions can have multiple physical assert statements. I usually consider creating functions that describe what a single or a group of physical assertions mean in the context of the test case.
Tests must have deterministic results. Given that I provide an input X when the method under test is invoked, then the outcome should always be Y. The tests should not be affected by the order that they are called or how my times you run them.
Each test should own and arrange its own data. If I have common data structures with other test cases, then I create a helper function. The helper function name should make sense for both test cases. If the name of the helper function doesn’t make sense for both test cases or if I can’t find a good name that satisfies both test cases then I prefer to have 2 functions with good names with the same code. I treat such a function as an accidental duplication.
No manual inspection to check whether the test has passed or failed.
My primary goal when writing tests is not to have 100% code coverage. I aim to have a good test suite that I can trust. But what trust really means? To me trusting the test suite is just asking the following question: “Am I confident to push this code to production?”. If the answer is yes, then I have a test suite that I can trust. I invest time and effort to build such as test suite.
I practice TDD, I write my tests before I write my code. This way I can describe a good test case and then go implement it. This means that I let the test drive the design. I let the use case that I want to implement become my test cases. I let the test cases describe the behavior/intention, and I go implement the behavior.
By writing the tests first, I am forced to describe in my test case the behavior rather than the implementation because I have no implementation yet. This is a nice trick! If I decide to refactor, which means, “Change the implementation without changing the behavior”, then all I need to do is keep the tests green. If the tests are green, because of TDD, the behavior is the same.