How to Add Tests for Existing Code with Methods That Aren’t Public
Tags: csharp, testing, dotnet, tutorial
Published: 2023-09-06 13:00:00

Writing tests alongside your code can often be helpful. It can help you think of edge cases where things might go wrong, provide a safety net when refactoring, and help you optimise your architecture for low coupling and reuse. Adding tests is relatively easy when you have enough time and are working on a greenfield project (i.e. you don’t have to worry about existing code). However, we don’t always have this luxury. The code we work with may have been written under different circumstances and/or without testing in mind.

In this article we’ll look at an example of some (possibly legacy) code you might typically come across. And we’ll also explore a few ways that we might go about to add tests to it.

## A Private Affair

Let’s imagine we’re working on an existing codebase. It contains an (admittedly unimaginatively named) service that’s responsible for doing some important and complex calculations. There’s one entry point (`RunCalculation`); calling it will run two sub-calculations. Each sub-calculation will compute its own result and then write it to a service-wide variable (`_mainResult`). Finally, `RunCalculation` returns the value of `_mainResult`.

As a sidenote, this approach isn’t thread-safe. One variable is shared across the service instance; usage from multiple threads could result in one thread’s sub-calculations overwriting another’s, potentially making the overall result unreliable.

``` csharp
public class ComplexCalculationService
{
    private double _mainResult;

    public double RunCalculation(double input)
    {
        _mainResult = input;
        SubCalculation1();
        SubCalculation2();
        return _mainResult;
    }

    private void SubCalculation1()
    {
        var subResult1 = _mainResult;
        // More calculations to transform subResult1...

        _mainResult = subResult1;

    }

    private void SubCalculation2()
    {
        var subResult2 = _mainResult;
        // More calculations to transform subResult2...

        _mainResult = subResult2;
    }
}
```

The service is an important part of the overall product, and we want to know as soon as possible if its results become inaccurate. The most obvious and readily available action is to write tests comparing the output of RunCalculation against values known to be correct. This is indeed useful. But it’s difficult to know where the miscalculation occurs if they fail, especially if the two transformations are long and complex. It would be helpful if we could narrow things down - if we could test each sub-calculation.

## A More Functional Approach

It’s easier to write tests for pure functions. Let’s first make some minor changes to our service so it no longer relies on state variables. Instead, the sub-calculations will use variables we pass to them and return their results. These can then be chained together to calculate the final result.

In addition to simplifying our tests, these changes also make our service thread-safe. As it’s no longer using shared variables, a single instance of `ComplexCalculationService` could potentially be used across multiple threads safely (and without synchronisation locks) if required.

``` csharp
public class ComplexCalculationService
{
    public double RunCalculation(double input)
    {
        var result1 = SubCalculation1(input);
        var finalResult = SubCalculation2(result1);
        return finalResult;
    }

    private double SubCalculation1(double input)
    {
        var subResult1 = input;
        // More calculations to transform subResult1...

        return subResult1;
    }

    private double SubCalculation2(double input)
    {
        var subResult2 = input;
        // More calculations to transform subResult2...

        return subResult2;
    }
}
```

## Increasing Code Visibility

We’re almost ready to write tests for the sub-calculations. However, we still can’t access them externally as they’re private – we need to change their visibility modifiers to be more permissive. But what do we change them to?

### Going Public

The most obvious option is `public`. But let’s pause for a moment to think about what this means: anything will be able to access the methods. _Anything_. While this is good from a testing perspective, it also invites other code modules to access the sub-calculations too. This isn’t necessarily a bad thing, especially if there’s a valid case for code reuse. But that wasn’t the original design/intention. And there’s a possibility that with this reuse, our (entire) service becomes a dependency for another module, including parts that may be unrelated. This could lead to two unintended consequences:

* Creating unnecessary coupling between modules.

* Making it easier to create circular dependencies.

Instead of directly changing the visibilities from `private` to `public`, a good strategy would be to refactor the sub-calculations into public methods on a separate service. This separation of concerns lessens the risk of these consequences. It also follows the Single Responsibility Principle (the S of SOLID) as each module would have its own dedicated purpose.

### Staying Protected
The next option is `protected`. While this doesn’t directly make our sub-calculations accessible, it lets us create a subclass of `ComplexCalculationService` with ways to expose the inherited methods we want for testing. This can work and might be advantageous if we have other services in our solution that should also inherit the protected members. However, in such cases we’re arguably better off refactoring the sub-calculations into a separate service as previously mentioned; doing so would also make testing easier as we wouldn’t have to maintain a separate version of the service just for testing.

### Being Internally Restricted
The last option we’ll consider is `internal`. Members marked as `internal` are usually only accessible to other classes within the same project. However, we can use the [InternalsVisibleTo](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.internalsvisibletoattribute) attribute to make them also accessible in any external modules we choose. Applying it is simple, but the process differs slightly depending on your project [format](https://learn.microsoft.com/en-us/nuget/resources/check-project-format).

Non-SDK-style projects (the default for .NET Framework) will have an `AssemblyInfo.cs` file in the `Properties` node of the project. We can add the following line to it, replacing `Project.Name` with the name of the assembly containing our tests:

```
[assembly: InternalsVisibleTo("Project.Name")]
```

For SDK-style projects, we need to add the following directly to the `.csproj` file. Again, we should replace `Project.Name` with an appropriate value.

```
<ItemGroup>
    <InternalsVisibleTo Include="Project.Name" />
</ItemGroup>
```

### Making a Decision

With three choices available, which is the best one?

As with many things in programming, the answer is _it depends_. I’ve worked on a few codebases and have found myself usually opting for either:

* Refactoring the code into a separate service, or

* Making members `Internal`.

Creating a new service is usually the cleanest option. It’s also a chance to review a module’s dependencies, reducing them to only what’s necessary. Another advantage is that as a separate module, it can be substituted or mocked in tests where appropriate.

However, if time is a limiting factor (or if refactoring isn’t cost-effective) I’ll mark the methods to be tested as `internal`. While more visible than originally intended, they’re still hidden to external modules (except those explicitly named). The compromise of being visible in a limited way and thoroughly tested feels safer than the alternative: being correctly hidden but potentially faulty.

## Summary

Tests can be helpful, but we sometimes need to work with codebases that were created without. In these cases, one option is to add them before making significant changes to act as a safety net. Writing tests for existing/legacy code can be difficult if it wasn’t written with testing in mind. However, there are a few tips to make things a bit simpler.

Where possible, it will be easier if you can convert methods into functions that aren’t reliant on variables on the state. You then need to make them accessible for testing. Where appropriate, the best choice might be to refactor the methods into their own service (or services). If this isn’t possible (or suitable), you might want to change some private methods to have greater visibility.

`public`, `protected`, and `internal` are all valid options. `internal` members are usually only accessible to other members of the same project, but you can cheat – the `InternalsVisibleTo` attribute will let you make them visible to your test projects too.

---

Thanks for reading!

This article is from my [newsletter](https://webdeveloperdiary.substack.com/?sd=pf). If you found it useful, please consider subscribing. You’ll get more articles like this delivered straight to your inbox (once per week), plus bonus developer tips too!