Solving 80% of legacy code testing blockers.
Getting beyond extract and test.
Enjoy an 8m whiskey moment with Arlo talking about testing the worst kind of untestable code: code that does many things at once. Then read on for a specific list of refactorings for common problems.
Let’s get the easy one out of the way. You want to test code that is in the middle of method, but actively don’t want to involve the rest of the method.
Extract and test.
Done. You probably knew this. The real point is that it’s ok to make the code testable in order to test it.
Now, let’s get to the cool stuff!
Oh wait, before we start, if you get excited and want to dive into a discussion about one of these, join our Code by Refactoring slack channel! Or just register for our next Ask me Anything!
Shared Data in Fields or Globals
This is the root of many evils. As you read through this, we’ll be coming back to shared data many times.
So, the context is that we have different parts of the code access the same variable, making it hard to test interactions with each other.
The most common solution is to collect all modifications of that variable into one easily testable entity. All other methods will be read-only, eliminating write-interactions between parts.
Refactorings to Use
Use extract method to isolate variable writes.
Convert function calls to events to decrease coupling between writer methods and calling methods.
Use move method or convert to instance method to gather the many write methods into a single class.
Use 1) extract method, 2) convert to template method, 3) insert guard clause, and 4) convert to chain of responsibility in order to separate the sequence of writes, reads, and conditionals. This separates concerns, allowing each testable component to do just one thing without interactions.
Ordering dependencies are only a problem because there multiple writes to shared data. Fix the shared data (see above) and the problem goes away.
Parallelism makes testing difficult in three ways. First, it causes unpredictable sequencing. Second, you often needs to ensure things are happening in parallel. Third, you need to test that when an error happens in one parallel process, the other ones do the right thing.
First Design | to fix unpredictable sequencing, fix your ordering dependencies, which means fix your shared data. See shared data. Again.
Second Design | to test that things are happening in parallel, refactor to use higher level parallelism constructs, e.g., don’t use threads, mutexes, or processes. Instead, use promises, co-routines, and an event loop.
Third Design | to test that a parallel process responds correctly to an error in a different parallel process, we’re going to convert parallel processes to co-routines and error conditions to asynchronous inputs.
Refactorings to Use (for the third design change)
Insert a top level event loop unless your language already has one, .e.g., Async/Await or JS event loop.
Use either extract method or convert to await in order to separate code that happens before and after a blocking wait.
Use convert return to event dispatch so that code can return multiple times.
Use convert except to object so that errors can be passed around as normal data.
Use convert exception throwing to event dispatch.
Route asynchronous returns to the message loops to the parallel co-routines.
Add handlers for the asynchronous returns that will be called only by the top level message loop.
The persistent state is complex so it’s difficult to set initial conditions for your test. Also, it also accessed everywhere in your code, acting as a shared global. Do not pass go; do not collect $200. Fix your shared data (see top)!
Three techniques you’ll use many times for this overall design change include:
make code depend on a smaller part of the data.
use a loader to simplify the complex data and pass it into the code.
make it possible to construct valid subsets of the complex data.
Refactorings to Use
Use convert to static method so that all information comes into the method via it’s argument list.
Eliminate global and static access from within your static methods for the same reason.
Use introduce parameter object to collect inputs into useful domain objects.
Use extract method to create a loader that converts your complex state into your new domain objects.
These are hard because they are slow, parallel, and often have persistent state.
See Persistent State.
Extra Complexity: Incidental
Sometimes the problem isn’t so complex as the solution that has been selected to solve the problem. Usually this happens because the solution grew over time and is general enough to handle cases that arose and disappeared.
Refactor to simplify. Simple.
Refactorings to Use
See Martin Fowler’s catalog of refactorings for descriptions of these basic refactorings.
Convert function call to event.
Rename, rename, rename.
Extra Complexity: Slicing Simple Out
Your code needs to access and use a simple part of a complex domain. Unfortunately the implementation of that complex domain is complex enough to handle the full domain. You really wish it was simpler!
Use ports and adaptors architecture. Arlo has already written a ports and adaptors blog post in much more detail the design and refactorings necessary to do this.
Unchangeable 3rd Party Code
This is exactly the same as slicing simple from a complex domain except that it’s also usually remote. As such, combine these two:
See Remote Components
In other words, Parallelism and Persistent State. Which also means you’ll probably find yourself back in the root of all evil: shared data.
See Extra Complexity: Slicing Simple Out.
Clocks & Time
You are invariably waiting for specifics moments in time or time to pass, which makes the tests slow or constrained.
Encapsulate everything time related into an abstract clock and provide a stopped clock for use in testing.
Refactorings to Use
Use extract method and convert to instance method repeatedly to introduce the clock in your existing code.
Use extract base class to abstract the clock.
Implement the stopped clock.
Use introduce factory to incapsulate creation of clocks.
Time Zones & Localization
Your computer runs in one locale and many other locales have funny behaviors. As such, you have to test in a set of the unusual locales.
Encapsulate location specific operations and test them independently.
Refactorings to Use
This the same thing as the Clocks & Time issue, except that you are extracting slightly smarter components. So you need to test that component as well as the code that uses it.
We want to test that the user actions will be routed correctly in the application and that the state of the application will be routed back out correctly to the user. We say it this way because we address presentation & layout next. The challenge is automating the UI, so we end up with a lot of manual tests or a slow automation driver.
Once again, we want to use ports and adaptors, just as we did with slicing simple out of extra complex code. However, there we focused on using the adaptor to simplify the real object. Here, we focus on using a simulator to drive the adaptor or port.
Also use MVVM to separate the application from the presentation. Make sure your view models are simple data transfer objects without behavior.
Presentation & Layout
Here there could be a few different kinds of problems.
Your presentation is tightly coupled to your UX testing.
Intent can be hard to see because of golden master testing.
Unimportant layout changes can cause all the tests to fail even though the product is still valid.
The first step is to separate UX from presentation, thus separating the tests. Once that’s done, all you have left to verify is presentation & layout. Now you want to replace golden master testing with explicit assertions that use UI measurement.
This does not have refactorings so much as writing new tests with new custom assertions.
Techie Talk with Arlo
Join Arlo’s Whiskey Hour to “Ask Me Anything” on legacy code. Capacity is limited, so register soon!
Business Value of Refactoring for your PO
Does your PO reject refactoring? Ask them to register for our June webinar that addresses the business value of refactoring. Both you and your PO will be grateful!
Give them a 50% discount code: “newsletter”.
Attending CRAFT 2022?
If so, check out talk by Marian Hartman, Arlo’s business partner and instructional designer! She’ll be presenting Using Empathy to Make Tests Easy and Code Safe.