A Case for Unit Tests
Frederic Simard, M.Eng.
Team Leader, Software Architect, AI enthusiast and speaker
I've been thinking about unit testing a lot recently. Some might argue that I care about unit tests a little too much, and I'd be inclined to agree. I would, however, point out that it's not the tests per se, but software quality itself that preoccupies me. Let's be frank: as it stands, the overall state of software quality is dismal. Considering that software engineering is a professional discipline, we should collectively feel a little bit of shame that we are not doing better than this.
Why don’t people do unit testing?
There are multiple reasons why things are this bad. The biggest one, of course, is business driven. You've probably seen it before: a project is launched, and at the start everyone has the best of intentions. Planning is done, some time is set aside to gather customer requirements, to define the architecture, to develop the solution, and to test it. If the project is big enough there's a QA team that will make sure that there aren't too many bugs. We start work, things get chaotic, priorities change, and then... we hit a deadline. What's the first reflex of many managers in that situation? We push back unit testing to the very end. Then the end approaches, we're still swamped with work, and the tests are never written.
This should never be OK, but it happens. A lot.
Another reason that this happens is misunderstanding. Unfortunately, a lot of developers confuse unit tests with integration / functional tests. They think a unit test is meant to test one feature.
If that's what you think, let me assure you, IT IS NOT. They are completely different. We'll get back to that later.
Although I'm sure there are others, the last reason I want to mention is improper training. While I can't comment on the quality of all academic institutions, if most IT programs are like the one I did, there should be a least one class somewhere in the curriculum that will put some emphasis on the importance of software quality, and students will be at least exposed to the concept of unit tests.
The thing is, as you may know, that our field is seriously understaffed. We're always looking for new resources. When that fails, some companies will resort to bringing in people from connected professions and training. As an example of this, I’ve met a number of IT workers / developers who had a background as actuaries and accountants. On the surface this made sense because they were the most knowledgeable in their employer’s field of activity, and they already had right mindset for this type of work (IT is at its core, math, math, and more math).
However, there is a reason why a bachelor's degree in CS takes three years to complete. There area lot of concepts you should be exposed to, and you won't get that exposition from work experience alone. If you are a driven and motivated individual, I have no doubt that you can bridge that gap through personal training, and reading the right books. Let's be honest though: most people won't. So, the result is that we have an important number of people who were never properly told what a unit test is, why we want to have them, and how they are NOT the same thing as a functional test.
What is a unit test?
The simplest explanation is that a unit test is a test that will validate the design of ONE unit of code - a method - for ONE class. It will target a public method, and the private methods it calls, but it will NOT execute code that is outside of the scope of that class. That's right: if your class calls a method from another class, you do not want to execute the code of that second class.
Why?
Well, if you've tried to write tests on classes that are too tightly bound together, you'll see that things can get out of control fast. Before you know it, you end up making calls to the database when all you wanted was to make sure that the logic of your method is sound. And that could be bad. Really, really bad.
That's why one of the GRASP principles is low coupling. As it's defined by Wikipedia, low coupling means lower dependency between the classes, ensures that changes in one class have a lower impact on other classes, and makes for higher reuse potential.
The best way to achieve low coupling is with good design, obviously, but the key to this design is what we call the dependency inversion principle. Robert Martin, aka Uncle Bob, talks quite a bit about this principle here, and the SOLID acronym, which stands for 5 good software design principles, also includes this principle (the “D” stands for Dependency Inversion Principle).
The idea is that when a class needs to work with another, it should only be aware of that class' methods through an interface, i.e. the abstraction of that class, instead of being aware of the concrete implementation.
One way to do it, and a better way to do it
Here’s a glimpse of what the code of these classes would look like (in C#):
public class Manager
{
public void HireEmployee(IEmployee employee)
{
//Go through the paperwork
//Train the employee?
employee.StartWork();
}
public void FireEmployee(IEmployee employee)
{
//Go through the paperwork
employee.GoHome();
}
}
public interface IEmployee
{
void StartWork();
void GoHome();
}
public class Employee : IEmployee
{
public void StartWork()
{
}
public void GoHome()
{
}
}
The benefit of this should be immediately clear:
- The first class, Manager in this case, expects to work with a class that implements the IEmployee interface;
- Employee, or any other class implementing IEmployee, will be passed as a parameter to the constructor of Manager or the public method needs that reference;
- Since Manager only knows that it will receive a class that implements IEmployee, the actual class we pass during execution doesn't matter;
- This allows us to substitute another class when it's necessary, i.e. when we're doing tests.
This is when we're going to use what is called a Stub, a class that implements Interface IEmployee, but will either do nothing or do the strict minimum to ensure that the test of Manager goes on. Since our goal (and the metric we use) with unit testing is code coverage, we want to make sure that every possible code path is tested.
If you don’t use testing framework, like MOQ, RhinoMock, or NUnit, you might create your stubs manually:
public class EmployeeStub : IEmployee
{
public void StartWork()
{
}
public void GoHome()
{
}
}
Ultimately, unit tests have three goals:
- Force you to rethink the design of your method to make sure you got it right;
- Force you to consider not just the happy path (when things go as expected), but also the not-so-happy path (some will argue that TDD does this better);
- Act as a safety net when you make changes: either the test breaks as expected, either it doesn't when it should have.
Conclusion
Personally, I think that the best comparison for unit tests is double-entry bookkeeping. It really should be considered the software equivalent. In double accounting, you have a debit and a credit column, and every entry requires an opposite entry in a different account. When you're done, your numbers should add up to the same in both columns, what we call "balancing the books". It's a summarized explanation, but I think you get the idea. The difference is that we've been doing double accounting for over 800 years (With some form of it being recorded even as far as the 11th or 12th century in Korea), while IT is... about 70 years old? So, we're still immature in that regard.
That should not be considered an excuse, but a call to do better!
References
- Header image, https://pixabay.com/en/software-testing-service-762486/, downloaded December 16, 2018.
- Why 45% of all software features in production are NEVER Used, David Rice, https://www.dhirubhai.net/pulse/why-45-all-software-features-production-never-used-david-rice/, consulted December 12, 2018.
- école de Technologie Supérieure’s website, https://www.etsmtl.ca/en/Studies/Graduate-Programs/Master-Software-Engineering, consulted December 12, 2018.
- Wikipedia entry on GRASP (object-oriented design), https://en.wikipedia.org/wiki/GRASP_(object-oriented_design), consulted December 12, 2018.
- Wikipedia entry on Robert Martin, https://en.wikipedia.org/wiki/Robert_C._Martin, consulted December 12, 2018.
- The Dependency Inversion Principle, Robert Martin, https://condor.depaul.edu/dmumaugh/OOT/Design-Principles/dip.pdf, consulted December 16, 2018.
- The Principles of OOD, Robert Martin, https://butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod, consulted December 12, 2018.
- MOQ GitHub repository, https://github.com/Moq/moq4, consulted December 12, 2018.
- RhinoMock framework website, https://hibernatingrhinos.com/oss/rhino-mocks, consulted December 12, 2018.
- NUnit Framework website, https://nunit.org/, consulted December 12, 2018.
- Wikipedia entry on double-entry bookkeeping, https://en.wikipedia.org/wiki/Double-entry_bookkeeping_system, consulted December 12, 2018.
- PlantUML server, https://plantuml.com/, (used for creating diagrams), used / consulted December 12, 2018.