HowToTestAClassThatUseStaticAPI
Architecture Clean Code Testability Testing

How to unit test a class that uses static API

Unit tests validate code in isolation, but if it uses statics you cannot mock this dependency, therefore a subject under test cannot be properly isolated and unit tested. This trick makes the code testable, as well as introduces an important principle that improves your architecture by making it cleaner and more flexible.


I won’t discuss here why static classes are good or bad, but in some cases, we have to work with statics when using third-party plugins, for example, ads providers.
Actually static API for 3rd party is good in my opinion. It leaves room for different approaches: global access anywhere in the code for fast prototypes and small games, or using the alternative that I suggest in this article with proper DI, separation of concerns, and architectural flexibility.

Prerequisites

If your workflow is mature enough that you decided to write unit tests, then I assume you are already applying the DI principle (dependency inversion) to some extent, either you have a poor man’s DI (dependency injection) or use a DI container (Zenject, VContainer, Reflex, etc). Because otherwise I hardly find a class unit testable if it uses a service locator or creates its dependency inside itself.

Static class wrapper

Well, given prerequisites, to unit test a class in complete isolation, we need all its dependencies to be mocked and passed outside.
So the trick is simple as it is, we need to define an interface for the static class and implement a wrapper that will just pass calls to the static one.
So imagine we got a third-party simple rewarded ads provider with methods ShowAd(), IsLoaded(), and events OnCompleted and OnInterrupted:

public static class AwesomeAdService
{
    public static event Action OnCompleted;
    public static event Action OnInterrupted;

    public static bool IsLoaded()
    {
        //...
    }

    public static void ShowRewardedAd()
    {
        //...
    }
}

And a state (imagine using the FSM pattern) that handles level completion:

public class LevelCompletedState
{
    public void OnEnter()
    {
        AwesomeAdService.OnCompleted += OnAdCompleted;
        AwesomeAdService.OnInterrupted += OnAdInterrupted;
        
        if(AwesomeAdService.IsLoaded)
        {
            AwesomeAdService.ShowRewardedAd();
        }
    }

    private void OnAdInterrupted()
    {
        //go to the main menu
    }

    private void OnAdCompleted()
    {
        //request reward from the backend
    }

    ...
}

The implementation could be different, some SDKs provide an interface to handle callbacks, it doesn’t matter at this point, the principle still applies. We need to create an interface:

public interface IAdService
{
   event Action OnCompleted;
   event Action OnInterrupted;
   bool IsLoaded();
   void ShowRewardedAd();
}

And the following implementation:

public class AdService : IAdService
{
    private event Action OnCompleted;
    private event Action OnInterrupted;

    event Action IAdService.OnCompleted
    {
        add => OnCompleted += value;
        remove => OnCompleted -= value;
    }

    event Action IAdService.OnInterrupted
    {
        add => OnInterrupted += value;
        remove => OnInterrupted -= value;
    }

    public bool IsLoaded()
    {
        return AwesomeAdService.IsLoaded();    
    }

    public void ShowRewardedAd()
    {
        AwesomeAdService.ShowRewardedAd();
    }
}

Now instead of calling the plugin’s API directly, we inject our interface implementation:

public class LevelCompletedState
{
    private readonly IAdService _adService;

    public LevelCompletedState(IAdService adService)
    {
        _adService = adService;
        _adService.OnCompleted += OnAdCompleted;
        _adService.OnInterrupted += OnAdInterrupted;
    }

    public void OnEnter()
    {
        if(_adService.IsLoaded)
        {
            _adService.ShowRewardedAd();
        }
    }

    private void OnAdInterrupted()
    {
        //go to the main menu
    }

    private void OnAdCompleted()
    {
        //request reward from the backend
    }

    ...
}

This allows us to easily test our class, that is invoking a rewarded video ad. We just pass the mock implementation of the ads provider, which is set up to return the result we need, in our case to invoke the completed or interrupted event. And that’s it. Now our class can be covered with unit tests, so we can write some, for example, to make sure that if the video is completed we make a call to the server to get a reward. With that logic being covered by automated tests, we can refactor our class freely and be sure that after refactoring all the business logic is left intact.
Here is an example of how LevelCompletedState can be tested:

    [TestFixture]
    public class LevelCompletedStateTests
    {
        private Mock<AdService> _adService;
        private LevelCompletedState _levelCompletedState;

        [SetUp]
        public void Setup()
        {
            _adService = new Mock<AdService>();
            _levelCompletedState = new LevelCompletedState(_adService.Object);
        }

        [TearDown]
        public void TearDown()
        {
            _adService = null;
            _levelCompletedState = null;
        }
        
        [Test]
        public void OnEnter_AdIsLoaded_ShowAd()
        {
            _adService.Setup(m => m.IsLoaded()).Returns(true);
            _levelCompletedState.OnEnter();
            _adService.Verify(m => m.ShowRewardedAd(), Times.Once());
        }

        [Test]
        public void OnEnter_AdIsLoaded_StateEnds()
        {
            _adService.Setup(m => m.IsLoaded()).Returns(false);
            _levelCompletedState.OnEnter();
            // assert that the state ends
        }
    }

In order to compile it we need to add Moq library to our project: firstly add "nuget.moq": "1.0.0" to the manifest.json, then add an assembly reference in the test assembly to Moq.dll.

{
  "dependencies": {
    "com.unity.2d.animation": "7.0.1",
    "com.unity.2d.pixel-perfect": "5.0.1",
    "com.unity.2d.sprite": "1.0.0",
    "com.unity.2d.spriteshape": "7.0.2",
    "com.unity.feature.vr": "1.0.0",
    "com.unity.ide.rider": "3.0.7",
    "com.unity.performance.profile-analyzer": "1.1.1",
    "com.unity.test-framework": "1.1.30",
    "com.unity.test-framework.performance": "2.8.0-preview",
    "com.unity.textmeshpro": "3.0.6",
    "com.unity.timeline": "1.6.2",
    "com.unity.ugui": "1.0.0",
    "com.unity.modules.animation": "1.0.0",
    "com.unity.modules.imageconversion": "1.0.0",
    "com.unity.modules.imgui": "1.0.0",
    "com.unity.modules.jsonserialize": "1.0.0",
    "com.unity.modules.ui": "1.0.0",
    "com.unity.modules.uielements": "1.0.0",
    "com.unity.modules.umbra": "1.0.0",
    "com.unity.modules.unitywebrequest": "1.0.0",
    "com.unity.modules.unitywebrequestassetbundle": "1.0.0",
    "com.unity.modules.unitywebrequestaudio": "1.0.0",
    "com.unity.modules.unitywebrequesttexture": "1.0.0",
    "com.unity.modules.unitywebrequestwww": "1.0.0",
    "com.unity.modules.xr": "1.0.0",
    "nuget.moq": "1.0.0"
  }
}


Other benefits of this change

Flexibility

Testability is not the only benefit we get with this approach. Now the code is more flexible as we can change behavior by binding a different implementation. For example, if for some reason a third-party plugin does not provide any implementation for running in the editor mode, we can write our own implementation for the defined interface. In the case of the ad provider, we can call it a DisabledAdService and it will just always call the needed event right after the call to show an ad.

Boundaries

The created interface is the boundary between our code and the third party. It is one of the principles suggested in the book “Clean Code” by Robert C Martin. It makes the code cleaner and more flexible too. And this is a perfect example of applying the principle.

Usually, we have more than one place where we want to show ads. For example, we can have rewarded videos after the loss in order to continue or to get some bonus currency in the main menu and in the shop. Then we can have interstitial videos between levels, some banner ads, and so on.

It means we would have a ton of third-party API calls here and there. And at one point a project manager comes to us and asks to make a technical release with another ad provider and open it only to 20% of the user base to perform a test. If you only have static calls everywhere in your code base then you’re going to spend pretty much time replacing them all, even if you will use an IDE like Rider or Visual Studio that greatly helps to find and replace all calls to the old API, you still need to be careful to match everything correctly in all places. Also after the test probably you need to revert everything back or not merge the test release branch to your master branch at all.

And if you have the boundary between your code and a third party, just like in the example with a simple wrapper and your own interface, then replacing the ad provider is as easy as providing and binding a new implementation. There are also fewer places where errors could appear, as you would be changing only one class and adding a new one.

Of course, names of methods or events can be different, but mostly if we speak about interchangeable third parties, they provide similar functionality and you should be able to cover a new plugin with your interface and maybe some additional little logic. Anyway, in the end, you should be able to keep both implementations and use one or another just by changing one line in bindings. Or you can go even further and delay plugin binding until a user logs in and gets a flag in a login response, that tells which plugin implementation to bind and use (obviously it is an exaggeration because ad libs are usually pretty heavy and you shouldn’t ship both in the build in most cases, but the concept still could be applied in various situations).


Conclusion

This approach is useful for the following reasons:

• improves flexibility,
• makes the code cleaner,
• increases testability.

If you are already following the DI principle, then I see no downsides to applying it to all third parties you use, including stuff as common as BCL types, e.g. DateTime. There is nothing worse than unstable tests that fail sometimes due to the usage of real-time and date in unit or integration tests. A custom IDateTimeProvider interface with a wrapper around DateTime implementation helps to avoid such issues.



Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.