We can test default implementations with our own fake objects, however, we cannot test them with mocking-framework objects (at least for now).Once we take a closer look at some tests and understand what mocking frameworks do for us, this will make a bit more sense. Hopefully our favorite frameworks will give us an easy way to test default implementations in the future, but for now we have to do things a bit more manually.
The code for this article is available on GitHub: jeremybytes/interfaces-in-csharp-8.
Note: this article uses C# 8 features, which are currently only implemented in .NET Core 3.0. For these samples, I used Visual Studio 16.3.1 and .NET Core 3.0.100.
This article uses 2 projects. UnitTests.Library is a .NET Standard 2.1 class library that contains the interface. UnitTests.Tests is a .NET Core 3.0 NUnit unit test project; this contains the tests.
Testing Default Implementation
I am a big believer in unit testing. When I put code into my projects, I think of how the code can be written so that it is easy to test. The same is true when it comes to default implementation in interfaces. The basics involve just a few steps.
Here's the interface that we use for these tests, IRegularPolygon (from the IRegularPolygon file in the UnitTests.Library project):
This interface describes a regular polygon -- meaning, a shape that has 3 or more sides where each side is the same length. The "GetPerimeter" method has a default implementation, and this is what we want to test.
A Fake Object
For the first test, we will use a fake object. This is a manually-created class that implements the interface.
Here is the code for the FakeWithDefault class (from the IRegularPolygon.Tests.cs file in the UnitTests.Tests project):
This class provides implementations for the abstract members of the interface, but it does *not* provide an override for the GetPerimeter method. It relies on the default implementation from the interface.
A First Test
Now that we have the interface and an implementing class, we can write our first unit test. Here is the code for that (from the IRegularPolygon.Tests.cs file -- the rest of the code samples will come from this file):
This test creates an instance of our fake object and assigns it to an IRegularPolygon variable.
It is important to specify that the variable is "IRegularPolygon". If we use "var" or "FakePolygonWithDefault" as the type, then the GetPerimeter method will not be accessible.
The next line calls the GetPerimeter method on the interface. This will use the default implementation.
The last line checks the value of the output. Our fake object uses the values of "4" and "5" for NumberOfSides and SideLength, respectively. So we expect that the value of GetPerimeter is 4 * 5, or 20.
This test passes:
So we can see that we can test a default implementation with just a few steps. We create a class that implements the interface but does not override the default implementations from the interface. In the test, we create an instance of that class and cast it to the interface. Finally, we call the member with the default implementation and check the result.
Testing with a Mocking Framework
Sometimes I manually create fake objects for my unit tests, but I generally use a mocking framework instead. This reduces the number of objects in the unit test projects. And once we're comfortable with a particular framework, we can quickly create test objects with a variety of configurations.
Unfortunately, we cannot test default implementation with the way that mocking frameworks work today. Let's start by looking at an example using Moq (the framework I generally turn to).
Creating a Mock with Moq
The test project includes a NuGet reference to the Moq package. With that in place, we can write a test that uses a mock object:
When using a mocking framework, we tell the framework how to create and configure our test object. The first line of our test creates a mock based on the IRegularPolygon interface.
The next two lines set up the mock with specific values for the NumberOfSides property and the SideLength property. In this case, if we ask the mock for "NumberOfSides", it will give us "3"; if we ask for "SideLength", it will give us "5".
The setup does *not* include configuration for the "GetPerimeter" or the "GetArea" methods. This is a key feature of mocking frameworks, and we'll discuss this further down.
The 4th line calls the "GetPerimeter" method on our mock object. (The variable "mock" is a "Mock<IRegularPolygon>". When using Moq, the "Object" property represents the "IRegularPolygon" object.)
The last line checks the results.
Unfortunately, we do not get the results we want.
This test fails. When we look at the Error Message, we see that the value was "0.0d" (meaning 0 as a double) instead of the expected "15.0d".
This is the expected behavior from Moq (and mocking frameworks in general). When we configure a mock object, we only need to fill in the items that we use. And that can be a big time saver, particularly if we need to quickly mock up a dependency needed for a test.
When we create an object manually (FakePolygonWithDefault), we need to provide implementations for all of the abstract members of the interface. This means that we have to provide an implementation for "GetArea" even though we do not use that method in the tests.
When we create a mock object using a framework, we only need to provide implementations for the things we care about. The mocking framework will take care of the rest.
In the unit test using Moq, we do not need to provide a configuration for the "GetArea" method because we do not use it in the tests. If, for some reason, a test calls the "GetArea" method, the mocking framework will return the default value. Since "GetArea" returns a double, the default value is "0.0d".
Does this value look familiar? We *are* using the "GetPerimeter" method in our tests. But instead of getting the default implementation from the interface, we are getting the default value from the mocking framework. Since "GetPerimeter" also returns a double, the default value is "0.0d". This is the value we see in the failing test.
We can explicitly configure the mock object with the "GetPerimeter" method:
And it works. This test passes:
But this does not use the default implementation; it uses an explicit implementation.
I tried a couple of other frameworks: NSubstitute and FakeItEasy. These both provided similar results.
Here is a test using NSubstitute without an override of the default:
And with an override:
And the test results are similar to using Moq:
Again, this isn't a surprise if we understand how the mocking frameworks work.
Here is a test using FakeItEasy. First without an override of the default:
And with an override:
And the results:
Again, we see similar behavior to the other frameworks.
Will Mocking Frameworks Change?
All of this is still really new. C# 8 with default implementation released at the beginning of this week. So the fact that these mocking frameworks do not support default implementation by default is not surprising.
Will this change in the future?
I'm not sure. I have not done extensive searches. But in my preliminary search I haven't seen anything about default implementation, and these particular packages do not currently have pre-release versions on NuGet.
Update: As Blair Conrad notes in the comments, FakeItEasy has an open issue for supporting default implementation: Issue - Support for calling default interface members.
Here are the versions that I used for this project:
I will keep an eye on this because I'm curious how mocking frameworks will adjust with default implementation in interfaces.
It may be difficult to change the default behavior. If someone does *not* configure an interface member, do they expect to get the default value (current behavior) or the default implementation from the interface (new behavior)? Changing the current behavior could result in breaking changes.
Whichever way the mocking frameworks go, we can test default implementations manually. We can create classes that do not override the default and then use those objects in our tests.
Default implementation in interfaces gives us a lot of new things to think about. I'm most concerned with how it affects the existing code, techniques, and processes that I already have (and other people have). Once we get the current things worked out, it will be easier to figure out how to move forward in the best way we can.