Unit Testing as a Method to Enforce Requirements
I have recently started putting a lot of stock in automated unit testing. Not that I didn’t think unit testing was important in the past, I just didn’t know how to write them properly. A little instruction from a co-worker has corrected that situation and I am now writing proper unit tests. I like the test first approach, though I don’t know that I’ll be doing the full “TDD, no code without a test” thing just yet, but at the moment, I am writing tests first. Only time will tell if I can keep it up throughout the life of my project.
I recently ran into a situation that made me see some additional value in my tests. Beyond making sure everything works properly and lives through changes to the application, I am starting to see how my tests keep me in check and make sure I don’t stray from the requirements.
As an example we’ll assume we are testing classes from an application’s business layer. Each class in this layer will inherit from an abstract base class called “ServiceBase”. In this case, the ServiceBase is a generic class with some abstract members, but it also contains implementation code for a Save method.
The Save method accepts a business entity as an argument and decides whether to call Insert or Update in the underlying data access class. If the value passed in is null, the method throws an exception. This behavior is pretty standard, but some classes that inherit from it may have a need to perform some additional steps when saving. To account for this, I have also made the Save method virtual so any implementation that needs to can override the Save method.
Now we will create a couple of subclasses for ServiceBase. Let’s call them ProductService, CustomerService and OrderService. To start we create the ProductService class and make it inherit from the ServiceBase class. Now in order to make sure we have a complete set of unit tests, we create a test that expects an exception if a null item is passed to the save method. As long as the ServiceBase class throws the exception, it will pass for our test against the ProductService class. Now if we repeat the process (including the test) each class (ProductService, CustomerService, and OrderService), they will all have the ability to save an item, and each test that expects the exception should pass.
Now, let’s say the ProductService class has a need to write to a log when its Save method is called. This is easy, all we have to do is create an override for the Save method in our ProductService class. We add our logging code and all is good. We run our test and it fails.
Why did it fail? Because in my contrived story, we forgot to add the code that throws an exception if a null value is passed in. This is an honest mistake, but it’s ok because our test has saved us. This type of requirement cannot be enforced by the compiler through an interface or abstract base class. As far as the compiler is concerned, this code is completely valid. The unit test however, has alerted us to the fact that we did not implement the code according to our requirements (which was the basis for writing the test in the first place).
While this may be a no-brainer for those of you who have been practicing test driven development or just writing solid unit tests for a while, but for those of us who are just starting down this road this is an added benefit that may not seem obvious at first.
.NET, C#, TDD, Unit Testing, programming