DEV Community

Cover image for 4 ways to create Unit Tests without Interfaces in C#
Davide Bellone
Davide Bellone

Posted on • Originally published at code4it.dev on

4 ways to create Unit Tests without Interfaces in C#

One of the most common traits of C# developers is the excessive usage of interfaces.

For every non-DTO class we define, we usually also create the related interface. Most of the time, we don't need it because we have multiple implementations of an interface. Instead, we say that we need an interface to enable mocking.

That's true; it's pretty straightforward to mock an interface: lots of libraries, like Moq and NSubstitute, allow you to create mocks and pass them to the class under test. What if there were another way?

In this article, we will learn how to have complete control over a dependency while having the concrete class, and not the related interface, injected in the constructor.

C# devs always add interfaces, just in case

If you're a developer like me, you've been taught something like this:

One of the SOLID principles is Dependency Inversion; to achieve it, you need Dependency Injection. The best way to do that is by creating an interface, injecting it in the consumer's constructor, and then mapping the interface and the concrete class.

Sometimes, somebody explains that we don't need interfaces to achieve Dependency Injection. However, there are generally two arguments proposed by those who keep using interfaces everywhere: the "in case I need to change the database" argument and, even more often, the "without interfaces, I cannot create mocks".

Are we sure?

The "Just in case I need to change the database" argument

One phrase that I often hear is:

Injecting interfaces allows me to change the concrete implementation of a class without worrying about the caller. You know, just in case I had to change the database engine...

Yes, that's totally right - using interfaces, you can change the internal implementation in a bat of an eye.

Let's be honest: in all your career, how many times have you changed the underlying database? In my whole career, it happened just once: we tried to build a solution using Gremlin for CosmosDB, but it turned out to be too expensive - so we switched to a simpler MongoDB.

But, all in all, it wasn't only thanks to the interfaces that we managed to switch easily; it was because we strictly separated the classes and did not leak the models related to Gremlin into the core code. We structured the code with a sort of Hexagonal Architecture, way before this term became a trend in the tech community.

Still, interfaces can be helpful, especially when dealing with multiple implementations of the same methods or when you want to wrap your head around the methods, inputs, and outputs exposed by a module.

The "I need to mock" argument

Another one I like is this:

Interfaces are necessary for mocking dependencies! Otherwise, how can I create Unit Tests?

Well, I used to agree with this argument. I was used to mocking interfaces by using libraries like Moq and defining the behaviour of the dependency using the SetUp method.

It's still a valid way, but my point here is that that's not the only one!

One of the simplest trick is to mark your classes as abstract. But... this means you'll end up with every single class marked as abstract. Not the best idea.

We have other tools in our belt!

A realistic example: Dependency Injection without interfaces

Let's start with a real-ish example.

We have a NumbersRepository that just exposes one method: GetNumbers().

public class NumbersRepository
{
    private readonly int[] _allNumbers;

    public NumbersRepository()
    {
        _allNumbers = Enumerable.Range(0, int.MaxValue).ToArray();
    }

    public IEnumerable<int> GetNumbers() => Random.Shared.GetItems(_allNumbers, 50);
}
Enter fullscreen mode Exit fullscreen mode

Generally, one would be tempted to add an interface with the same name as the class, INumbersRepository, and include the GetNumbers method in the interface definition.

We are not going to do that - the interface is not necessary, so why clutter the code with something like that?

Now, for the consumer. We have a simple NumbersSearchService that accepts, via Dependency Injection, an instance of NumbersRepository (yes, the concrete class!) and uses it to perform a simple search:

public class NumbersSearchService
{
    private readonly NumbersRepository _repository;

    public NumbersSearchService(NumbersRepository repository)
    {
        _repository = repository;
    }

    public bool Contains(int number)
    {
        var numbers = _repository.GetNumbers();
        return numbers.Contains(number);
    }
}
Enter fullscreen mode Exit fullscreen mode

To add these classes to your ASP.NET project, you can add them in the DI definition like this:

builder.Services.AddSingleton<NumbersRepository>();
builder.Services.AddSingleton<NumbersSearchService>();
Enter fullscreen mode Exit fullscreen mode

Without adding any interface.

Now, how can we test this class without using the interface?

Way 1: Use the "virtual" keyword in the dependency to create stubs

We can create a subclass of the dependency, even if it is a concrete class, by overriding just some of its functionalities.

For example, we can choose to mark the GetNumbers method in the NumbersRepository class as virtual, making it easily overridable from a subclass.

public class NumbersRepository
{
    private readonly int[] _allNumbers;

    public NumbersRepository()
    {
        _allNumbers = Enumerable.Range(0, 100).ToArray();
    }

-    public IEnumerable<int> GetNumbers() => Random.Shared.GetItems(_allNumbers, 50);
+    public virtual IEnumerable<int> GetNumbers() => Random.Shared.GetItems(_allNumbers, 50);
}
Enter fullscreen mode Exit fullscreen mode

Yes: we can mark a method as virtual even if the class is concrete!

Now, in our Unit Tests, we can create a subtype of NumbersRepository to have complete control of the GetNumbers method:

internal class StubNumberRepo : NumbersRepository
{
    private IEnumerable<int> _numbers;

    public void SetNumbers(params int[] numbers) => _numbers = numbers;

    public override IEnumerable<int> GetNumbers() => _numbers;
}
Enter fullscreen mode Exit fullscreen mode

We have overridden the GetNumbers method, but to do so, we had to include a new method, SetNumbers, to define the expected result of the former method.

We then can use it in our tests like this:

[Test]
public void Should_WorkWithStubRepo()
{
    // Arrange
    var repository = new StubNumberRepo();
    repository.SetNumbers(1, 2, 3);
    var service = new NumbersSearchService(repository);

    // Act
    var result = service.Contains(3);

    // Assert
    Assert.That(result, Is.True);
}
Enter fullscreen mode Exit fullscreen mode

You now have the full control over the subclass. But this approach comes with a problem: if you have multiple methods marked as virtual, and you are going to use all of them in your test classes, then you will need to override every single method (to have control over them) and work out how to decide whether to use the concrete method or the stub implementation.

For example, we can update the StubNumberRepo to let the consumer choose if we need the dummy values or the base implementation:

internal class StubNumberRepo : NumbersRepository
{
    private IEnumerable<int> _numbers;
    private bool _useStubNumbers;

    public void SetNumbers(params int[] numbers)
    {
        _numbers = numbers;
        _useStubNumbers = true;
    }

    public override IEnumerable<int> GetNumbers()
    {
        if (_useStubNumbers)
            return _numbers;
        return base.GetNumbers();
    }
}
Enter fullscreen mode Exit fullscreen mode

With this approach, by default, we use the concrete implementation of NumbersRepository because _useStubNumbers is false. If we call the SetNumbers method, we also specify that we don't want to use the original implementation.

Way 2: Use the virtual keyword in the service to avoid calling the dependency

Similar to the previous approach, we can mark some methods of the caller as virtual to allow us to change parts of our class while keeping everything else as it was.

To achieve it, we have to refactor a little our Service class:

public class NumbersSearchService
{
    private readonly NumbersRepository _repository;

    public NumbersSearchService(NumbersRepository repository)
    {
        _repository = repository;
    }

    public bool Contains(int number)
    {
-       var numbers = _repository.GetNumbers();
+       var numbers = GetNumbers();
        return numbers.Contains(number);
    }

+    public virtual IEnumerable<int> GetNumbers() => _repository.GetNumbers();
}
Enter fullscreen mode Exit fullscreen mode

The key is that we moved the calls to the external references to a separate method, marking it as virtual.

This way, we can create a stub class of the Service itself without the need to stub its dependencies:

internal class StubNumberSearch : NumbersSearchService
{
    private IEnumerable<int> _numbers;
    private bool _useStubNumbers;

    public StubNumberSearch() : base(null)
    {
    }

    public void SetNumbers(params int[] numbers)
    {
        _numbers = numbers.ToArray();
        _useStubNumbers = true;
    }

    public override IEnumerable<int> GetNumbers()
        => _useStubNumbers ? _numbers : base.GetNumbers();
}
Enter fullscreen mode Exit fullscreen mode

The approach is almost identical to the one we saw before. The difference can be seen in your tests:

[Test]
public void Should_UseStubService()
{
    // Arrange
    var service = new StubNumberSearch();
    service.SetNumbers(12, 15, 30);

    // Act
    var result = service.Contains(15);

    // Assert
    Assert.That(result, Is.True);
}
Enter fullscreen mode Exit fullscreen mode

There is a problem with this approach: many devs (correctly) add null checks in the constructor to ensure that the dependencies are not null:

public NumbersSearchService(NumbersRepository repository)
{
    ArgumentNullException.ThrowIfNull(repository);
    _repository = repository;
}
Enter fullscreen mode Exit fullscreen mode

While this approach makes it safe to use the NumbersSearchService reference within the class' methods, it also stops us from creating a StubNumberSearch. Since we want to create an instance of NumbersSearchService without the burden of injecting all the dependencies, we call the base constructor passing null as a value for the dependencies. If we validate against null, the stub class becomes unusable.

There's a simple solution: adding a protected empty constructor:

public NumbersSearchService(NumbersRepository repository)
{
    ArgumentNullException.ThrowIfNull(repository);
    _repository = repository;
}

protected NumbersSearchService()
{
}
Enter fullscreen mode Exit fullscreen mode

We mark it as protected because we want that only subclasses can access it.

Way 3: Use the "new" keyword in methods to hide the base implementation

Similar to the virtual keyword is the new keyword, which can be applied to methods.

We can then remove the virtual keyword from the base class and hide its implementation by marking the overriding method as new.

public class NumbersSearchService
{
    private readonly NumbersRepository _repository;

    public NumbersSearchService(NumbersRepository repository)
    {
        ArgumentNullException.ThrowIfNull(repository);
        _repository = repository;
    }

    public bool Contains(int number)
    {
        var numbers = _repository.GetNumbers();
        return numbers.Contains(number);
    }

-    public virtual IEnumerable<int> GetNumbers() => _repository.GetNumbers();
+    public IEnumerable<int> GetNumbers() => _repository.GetNumbers();
}
Enter fullscreen mode Exit fullscreen mode

We have restored the original implementation of the Repository.

Now, we can update the stub by adding the new keyword.

internal class StubNumberSearch : NumbersSearchService
{
    private IEnumerable<int> _numbers;
    private bool _useStubNumbers;

    public void SetNumbers(params int[] numbers)
    {
        _numbers = numbers.ToArray();
        _useStubNumbers = true;
    }

-    public override IEnumerable<int> GetNumbers() => _useStubNumbers ? _numbers : base.GetNumbers();
+    public new IEnumerable<int> GetNumbers() => _useStubNumbers ? _numbers : base.GetNumbers();
}
Enter fullscreen mode Exit fullscreen mode

We haven't actually solved any problem except for one: we can now avoid cluttering all our classes with the virtual keyword.

A question for you! Is there any difference between using the new and the virtual keyword? When you should pick one instead of the other? Let me know in the comments section! 📩

Way 4: Mock concrete classes by marking a method as virtual

Sometimes, I hear developers say that mocks are the absolute evil, and you should never use them.

Oh, come on! Don't be so silly!

That's true, when using mocks you are writing tests on a irrealistic environment. But, well, that's exactly the point of having mocks!

If you think about it, at school, during Science lessons, we were taught to do our scientific calculations using approximations: ignore the air resistance, ignore friction, and so on. We knew that that world did not exist, but we removed some parts to make it easier to validate our hypothesis.

In my opinion, it's the same for testing. Mocks are useful to have full control of a specific behaviour. Still, only relying on mocks makes your tests pretty brittle: you cannot be sure that your system is working under real conditions.

That's why, as I explained in a previous article, I prefer the Testing Diamond over the Testing Pyramid. In many real cases, five Integration Tests are more valuable than fifty Unit Tests.

But still, mocks can be useful. How can we use them if we don't have interfaces?

Let's start with the basic example:

public class NumbersRepository
{
    private readonly int[] _allNumbers;

    public NumbersRepository()
    {
        _allNumbers = Enumerable.Range(0, 100).ToArray();
    }

    public IEnumerable<int> GetNumbers() => Random.Shared.GetItems(_allNumbers, 50);
}

public class NumbersSearchService
{
    private readonly NumbersRepository _repository;

    public NumbersSearchService(NumbersRepository repository)
    {
        ArgumentNullException.ThrowIfNull(repository);
        _repository = repository;
    }

    public bool Contains(int number)
    {
        var numbers = _repository.GetNumbers();
        return numbers.Contains(number);
    }
}
Enter fullscreen mode Exit fullscreen mode

If we try to use Moq to create a mock of NumbersRepository (again, the concrete class) like this:

[Test]
public void Should_WorkWithMockRepo()
{
    // Arrange
    var repository = new Moq.Mock<NumbersRepository>();
    repository.Setup(_ => _.GetNumbers()).Returns(new int[] { 1, 2, 3 });
    var service = new NumbersSearchService(repository.Object);

    // Act
    var result = service.Contains(3);

    // Assert
    Assert.That(result, Is.True);
}
Enter fullscreen mode Exit fullscreen mode

It will fail with this error:

System.NotSupportedException : Unsupported expression: _ => _.GetNumbers()
Non-overridable members (here: NumbersRepository.GetNumbers) may not be used in setup / verification expressions.

This error occurs because the implementation GetNumbers is fixed as defined in the NumbersRepository class and cannot be overridden.

Unless you mark it as virtual, as we did before.

public class NumbersRepository
{
    private readonly int[] _allNumbers;

    public NumbersRepository()
    {
        _allNumbers = Enumerable.Range(0, 100).ToArray();
    }

-    public IEnumerable<int> GetNumbers() => Random.Shared.GetItems(_allNumbers, 50);
+    public virtual IEnumerable<int> GetNumbers() => Random.Shared.GetItems(_allNumbers, 50);
}
Enter fullscreen mode Exit fullscreen mode

Now the test passes: we have successfully mocked a concrete class!

Further readings

Testing is a crucial part of any software application. I personally write Unit Tests even for throwaway software - this way, I can ensure that I'm doing the correct thing without the need for manual debugging.

However, one part that is often underestimated is the code quality of tests. Tests should be written even better than production code. You can find more about this topic here:

🔗 Tests should be even more well-written than production code | Code4IT

Also, Unit Tests are not enough. You should probably write more Integration Tests than Unit Tests. This one is a testing strategy called Testing Diamond.

🔗 Testing Pyramid vs Testing Diamond (and how they affect Code Coverage) | Code4IT

This article first appeared on Code4IT 🐧

Clearly, you can write Integration Tests for .NET APIs easily. In this article, I explain how to create and customize Integration Tests using NUnit:

🔗 Advanced Integration Tests for .NET 7 API with WebApplicationFactory and NUnit | Code4IT

Wrapping up

In this article, we learned that it's not necessary to create interfaces for the sake of having mocks.

We have different other options.

Honestly speaking, I'm still used to creating interfaces and using them with mocks.

I find it easy to do, and this approach provides a quick way to create tests and drive the behaviour of the dependencies.

Also, I recognize that interfaces created for the sole purpose of mocking are quite pointless: we have learned that there are other ways, and we should consider trying out these solutions.

Still, interfaces are quite handy for two "non-technical" reasons:

  • using interfaces, you can understand in a glimpse what are the operations that you can call in a clean and concise way;
  • interfaces and mocks allow you to easily use TDD: while writing the test cases, you also define what methods you need and the expected behaviour. I know you can do that using stubs, but I find it easier with interfaces.

I know, this is a controversial topic - I'm not saying that you should remove all your interfaces (I think it's a matter of personal taste, somehow!), but with this article, I want to highlight that you can avoid interfaces.

I hope you enjoyed this article! Let's keep in touch on Twitter or LinkedIn! 🤜🤛

Happy coding!

🐧

Top comments (1)

Collapse
 
canro91 profile image
Cesar Aguirre

Good point! Excessive mocking leads to tests harder to refactor...change one name and we will end up changing lots of classes :\

in all your career, how many times have you changed the underlying database?

Only once too...and probably due to a bad tech choice in the first place