Unit Testing Basics

Scott McFarlane
lendingtree-engineering
8 min readJan 3, 2017

Let’s face it. As developers, we hate writing tests. Maybe it’s because we think it’s a waste of time — indeed it does take time. Maybe it’s because our brains already work faster than we can write code, so why further burden the coding process? Or perhaps it’s because we are so awesome that we never have to modify or refactor our code.

If you consider yourself a seasoned developer, but you’ve never written a single unit test in your career, you are probably not alone. I must confess that I was that developer until a few years ago when I decided to become a “student” of unit testing and learn everything I could about it. Like most developers, I am continuously learning and adapting my software design style as my knowledge grows. I have now come to realize the many benefits of automated software testing, and it has become an integral part of my software development process.

One of the most important lessons that I’ve learned is that writing tests isn’t just about testing. It breeds a whole new way of thinking about how you write code. Writing unit tests has many benefits — only one of which is the ability to quickly and frequently test your code.

Other benefits to testing include:

  • Tests serve as documentation of how your code is supposed to work.
  • Tests provide a self-verifying executable specification for your code.
  • Tests provide a safety net for incremental development and refactoring
  • Testing promotes the use of good programming principles, such as the Single Responsibility Principle (SRP) and the Dependency Inversion Principle (DRP). See http://en.wikipedia.org/wiki/SOLID_(object-oriented_design)

Unit testing is all about isolation. The problem is that the programs we write don’t live in isolation. They rely on other components or systems to do their work. We call these other components dependencies. To test the logic in one component, we must be able to isolate it from its dependencies — particularly those that reach out to alien worlds like file systems, databases, web services, or even the end user. We do this by not using the actual dependent components in our code, but instead by using abstractions of those components, or as in most programming languages: interfaces.

Interfaces allow you to define the protocol by which components interact, separately from the components themselves. By coding to interfaces rather than concrete classes, you eliminate the dependency on any one specific implementation of that interface. This allows you substitute a different implementation without modifying your code.

What Is A Unit Test?

So, what defines a Unit Test? Some definitions suggest that a unit test should test the smallest possible piece of testable code. In my opinion, the number of methods or lines of code your test covers is less important than making sure that your test takes only one path through the code. And, that it is only testing one “thing”. We’ll talk about what a “thing” is in a moment.

But the biggest distinction in my mind between a unit test and, say, an integration test, is that a unit test runs entirely in memory.

Any automated test has the following four stages:

1. Setup — This is where you build up the environment needed to run your test. This environment is often called a Test Fixture.

2. Exercise — This is where you execute the code being tested. This typically means calling a public method on a class, but your code may also be triggered in other ways such as an event being fired.

3. Verify — This is where you validate that the outcome of the test was what you expected. If the test failed, this step should give you enough information to determine why the test failed. In the Verify stage, you will always be verifying one of three things:

  • Return Value Verification — Given the input parameters 1 and 2, did the Add method return 3?
  • State Verification — Given a user has entered information about a new customer, did that customer get added to the system?
  • Behavior Verification — Given that an exception occurred in a component, did that component call the Send method of the email component to notify the administrator?

4. Teardown — This is where we undo any permanent “damage” the Exercise and Verify steps may have caused. Unit tests, since they run entirely in memory (by my definition), should never need the Teardown stage.

The “Thing”

Earlier I stated that a single unit test should only test one “thing”. How you define one “thing” depends entirely on the nature of your application, but you should generally be able to describe it using a simple sentence, without using the word “and” — in fact, I like to spell it out in my test method name. Keep in mind that testing one thing doesn’t necessarily mean that you will only have one assertion in your test. For example, state verification may require several assertions. However, if checking a particular state requires many assertions, then consider using a helper method to make your test more readable.

The benefit of only testing one thing is that if the test fails, then you have a much better chance of quickly identifying the reason why it failed. If multiple conditions are tested in a single test method, then you’ve got the extra step of tracking down which of the conditions failed before you can start tracking down your bug.

Characteristics of Good Tests

Keep in mind the following good practices when writing tests:

  • Tests should be fully automated and easy to run — the easier your tests are to run, the more likely you will be able to run them, and run them often. They should also run fast enough to provide timely feedback.
  • Test should be reliable — Tests are useless if you can’t trust them. They should be repeatable, independent, and self-checking. Any data used for an automated test should be isolated by test. Shared data between tests can lead to unreliable tests.
  • Tests should be easy to maintain — You should treat your test code with the same care and attention as your production code. As your code evolves, your tests will evolve as well. If tests are hard to maintain, they will quickly become obsolete and you will stop using them.
  • Tests should help us understand the system — Tests can be a great source of documentation, and since they are compiled code they will never become out-of-sync with production code like code comments tend to do.

Writing Testable Code

Once you get into the habit of writing automated tests, you will quickly notice another habit starting to form — writing testable code. If you have ever tried to write tests against legacy code, then you probably know what untestable code looks like. It is littered with hard-coded dependencies, user interface logic, global variables and complex constructor logic.

To create testable code, one of the most important software engineering principles to follow is the Dependency Inversion principle. This basically means that instead of creating your dependencies using the new keyword, have your classes receive their dependencies through constructor arguments or property setters. This, in turn, leads to the use of abstraction; using interfaces to represent your dependent components rather than concrete classes. Let’s look at a simple example.

Consider the following simple code example, which might represent some business logic code that uses a data tier, and a logging component.

public class Example1{    public void DoTheWork()    {        DataRepository dataRepository = new DataRepository();        Logger logger = new Logger();        logger.Log(“Getting the data”);        DataSet theData = dataRepository.GetSomeData();        // Do some work with the data…        logger.Log(“Done.”);    }}

While it is good that we’ve separated the data access and logging code into their own components, there are still some issues with this example. First, this class is not only tightly coupled with its dependencies; it is also responsible for their creation. This results in the following issues:

  • This code is nearly impossible to reuse because it is so tightly coupled with its dependencies.
  • This code would have to be modified if there is a need to replace one of the dependent components with a new implementation.
  • This code is impossible to unit test, because it cannot be isolated from its dependencies.

Now examine the following alternative:

public class Example2{    private readonly IDataRepository _dataRepository;    private readonly ILogger _logger;    public Example2(IDataRepository dataRepository, ILogger logger)    {        _dataRepository = dataRepository;        _logger = logger;    }    public void DoTheWork()    {        _logger.Log(“Getting the data”);        DataSet theData = _dataRepository.GetSomeData();        // Do some work with the data…        _logger.Log(“Done.”);    }}

Here, we’ve done two key things:

  1. We’ve introduced abstraction by creating and developing against interfaces rather than specific concrete implementations.
  2. We’ve introduced the Dependency Inversion Principle by using a software design pattern called dependency injection. The class is no longer responsible for the creation of its dependencies — they are injected into it via the class constructor.

Why is this important for unit testing? Because a test can now create an instance of this class, substitute its own “fake” implementations of IDataRepository and ILogger, and exercise the DoTheWork method in complete isolation from any actual database or logging mechanism. The tester now has complete control over what gets returned by the GetSomeData method, and can test any number of possible scenarios — including throwing exceptions — and verify that the code does what it is supposed to do under those conditions. It can also interrogate the fake logger and verify that it is getting called when it should.

So now we have “testable code”, accomplished through the use of the following key principles and design patterns of software engineering:

  • Separation of Concerns — This class is now only responsible for the specific job it was designed to do.
  • Abstraction — The interfaces define how our class interacts with its dependent components, but eliminate the dependency on any specific implementation.
  • Dependency Inversion (also called Inversion of Control) — The class has relinquished control of the creation and initialization of its dependencies.
  • Dependency Injection — The dependencies are “injected” into our class through its constructor.

These are all very good habits for any software developer, but you will find that they start to come naturally when you force yourself to write tests as early as possible in your coding process. Testing early (or even before the production code is written — a practice known as Test-Driven Development or TDD) forces your code to be testable without having to think about it.

The best part is understanding and using these principles will ultimately lead to higher quality code — code that is extensible, maintainable, scalable and reusable.

Join Us

Thanks for reading about how we are doing things here at LendingTree. If you would like to join and help shape our company, please visit careers.lendingtree.com and contact us. Follow us on Twitter: @Careers_LT

--

--

Scott McFarlane
lendingtree-engineering

Scott McFarlane is a senior developer with the My LendingTree team.