For my entire career as a software developer, I have believed that unit testing is an essential activity that is part of delivering a quality product. However, what that means to me has gone through multiple revisions. This post is not intended to give a detailed overview of how to write unit tests or any specific unit testing practice. What I intend is to take you through my journey and a few things I’ve learned as I’ve incorporated automated unit testing into my developer’s tool belt.
I started my career as a professional software developer writing classic ASP applications with VB COM components as the business and data layer. At that time, I would unit test the VB components manually using a debugger. Often, I would build up a scaffolding that would allow me to invoke various methods and then I would step through and observe the behavior to ensure that the code did what I expected.
This approach presented a few obstacles:
- Every test was manually run, and hence any given test was not necessarily repeatable.
- In order to unit test, I had to build up some form of scaffolding so that I could execute the methods I wanted to.
- Executing tests was labor-intensive and prone to error as it relied on my observations.
As I moved to the .NET platform, I became familiar with nUnit and the idea of writing automated unit tests. Since I was already building scaffolding in order to unit test previously, the amount of effort required did not really increase. Instead of writing scaffolding, I just wrote tests. As an added benefit, anytime I thought of a new test and wrote it, that test became repeatable.
The challenges that I encountered at this stage in my learning were:
- I was not yet familiar with stubbing or mocking practices; hence, my tests frequently involved database access. This made my tests relatively slow and created dependencies on the data in the database.
- I tended to think of Unit Testing as the need to write a test for each method that I wrote. This meant that although every method would have a test, the entire behavior of the method was not necessarily covered.
During this time period my unit test classes would all be named following the convention MyClassTest with individual methods being named MyMethodTest.
I became more aware of the fact that my tests generally covered the “happy path” and often did not cover edge cases or alternative behaviors. This led to a change in focus in how I looked at writing unit tests. At this time, I no longer think about testing individual methods or ensuring that every method in a class has a unit test. Rather, I am more interested in focusing on the expected behavior of the class.
Now, my unit test classes and methods tend not to focus on names of classes or methods, but rather on the behaviors that I’m interested in. For example, now I may use the class name When_saving_a_customer with the method name customer_is_saved_to_database_if_customer_is_valid.
Some benefits I’ve discovered from this approach:
- The unit tests generally cover a significant portion of the class that I am testing, including edge cases.
- The unit tests are more robust and hold up whenever I’m refactoring a class without having to make major modifications.
This approach also works really well with a Test-Driven Development (TDD) approach as it forces me to think about the behavior that I want, write the unit test for that behavior, and only then update the code to pass the given test.
This post is the first of a series in which I do intend to describe in more detail some of the specific practices and approaches I’ve used to create better unit tests and ensure a higher degree of coverage and confidence.