Defining the Unit in Unit Test

A unit test is a software test that focuses on a subset of an application, often removing the overarching context for how the code is used. It’s common to find a unit test focused on a single function and just the logic in that function, possibly encapsulating private functions available to it. Sometimes it makes more sense to include code from multiple modules, or processes, inside the scope of your unit tests, expanding the scope of the “unit.” Keeping a narrow scope can be beneficial to finding errors faster, but learning when it makes sense to expand the scope can greatly reduce the complexity of your codebase and your tests. If the scope of your unit tests, the unit itself, can be expanded slightly to include other modules, your tests can sometimes end up being simpler and easier to maintain while still providing all the benefits of a traditional, narrowly scoped unit test.

Before we dive into when and how to expand the scope of your unit tests, let’s touch on why we test. We need to keep four goals in mind when designing our tests:

  • Prove that your code does what it’s supposed to do.
  • Prevent breaking changes (regressions).
  • Make it easy to find where your code is broken.
  • Write the least amount of test code possible.

It’s likely you’re already on board with the first two goals. However, the last two goals are what will guide you in choosing how much of your code should constitute the unit in the scope of your tests. Your tests should draw a black box around the code in their scope, which we’ll call the code under test or the application code. This black box defines the places where your test code interacts with your application code. In the case of a unit test, this black box would often treat a single function as the unit. The advantage of keeping the scope narrow is that if the code under test has issues, keeping the scope small helps you quickly identify where the issue is. The downside to a narrow scope is that often, in order to isolate the code under test, you need to write more code in your tests, which can make it hard to follow what’s happening. Allowing your test scope (black box) to include a well-tested, functional dependency can help you take advantage of the narrow scope while avoiding some of the steps of isolating the code that uses that dependency.

Your tests need to interact with the code under test in two ways. The first is the more obvious one: your test will call a function (or send a message) with parameters and it will get a return value. The second is more complex. Your tests might need to intercept the calls out and return responses, stepping in for a dependency, if your code depends on other code in your codebase. We call this “isolating your code under test.” We will cover isolating code later in this chapter, but first we’ll look at when we can get away without strict isolation. The following diagram shows the black box drawn around the code under test.

images/blackbox-narrow.png

Because the code under test depends on other code in the codebase, the tests will have to address isolating the code. There are different methods of isolating code, but all of them come with costs, typically in terms of time and test complexity. The presence of purely functional code can impact what needs to be isolated. A function is “pure” if it returns the exact same answer and has no side effects every single time you call the function with the same parameters. We’ll expand on this later in this chapter. When your dependency is purely functional and well tested on its own, you can expand your black box to include that dependency, as seen in the diagram shown.

images/blackbox-wide.png

Expanding the black box allows us to skip the extra work of isolating your code while not giving up the ability to easily locate your broken code. Your tests are still narrowly focused on the code under test, and—because your code is purely functional—the environment in which you’re testing your code is completely controlled, preventing inconsistent results. When you’re able to include dependencies in this fashion, your tests are easier to write, understand, and maintain. Because your dependency is tested on its own, you can write less test code while still being able to identify where your code is broken when a test fails. If your dependency’s tests don’t fail but you have failures in the tests where you pulled it in, you know that your failures are in the new code and not in your dependency.

The challenge is finding the balance for when you should include your purely functional dependencies in your test scope. Tuning this sense will take some trial and error and learning from pain points. Start off by expanding your black box to include those dependencies and then tighten the scope if you find yourself having issues debugging your code. Practice will help you hone your sense of when this is the right choice.

Now, let’s learn to write some tests.

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

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