technical skills - testing without mocks - legacy code patterns
src: Legacy code patterns
If you’d like to convert your existing code and tests to use Nullables, the patterns in this section will help you do so.
Work incrementally. You can mix Nullables with your current approach in the same codebase, and even in the same test, so there’s no need to convert everything at once. Similarly, focus your efforts on code where testing with Nullables will have noticeable benefit. Don’t waste time converting code that’s already easy to maintain, regardless of how it’s tested.
Descend the Ladder
Complex codebases have a lot of dependencies, and it isn’t feasible to improve all the tests at once. Instead, you’ll need to make progress incrementally. Therefore:
When converting a module or class to use Nullables, convert the code and its direct dependencies, but nothing more. Work your way down through the rest of the dependency tree gradually, when time allows.
Each module or class you convert will fall into one of three categories:
A. No Infrastructure Dependencies
If the code doesn’t have infrastructure anywhere in its dependency tree, it doesn’t need to use Nullables. It can be tested with the Logic Patterns instead.
B. Infrastructure Wrapper with Third-Party Dependencies
If the code is an Infrastructure Wrapper with direct third-party infrastructure dependencies, test it with Narrow Integration Tests, then make it Nullable with an Embedded Stub.
C. Everything Else
For everything else, you’ll make your code’s direct dependencies Nullable, then Fake It Once You Make It. To make the dependencies Nullable, apply one of the following options to each one:
- In most cases, the dependency will have a combination of logic and infrastructure in its dependency chain. Check the following bullet points. If none of them apply, make the dependency Nullable by introducing a Throwaway Stub.
- If the dependency is already Nullable, or if it doesn’t have any infrastructure dependencies, no changes are needed.
- If the dependency isn’t Nullable, but all of its dependencies are, make it Nullable by Faking It Once You Make It.
- If the dependency is a low-level Infrastructure Wrapper with third-party dependencies, make it Nullable by introducing an Embedded Stub.
- If the dependency is third-party infrastructure code, extract it into an Infrastructure Wrapper. Test the new Infrastructure Wrapper with Narrow Integration Tests and make it Nullable by introducing an Embedded Stub.
After you’ve updated the dependencies, Fake It Once You Make It. (If your code has a Throwaway Stub, replace it.) Replace Mocks with Nullables and add tests as needed.
When you’re done, the code you’re converting will be Nullable and tested. Its dependencies will be Nullable, but not tested. You can move on to other work. When you’re ready to convert another class or module, Descend the Ladder again. Over time, you’ll gradually convert the entire codebase.
Example
Imagine you have the dependency chain Router
→ LoginController
→ Auth0Client
→ HttpClient
, where HttpClient
is a low-level Infrastructure Wrapper. To convert Router
, you would follow these steps:
Router
’s direct dependency isLoginController
, which has a mix of logic and infrastructure in its dependency chain. MakeLoginController
Nullable with a Throwaway Stub.- Make
Router
Nullable with Fake It Once You Make It. - Convert
Router
’s tests with Replace Mocks with Nullables.
Later, if you wanted to convert Auth0Client
, you would follow these steps:
Auth0Client
’s direct dependency isHttpClient
, which is a low-level Infrastructure Wrapper. MakeHttpClient
Nullable by introducing an Embedded Stub.- Make
Auth0Client
Nullable with Fake It Once You Make It. - Convert
Auth0Client
’s tests with Replace Mocks with Nullables.
When you wanted to convert LoginController
, you would follow these steps:
LoginController
’s direct dependency isAuth0Client
, which was previously converted, so it’s already Nullable.LoginController
has a Throwaway Stub from whenRouter
was converted. Now thatAuth0Client
is Nullable, replace the stub with Fake It Once You Make It.- Convert
LoginController
’s tests with Replace Mocks with Nullables.
Finally, when you were ready to convert HttpClient
, you would follow these steps:
HttpClient
is a low-level Infrastructure Wrapper, and it was made Nullable whenAuth0Client
was converted, so it only needs to be tested.- Test
HttpClient
with Narrow Integration Tests.
Code that’s been converted can be refactored without breaking its tests. Once you’ve converted enough code, you can refactor it to use A-Frame Architecture or any other architecture you like.
Descend the Ladder is for code with large dependency trees. If the code you’re converting has a small dependency tree, Climb the Ladder instead.
Climb the Ladder
Descending the Ladder is a careful, methodical approach to improving existing code. However, it involves creating Throwaway Stubs, which is wasteful, and it takes a long time. Simple dependency trees don’t need so much care. Therefore:
When your dependency tree is simple, convert the entire tree at once. Start by graphing out a dependency tree for the code you want to convert, ignoring third-party dependencies. Then convert each node from the bottom of the tree up. (A post-order depth-first traversal). Apply one of the following options to each node:
- If the node is pure logic, with no infrastructure dependencies, make sure it has Easily Visible Behavior, then add tests as needed.
- If the node is already Nullable, convert its tests by Replacing Mocks with Nullables and adding tests as needed.
- If the node is an Infrastructure Wrapper and it uses third-party infrastructure code, make it Nullable by introducing an Embedded Stub. Test it with Narrow Integration Tests.
- If the node isn’t an Infrastructure Wrapper, but it does use third-party infrastructure code, extract the third-party code into an Infrastructure Wrapper. Apply the above option to the Infrastructure Wrapper, then apply the below option to the remaining code.
- If the node’s isn’t an Infrastructure Wrapper and doesn’t use third-party infrastructure code, Fake It Once You Make It. Convert its tests by Replacing Mocks with Nullables and adding tests as needed.
When you’re done, the entire dependency tree will be tested and Nullable. You can then refactor it toward A-Frame Architecture or any other architecture you like.
For example, imagine you have the dependency chain Router
→ LoginController
→ Auth0Client
→ HttpClient
, where HttpClient
is a low-level Infrastructure Wrapper. To convert Router
, you would follow these steps:
HttpClient
is a low-level Infrastructure Wrapper. Make it Nullable by introducing an Embedded Stub.- Test
HttpClient
with Narrow Integration Tests. - Make
Auth0Client
Nullable with Fake It Once You Make It. - Convert
Auth0Client
’s tests with Replace Mocks with Nullables. - Make
LoginController
Nullable with Fake It Once You Make It. - Convert
LoginController
’s tests with Replace Mocks with Nullables. - Make
Router
Nullable with Fake It Once You Make It. - Convert
Router
’s tests with Replace Mocks with Nullables.
Climb the Ladder works best when you have a small dependency tree. If you have a large dependency tree, Descend the Ladder instead.
Replace Mocks with Nullables
Existing code is often tested with mocks, spies, and other test doubles. Some of those tests will get in your way. They might be hard to understand and maintain, or they might make refactoring difficult. Therefore:
When an existing test gets in your way, use Nullables in place of the existing test doubles. Depending on the quality of the existing tests, it might be easiest to inline any setup blocks or helper methods prior to starting. Then apply the following options to each mock, spy, or other test double in each test you want to convert:
- Start by replacing the test double with a Nulled version of the real dependency.
- If the test double is configured to return specific values, replace the configuration with Configurable Responses.
- If the test double is configured to emit events, replace the configuration with Behavior Simulation.
- If the test checks how a test double is called, replace its assertions with Output Tracking. Convert these test doubles last, after test doubles with only configuration have been replaced.
For example, here’s a controller for a web page. When the user posts to the page, it uses the rot13Client
infrastructure wrapper to call a web service, then renders the result.
The following test uses spies to check that the above code calls the web service. It’s an interaction-based test that checks whether the dependency’s methods are called correctly.
This test can be converted one spy at a time. First, we replace the HttpRequest
spy with a Configurable Response.
Because Nullables can coexist with test doubles, the tests still pass after this change is made. Next, we replace the WwwConfig
spy:
The tests continue to pass. Finally, we replace the Rot13Client
spy:
Here’s a side-by-side comparison of the two tests.
To make a dependency Nullable, either Descend the Ladder or Climb the Ladder.
Throwaway Stub
Making a dependency Nullable requires making all of its infrastructure dependencies Nullable, too. Sometimes, that’s too much work to tackle all at once. Therefore:
In the code you’re making Nullable, create Embedded Stubs for any dependencies you don’t want to make Nullable. This will break the chain of Overlapping Sociable Tests, leaving you vulnerable to behavioral changes in the dependencies, so throw away the stub and replace it with Fake It Once You Make It as soon as the dependency is Nullable.
To avoid writing throwaway stubs, Climb the Ladder.