What Is Unit Testing?

Agile Unit Testing 1

Unit Testing: A Tutorial on What It Is, How To Do It + Tools To Use

What Is Unit Testing?

Unit testing is essential. Without it, you’re walking on eggshells all the time. That’s what you hear, at least. But why is that? And what is it, anyway? Let’s dive in.
Unit testing is about verifying the behavior of the smallest units in your application. Technically, that would be a class or even a class method in object-oriented languages, and a procedure or function in procedural and functional languages. Functionally, it may be a set of closely related classes. Like a `Deer` and its supporting `Head,` `Tail,` and `Locomotion` classes. What you consider a unit of work is whatever makes sense to you. There are no hard and fast rules. As long as it helps you understand and think about your application. That’s different for what a unit test is.

What Makes a Unit Test a Unit Test?

Michael Feathers (author of “Working Effectively with Legacy Code”) put the distinction at the process and system level. Unit tests need to run in isolation because they need to run fast. The entire unit test suite for an application needs to run in minutes, preferably seconds. You’ll see why later. That’s why unit tests can’t use any external processes or systems. No I/O operations of any kind (database, file, console, network) except logging test failures and perhaps reading the default feature toggle configuration at the start of a test run. If your test code (or the libraries it uses) does I/O or accesses anything outside its process, it is not a unit test, but an integration test.

What Is the Purpose of Unit Testing?

Many say that the purpose of unit testing is to validate that each unit of work behaves as designed, expected, or intended. And that you can run your unit tests every time someone makes a change. With just the push of a button. But the real purpose of unit testing is to provide you with almost instant feedback about the design and the implementation of your code. Creating unit tests, or rather the ease or difficulty with which you can create them tells you precisely how testable your code is. How well you designed it. And if you listen to it, instead of using hacks to overcome any difficulty, everyone wins. Here’s how.

What Are the Benefits of Unit Testing?

Unit testing has many benefits.
  • You save a lot of time in regression testing.
  • You get an alert whenever you inadvertently break existing behavior. Allowing you to tackle it immediately while still fully immersed in what you’re working on. Meaning fewer bugs escaping into the wild.
  • Fewer escaped bugs mean you have more time to create value.
  • Working without fear of breaking existing code without knowing it, you have more precious cognitive resources. Meaning you’ll be more creative and innovative, and able to create better solutions.
  • You get living, breathing, never out-of-date documentation — at least when you give each unit test a meaningful name (more on that later.)
  • Always up-to-date documentation allows you to bring new hires up to speed more quickly.
  • Working without fear of breaking existing code without knowing it, new team members become productive sooner.
  • Fewer bugs also mean you’ll reduce the load on support staff and free them to focus on customer success instead of controlling damage.
I’m sure you’re wondering by now how to get all of this.

How To Write Unit Tests (Using Best Practices)

Creating unit tests is the same as developing any code, but there is a difference. You create functional application code to solve a problem for your customers. You create unit tests to solve the problems that arise in developing that application code. Guess what that means. Yes, you are your own customer! And of course, you want to make your life as easy as possible
So, make the most of these best practices
  • Give your test method names that help you understand the requirements for the code you’re testing without having to look elsewhere. Pick one of several tried and tested templates that exist for this and stick to it.
  • Ensure a test only succeeds because the code it tests is correct. Likewise, ensure that a test only fails because the code it tests is incorrect. Any other reason for success or failure is either fooling yourself or chasing down a rabbit hole.
  • Make an effort to create short meaningful failure messages that include relevant test parameters. There’s little more frustrating than “Expected 5, but found 7” and having to hunt for the value of the parameters to the method under test.
  • Ensure that every test can produce the correct results (succeed or fail) even when it’s the only test you run.
When a test depends on how another test changed the environment (values of variables, contents of collections, etc.), you’ll have a hard time keeping track of the starting conditions for each test. More importantly, when you get unexpected results, you’ll forever wonder whether test conditions or production code caused them.
  • Do not optimize test setup beyond the source file of a test class. Keep a test class as a world unto itself, independent of others.
Use setup methods and nested utility classes if you like, but avoid test class hierarchies and general utility classes. It’ll save you having to hunt through several base classes or utility class units to find the values a test uses.
  • Follow the Arrange, Act, Assert pattern. Mark each part with a comment. The reason to follow this pattern is in the next point.
  • Each test should deal with only: One arrange — One scenario to test (one “given”). One action — One method to test (one “when”). One assert — One call to a verification method (one “then”).
When an action should have more than one effect, test each of them in a different method. When you feel tempted to use more than one assert, ask yourself whether you’re asserting the most significant fact.
  • When you get frustrated with how hard it is to arrange a test, listen to that pain. Take the hint that your code design needs improvement.
Resist the urge to use hacks and “automagic.” Hacks only ever address symptoms, and automagic reduces the transparency you need to figure out why a test fails that should succeed or, worse, why a test succeeds that should fail. Tackle what’s causing the pain using the most straightforward means possible. Quite often, you can make simple changes to make at least part of a class more testable. Working effectively with Legacy Code by Michael Feathers is an excellent resource for this.

Common Pitfalls in Unit Testing

Simple mistakes can trip you up badly. Worse, they can lull you into a false sense of security.
These are the mistakes you want to avoid
  • Writing tests without assertions.
Such a test will never fail! If that’s okay because you’re merely verifying no exceptions occur, make that explicit with an assert and an appropriate message.
  • Writing more than one test at a time.
It’s an indication that you’re testing after the fact (instead of a test-first approach), and you’re creating headaches for yourself in keeping track of which tests are complete and should succeed and which tests fail because they’re incomplete.
  • Not starting with a failing test.
When you don’t start with a failing test, you won’t know whether it succeeds because you have an error in your test or because the functional code is correct.
  • Not running tests frequently.
Ideally, you want to run all unit tests at every stage of a red-green-refactor cycle, whether you use that with or without a test-first approach such as TDD and BDD.
  • Writing slow tests.
Slow tests interrupt your flow. It’s okay, by the way, to use a unit test framework (see below) to write slow(er) tests, but they’re not unit tests, and you want to keep them in a separate test suite.
  • Making tests dependent on their testing environment.
That’ll give you headaches when tests fail in one environment and succeed in another. Stay with me now. You’re almost there. Here are some tools and techniques for you to use.

Unit Testing Tools and Techniques

Unit Test Frameworks

Unit test frameworks provide everything you need to create unit tests
  • Code attributes so a test runner (see below) can discover and run your tests.
  • Code attributes to provide your tests with test data.
  • Code attributes to provide your tests with test data.
You can find one or more unit test frameworks for almost every programming language out there.

Unit Test Runners

Unit test runners discover the tests in your test code automatically, run all tests or a specific selection, and then report the test results. They come as IDE extensions and as stand-alone command-line utilities. You can use the latter in build scripts, so integration builds fail when a merge breaks existing code. Unit test frameworks have their own runners, but you can also find dedicated runners that can discover and run tests written using multiple frameworks.

Mocking Libraries

Mocking libraries allow you to create test doubles, or fakes, easily.
You use these to provide a class under test with instances of their collaborating classes. This way, you can easily customize the collaborators’ behavior to the needs of your test.

Dependency Injection / Inversion of Control

Inversion of Control, or Dependency Injection, is a design pattern you use to break hard ties between classes. Instead of class A instantiating a class B5, you provide it with an instance of the most abstract B5 ancestor that gives A the methods and properties it needs. It makes unit testing a class with collaborators easier because it’s much easier to provide it with fakes.

Unit Testing Resources

  • Growing Object-Oriented Software, Guided by Tests by Steve Freeman
  • The Art of Unit Testing: With Examples in c# by Roy Osherove
  • Working effectively with Legacy Code by Michael C. Feathers.
  • Refactoring: Improving the Design of Existing Code by Martin Fowler.
  • Clean Code: A Handbook of Agile Software Craftsmanship by Robert C. Martin (Uncle Bob)

So, Go. Unit Test Your Way To Confident Coding

So, that’s it—everything you need to know to start your unit testing career. You’ll never have to walk on eggshells again when you change your code.

Signup for updates!

Check out Nimble Now!

Humanize Work. And be Nimble!