Making untestable code testable again!

Sunday, January 31, 2021

Introduction

At some point in your developer career, you might face the unpleasant task to write code tests for an already implemented feature of your app which was never tested before. At first, you get started right away but then you might realize that the business logic and the framework code are tightly coupled. At that point, writing tests look like a painful task. So what are you doing?

There are different ways to tackle those issues. In this first part, I want to look at one possible option to make untestable code testable again. Following points will be discussed:

  • What does untestable code mean
  • Why do we need to minimize untestable code
  • How to extract untestable components

What does untestable code mean?

In essence, you cannot write automated code tests for a function of your app because it is tightly coupled to components or libraries which are out of your control. This can be GUI components, asynchronous methods or a 3rd-party library.

Here is a simplified example of untestable code in .NET:

GIST: https://gist.github.com/adrinamin/1d1e7fb4349ad65b9fd8b9651cb54294.js

I'm using code-behind of a WPF app to calculate the cross sum of the input. When trying to write a unit test for the CalculateCrossSum method you would usually mock you dependencies with some mocking framework like moq or RhinoMocks. Unfortunately, it is not possible to mock MessageBox.Show for a Unit Test. Due to tight coupling between business logic and UI components, there is no way to implement an automated Unit Test for the CalculateCrossSum method.

Why do we need to minimize untestable code?

As shown above, untestable code reduces the overall test coverage of your application. Of course, you don't write tests to achieve 100% test coverage. Your tests need to be useful, and they are supposed to test the functionality of your app. The less source code is tested, the higher the chance of introducing new bugs into your software. The confidence in your code reduces, and it is more difficult to refactor the source code in the long-run. Ultimately the code quality reduces over time, and implementing new features gets almost impossible.

Staying with the WPF example above, you want to make sure that the current functionality of our app is not changed when refactoring the code or implementing new features. If we do not have automated Unit Tests to verify the basic functionality of our app, how can you make sure that everything works as expected? Of course, the above example is very simplified, but this is also the case when talking about big enterprise application. In the end, the last option is verifying business-rules manually. This can be very costly, and it takes a lot of time.

Further, I want to show extraction of logic from hard-to-test components into testable components.

How to extract an untestable component?

Below is shown a basic C# example of a service that contains a method called CreateSuperHeroAsync. It is an asynchronous method, and the business logic is kept very simple for demonstration purposes.

GIST: https://gist.github.com/adrinamin/4725f3a8b740a6252f39e81c90a96b85.js

Now we want to create a Unit Test for the CreateSuperHeroAsync method. One major issue here is the unpredictability due to the asynchronous method call and the static Thread.Sleep call. We also want to keep the System.IO.File.AppendAllLinesAsync callout of our business logic. How to test the business logic without losing the asynchronous functionality of the overall method?

Using the Humble Object pattern

Gerard Meszaros talked in his book "xUnit Test Patterns" about the Humble Object pattern. It is part of the Design-for-Testability Patterns, and it is similar to the Adapter pattern. Even though the purpose is different. The Adapter pattern converts the interface of a class into another interface that a client expects.

On the other side, the Humble Object pattern, extracts the logic out of the hard-to-test components into a testable module. You implement a service interface that contains methods that expose the business logic of untestable code. As a result, you get a thin adapter layer that consists of almost no code.

Extracting the business logic into a separate component

We refactored the SomeService class and extracted the logic from the CreateSuperHeroAsync method into a separate C# class. Thus we get a True Humble Object. We inject the interface of this class into SomeService and call the method CreateHeroes inside the CreateSuperHeroAsync class.

GIST: https://gist.github.com/adrinamin/6f75fc2b6ee2bb1c726940769e4f8312.js

GIST: https://gist.github.com/adrinamin/db3be6889ea7cf48a91aa5b820e753a6.js

GIST: https://gist.github.com/adrinamin/baf23ef8f0f28ab42c8b7faf356d7ef8.js

So what is the advantage? We are now able to test our business logic synchronously and verify the Humble Object if we need to. Thus it makes the Humble Object pattern very powerful when splitting the hard-to-test behaviors and the testable behaviors.

Conclusion

The Humble Object pattern gives us the change to divide testable and untestable code. With this pattern, you can even test the hard-to-test part without worrying about the "moving parts" inside your system undertest. To recap the key takeaways:

  • There are many parts inside your application that are not unit-testable.
  • The more your app is tested, the more confident you are when making changes.
  • The Humble Object pattern splits hard-to-test code and testable code.
  • You can test the business logic synchronously.
  • You get an extra thin adapter layer without any business logic that you can verify as well.

In part two of this series, we take a closer look at decoupling your code from the UI by inspecting different architectural patterns. So stay tuned, and in the meantime, let me know what you think about the Humble Object pattern.

Let us know what you
think about this article