Enable Unit Testing

Make the Code Testable Before Testing It

Problem and Solution Overview

This recipe just states how to execute a solution. Read our Legacy Newsletter blog post: DevOps #2 – Enable Unit Testing to understand the specific problem we are solving and the solution approach.

This page contains the recipes for the 5 most test-blocking design flaws.

Recipe 1: Solving Poor Parameters
Peel Poor Parameters

Recipe 2: Solving Large Privates
Extract Large Privates

Recipe 3: Solving Method Calls
Signal Event Instead of Method Call

Recipe 4: Solving Micro-Methods
Gather Micro-methods

Recipe 5: Solving Long Methods
Make Template Method from Long Method

Bonus Recipe: Solving Real World Problems using the Recipes
Bonus: Decompose a Vase-shaped Legacy Method

Peel Poor Parameters

  1. Identify one parameter class that you would like to eliminate.
  2. Establish whether your method calls a mutating member on the parameter class.
  3. If not, then Peel Simple Data Parameter.
  4. If so, then Peel Complex Object Parameter.

Peel Simple Data Parameter

  1. Search and replace every access to the parameter. Replace parameter_name. with parameter_name__.
    • This converts every field or property access into an access to a local variable. Those local variables don’t exist yet.
    • parameter.field becomes parameter__field.
  2. Compile. Find all missing local variables.
  3. For each missing local variable:
    • Declare it at the top of the method. Assign the value from the field to the local.
    • At the bottom of the method, assign the value from the local back to the field.
  4. Check in.
  5. Use the compiler to find write usages for each local variable. If the initial assignment is the only one, then delete the assignment back from the local to the field at the end of the method.
  6. Check in.
  7. Extract method everything between the variable assignments at the top and the assignments back at the bottom.

Peel Complex Object Parameter

  1. Search for one instance of “parameter_name.“.
  2. Look around and Extract Method the right amount of enclosing code. This might be a simple expression or several lines.

    For example, chatty DB APIs might have you get a command object from a connection object, set the sql or sproc name to call, set parameters, and then finally call. You can extract all of that into a single helper method that takes parameters and calls that one sproc correctly.
  3. Write unit tests if you extract a non-trivial method.
  4. Check in.
  5. Transform the method call into an assignment of the method to a variable, then a call. saveUser(); becomes Action saver = saveUser; saver();
  6. Move the variable assignment to the top of the method.
  7. Check in.
  8. Repeat from step 1 until there are no more dereferences of the parameter.
  9. Extract method everything after the declaration of the function variables. Your new method will take several functions as arguments.
  10. Write unit tests for the method. Supply functions to perform interaction between the method and the test.

You have finished this recipe. Back to Top.

Extract Large Privates

If you have a private method that you wish to test separately, then it has some independent functionality. This functionality may be in the customer domain, but is often in a different domain — such as CS algorithms.

Therefore, the design really has a missing object. The private method(s) should really be the public API of a new class. That new class is in a different domain and has a different responsibility.

Executing this requires a Split Class refactoring sequence. The original class maintains a reference to the new class. You will typically move several private members and a couple of fields to the new class.

Many refactoring tools offer Split Class, but most of them are unable to guarantee that the refactoring will not change behavior.

If the Split Class is not safe, then apply the provably-safe recipe provided in Use Data to Split a God Class.

You have finished this recipe. Back to Top.

Signal Event Instead of Method Call

The purpose of this design change is to slightly reduce the responsibilities of the method we want to test. Currently, it is responsible to do:

  • Whatever its code does.
  • Whatever the codes does that it calls.

Specifically, that means it needs to:

  1. do whatever its code does,
  2. know when to call other code,
  3. know what other code to call
  4. gather the right data, and then
  5. call that code.

We will slightly reduce those responsibilities. We are changing the method’s responsibilities to:

  1. do whatever its code does,
  2. know when other code needs to execute,
  3. describe the situation so that someone else can decide what code to call,
  4. gather the right data, and then
  5. execute whatever code “someone else” has told it to execute in this situation.

We will describe the situation with an Event. The “someone else” will configure that event to point to the correct code. Now we can independently unit test

  1. The method fires the right Event in the right situation with the right data.
  2. The Event is bound to the right code in production.

Introducing an Event

  1. Split the method call into an assignment to a local variable, then a call through that variable.
    • saveGame(game) becomes Action<Game> saver = saveGame; saver(game);.
  2. Check in.
  3. Create an Event on the class.
  4. Assign the method to both the local variable and the Event. Be sure to clear all handlers from the Event before making this assignment.
  5. Change the call code to fire the event rather than calling the local.
  6. Delete the local.
  7. Check in.
  8. Extract Method the code that binds the handler to the event.
  9. Move that call to be initialized at an appropriate time. This might be static initialization time, during construction, or called by some other object.
  10. Check in.
  11. Write unit tests for your method. The test binds the Event to nothing (or to a tracker lambda).
  12. Check in.
  13. Write a test for the initializer code to make sure that the Event is bound to the correct handler after initialization.
  14. Check in.

You have finished this recipe. Back to Top.

Gather Micro-methods

The recipe here is very simple:

  1. Inline Method.
  2. Repeat as needed.

The challenge is finding the right methods to inline, especially as they are often on different classes. Use your story as your guide.

  1. Write the test you want to write, in English. The method body is entirely in comments.
  2. Identify the method called in the Act section and the code you check in the Assert.
  3. Inline Method until you get those to be on the same class.
    • If you are unsure whether these should be together, Inline them without deleting the original definition (just inline this one call site).
    • It is OK to create duplication. Often micro-methods arise because people have been over-zealous about eliminating duplication. You will probably have to add apparent duplication to fix the micro-methods.
  4. In the test, convert the English for the Act section into code. Leave the Assert as comments.
  5. Check in.
  6. Now identify the first part of Arrange that you need.
  7. Inline Method between the code called by Arrange and that called by Act.
    • Don’t necessarily get this down to the same class. Arrange may be arranging a direct collaborator, but Arrange and Act should be no more than one class apart.
  8. Convert this one part of Arrange from English to code.
  9. Check in.
  10. Repeat from step 6 until you are done with the Arrange section.
  11. Convert the Assert from English into code.
  12. Check in.

Repeat this recipe several times for adjacent unit tests, and you will start to see larger methods an classes emerge. The methods you create by Inline will naturally follow the tested responsibilities.

You have finished this recipe. Back to Top.

Make Template Method From Long Method

We proceed in two phases.

  1. Extract methods to leave template method.
  2. (optional) Convert template method to declarative call sequence.

Extract methods to leave template method

  1. Start at the bottom of the method.
  2. Extract method on one top-level code paragraph.
    • A code paragraph is a set of lines that hang together. This might be one control flow block or just a bunch of lines with a comment.
  3. If the new method takes parameters that feel closely related, then Introduce Parameter Object on those parameters.
  4. Check in.
  5. Write unit tests on the extracted method.
    • Unless it is too big to test, in which case continue with your original method and then refactor that new method as a next phase.
  6. Check in.
  7. Now start at the code right above the method you extracted. Go back to step 2 until you have extracted everything.

It is usually easiest to start at the bottom for two reasons.

First, the top of a method tends to be setup, so the most important action is usually near the bottom. Extracting that first makes it easier to come up with names for the rest.

Second, many languages allow multiple input parameters but only one return value. Starting at the end means each extraction will take in more data than it gives back, making the extractions easier. The Introduce Parameter Objects gather loose parameters and make it easier for the next extraction above to pass data out.

You want to finish with a method that consists entirely of method calls. There are no conditionals, loops, or other constructs.

(optional) Convert template method to declarative call sequence

The only remaining untested code is the outer method, and it consists of a straight-line series of method calls with good names. Rename those methods iteratively until they make sense in the domain.

Now assess: do you need to test the template method?

Many template methods can be verified by inspection, and changes can also be verified by inspection — as long as the change keeps it as a template method. If that is true for you method then adding a test will add no value. Stop here.

However, if you do need to test your template method, then be clear on what you need to test. You have already verified that each called method does the right thing. You just need to verify that the code calls the right methods in the right order.

You can accomplish this in two ways: mocks or a declarative call sequence. I avoid mocks because they are abused more often than they are used well. The most effective way to avoid problems at scale is to simply ban them. I don’t allow linking to any test double library.

Here’s how you make a declarative call sequence.

  1. Refactor each method until it takes the same signature.
    • For example, create a new class, move each method it, and lift all parameters to be fields. Set some in the constructor and others as side-effects within the methods. Now each method takes no args and returns nothing.
    • Another option is to create a chain, where each method takes one argument and returns one. Each takes the argument returned by the one before it, so they can be directly composed.
  2. Refactor the first method call to assign the method to a variable, then call it through the variable.
    • writeToScreen(); becomes Action s = writeToScreen; s();
  3. Check in.
  4. Wrap the call through the variable in a loop that iterates one time.
  5. Convert the variable from a function to an array of functions with one element. Inside the loop index to position 0 and call it.
  6. Convert the loop to iterate your array of functions and call each one.
  7. Check in.
  8. Go to the next method call and turn it into a variable assignment plus a call through the variable.
  9. Add the assignment to the array of functions and remove call after the loop.
  10. Check in.
  11. Repeat until done.
  12. Store the list of functions somewhere it can be read (like a class constant).
  13. Write a unit test that inspects the array and makes sure it contains the correct values.

You have finished this recipe. Back to Top.

Bonus: Decompose a Vase-shaped Legacy Method

Real-world codebases combine several of these design flaws in each method. Getting a real-world method under test requires solving several different problems. The key is to solve these problems one at a time and to pick a good order.

Every codebase combines them in different ways, but one of the most common in older code is the Vase-shaped method. We will explore the vase-shaped method as an example for how to combine these recipes on a real codebase.

Understanding the Target Method

A vase-shaped method typically has several untestable design elements:

  • It interacts with Poor Parameters.
  • It Calls Methods directly (often, but not always, on Poor Parameters).
  • It sequences many paragraphs as a Long Method. These are:
    • One or more paragraphs extracting data from Poor Parameters.
    • Zero or more paragraphs of guard clauses.
    • One or more paragraphs that decide what to do. These are usually nested conditionals and loops.
    • Within each scope where the code has determined what to do, one or more paragraphs of unrelated actions to take in that case.
    • Zero or more paragraphs of cleanup and data storing into Poor Parameters.

Developing a Strategy

Our goal is always the same: get the code under test while committing along the way. We want to minimize the amount of time we are away from main as that minimizes the cost of merges and distractions. So we look at the problems in order of their impact to testability.

  1. Solve anything that would make every test hard to write.
  2. Then start with the easiest repair first. Make it testable and then test it.
  3. Generally work on one type of design flaw at a time so that the recipes are easier to implement, code review, and merge to main.

The most global design flaw is usually either Poor Parameters or Micro-Methods. Therefore the first step is usually either Peel or Gather.

After that, it is usually easiest to test straight-line code – code without control flow. This code tends to exist inside the body of nested conditionals and loops. This is an example of Long Method inside a Long Method. So we will use the Template Method recipe in chunks.

The next step is to solve whatever makes the outermost method hard to test. Usually the outermost method will end up as a Template Method, but something may prevent us from going there directly. We address the obstacles and do Template Method.

Finally there may be some cleanup. We look at remaining untested code and address it.

The Recipe for a Vase-shaped Method

The below recipe applies this strategy to a vase-shaped method.

Phase 1: Fix the Global Obstacles
  1. Identify the gap between the end of the data extraction and the start of the guard clauses. Extract Method everything from here down.
    • Consider leaving the cleanup paragraphs in the outer method (the Peel). This works particularly well with paragraphs that are storing data back into Poor Parameters.
  2. Use the above recipe to Peel any remaining Poor Parameters on your new method, though you won’t be able to test it yet.
  3. Check in and merge to main.
Phase 2: Test the Easy Parts
  1. Extract Method one deeply-nested scope block where the code has decided what to do. Name it according to the state that the code has detected.
  2. That new method will now be a Long Method. Use the above recipe to turn it into a Template Method and test it.
  3. Check in and merge to main.
  4. Repeat steps 1-3 until you run out of cases.
Phase 3: Start the Outermost Long Method
  1. Identify the boundary between the remaining kinds of sections in the method. You will have, in order:
    • 0 or more guard clause paragraphs.
    • an action section, which detects states and then calls methods for each state.
    • 0 or more cleanup paragraphs.
  2. Use the Template Method recipe to extract and test the cleanup paragraphs.
  3. Check in and merge to main.
  4. Continue the Template Method recipe for the paragraphs in the action section. Each top-level control-flow statement is its own paragraph.
    • Name each according to the kinds of decisions it makes, data it examines, preconditions it assumes, or states it detects.
    • You will not be able to test these methods yet, because they each Call Method.
  5. Check in and merge to main.
Phase 3a: Solve the Obstacles — Method Calls

In Phase 2 we addressed Long Method by creating Method Calls, and now we need to resolve those Method Calls.

  1. Pick one method from the action section. Use the above recipe to convert it to use Events and test it. Each Event corresponds to a state the code has detected.
  2. Check in and merge to main.
  3. Repeat steps 1-2 until you have tested the entire action section.
Finish Phase 3: Finish the Template Method
  1. Finish off the Template Method recipe for the vase function by extracting the guard clause paragraphs.
  2. Check in and merge to main.
Phase 4: Do any Cleanup

Vase methods often don’t have any cleanup steps after the main Template Method extraction is done. Other real-world examples will. Just look through your code and make sure everything is tested.

You have finished this recipe. Back to Top.