Chapter 11. Unit Testing

Unit testing is the process of breaking down your application to the smallest possible functions, and creating a repeatable, automated test that should continually produce the same result. These tests are the heart and soul of your application. They provide the foundation that all future code is built upon. Without unit tests, it is possible that a seldom-used function could remain broken for months without anyone noticing. With unit tests, every system function can be verified before a single line of code is even merged into the master branch, let alone pushed to production.

As a frontend architect, your primary role is making sure that developers have the tools necessary to be as efficient as possible. Unit tests are one of those essential tools for building an application of any scale. Whether your application logic is written mostly in a backend or frontend language, there are plenty of options to fit your workflow. No matter if you are running PHPUnit with PHP, NodeUnit with Node, or QUnit with JavaScript, you will find mature, stable platforms to build your tests upon.

Though your technology stack (and the tests associated with it) might be left up to the software architect, it is quite probable that your frontend developers will also be writing code that requires tests. Therefore it is important to become familiar with as many of the suites as possible. Acquiring mastery, or even proficiency, with all of them is not something we typically have the luxury of doing, but a solid understanding of the basic concepts will help you and your team write more testable code, and get up to speed with any framework quickly.

Let’s review some of these basic concepts now, and then we’ll have an opportunity to look at the code in action.

The Unit

“Do one thing, and do it well” is the mantra when you are building an application with unit tests. Too often we write functions that try to do too many things. Not only is this very inefficient, as it does not lead to reuse, but it also makes these functions very difficult to test.

Consider this simple function: given a customer’s address, the function determines the cost to ship a product to that customer from the nearest distribution center.

Let’s break this function down a bit. The first thing that happens is that our function uses the address to find the nearest distribution center. Using that distribution center address, the function then determines the distance between the center and the customer’s address. Lastly, using that distance, the function calculates the shipping cost to move the package from point A to point B. So even though we have a single function, the function is performing three separate actions:

  1. Look up distribution center nearest to given address.
  2. Calculate a distance between two addresses.
  3. Evaluate shipping cost for a given distance.

Going back to the idea of “doing one thing, and doing it well,” it is pretty obvious that we could better accomplish our address lookup action by combining three separate functions. When our action is split up, we get a function that allows us to find the nearest distribution center to any given address, a function that calculates the distance between two given addresses, and a function that returns the cost of shipping a product any given distance.

More Reuse

Now these three functions can be used throughout our entire application, not just for calculating shipping costs. If we have another part of the application that needs to find a distribution center, or to calculate the distance between two addresses, those functions have already been created. We aren’t reproducing small units of functionality over and over again in separate, larger functions.

Better Testing

Instead of testing every possible way our application might determine shipping, we can instead test each individual, reusable function. As our application grows, the number of new functions needed to create new features decreases. In the end, we have a smaller number of less complex functions performing more advanced functionality.

Test-Driven Development

When most of us first approached unit testing, we probably wrote up some functionality that met a business goal (such as our shipping example) and then worked to refactor it into smaller, reusable, testable bits. At that point, we would consider the tests we’d want to write. Test-driven development (TDD) turns that idea upside down by putting the tests first, before any functional code is even written.

But if we write tests for functions we haven’t created, won’t they all fail? Exactly! TDD sets out to describe how a system should work when written properly, and creates a path for us to create that system.

For our shipping example, TDD would write three tests, one for each of the three functions required to perform this business function. The developer’s job is to turn those failing tests into passing tests. First, we write a function that properly looks up the location of the nearest distribution center, and we have one passing test. We then move on to measuring distance and calculating shipping, and finish with three functions that make their associate test pass.

With this done, not only have we built the functionality required for our application to look up shipping cost, but we have complete test coverage for these functions.

A Test-Driven Example

At its core, unit testing is extremely simple. The basic idea is to call the function being tested, passing it preset values and describing what the result should be. Let’s look at how we’d do this for our function that calculates the shipping cost for a given distance:

function calculateShipping(distance) {
  switch (distance) {
    case (distance < 25):
      shipping = 4;
      break;
    case (distance < 100):
      shipping = 5;
      break;
    case (distance < 1000):
      shipping = 6;
      break;
    case (distance >= 1000):
      shipping = 7;
      break;
    }
    return shipping;
  }

QUnit.test('Calculate Shipping', function(assert) {
  assert.equal(calculateShipping(24), 4, "24 Miles");
  assert.equal(calculateShipping(99), 5, "99 Miles");
  assert.equal(calculateShipping(999), 6, "999 Miles");
  assert.equal(calculateShipping(1000), 7, "1000 Miles");
};

QUnit has several operators for testing assertions, including ok() for testing Boolean values or deepEqual() for comparing complex objects. In this case, we are using the equal() function to compare the value returned by calculateShipping() with the expected result. As long as calculateShipping(24) returns a value of 4 (which it will here), our test will pass. The third value, 24 Miles, is used to label the pass/fail statement in the test output.

With these tests (and others) in place, we have a single suite to run that will assert whether or not our system is working. If someone were to change the name of the calculateShipping() function or modify the shipping prices, this test would return failures, and we could fix the problem before offending code was pushed to production.

QUnit is capable of doing much more than the preceding example. For instance, it is capable of performing tests on both synchronous and asynchronous functions. QUnit can also interact with the web page that the tests are loaded on (remember, this is all just JavaScript). So if your tests include values being returned when a mouse is clicked or a key is pressed, QUnit has you covered there too.

How Much Coverage Is Enough?

Determining proper test coverage can be a very difficult balancing act. If you aren’t running test-driven development (where nothing is written without tests), it will be important to determine how much coverage is enough coverage. Test everything, and your development process can get bogged down. Don’t test enough, and you risk regressions slipping through.

Fixing the Gaps

If you are implementing unit tests on an existing project, you most likely won’t have the time or budget to write 100% test coverage for current functionality. And that’s OK! The beauty of test coverage is that even a single test adds value to a system. So when determining where to start writing tests, look for the biggest wins first. Sometimes the biggest win is writing tests for the simplest parts of your system. Just like paying off your small credit card debt before trying to tackle your larger debt, writing some simple, but still valuable, tests will be a great place to build momentum.

Once you have a working suite providing some basic coverage, start looking at the parts of the system that are either the most critical, or have had recurring trouble in the past. Create stories for your backlog for each of them and make sure to pull them forward as often as possible.

Coverage from the Start

If you are fortunate enough to be starting a new project as a frontend architect, your job is not just to get a testing framework set up, but to make sure that the development process itself is prepared for unit testing. Just like writing documentation or performing code review, writing unit tests take time! You’ll need to make sure that any story requiring tests is given the extra time required to write and verify the necessary test coverage.

At Red Hat, every user story starts with a set of tasks and time to develop and verify the test coverage required for that feature. If a new feature is estimated to take eight hours of development time to complete, we make sure to schedule another two hours to write and verify test coverage. This additional time can often be a hard sell, so the frontend architect frequently needs to play diplomat and salesperson. Even though this is a 25% increase in the time, we know that this test coverage will save us dozens of hours in the future that would have been spent tracking down bugs.

As I said earlier, not every feature requires the same amount of test coverage. But the assumption is that every story starts with tasks for test coverage. As long as those tasks are only removed when everyone agrees that coverage isn’t necessary, we can be confident that any feature requiring coverage is given the time needed to finish that task.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.254.44