Skip to content

πŸ’‰ β†ͺ️ 🎁 Unmockable objects wrapping in .NET

Notifications You must be signed in to change notification settings

riezebosch/Unmockable

Repository files navigation

build status codecov maintainability nuget stryker

πŸ“’ Shout-out

A big shoutout to Microsoft and other vendors to start unit testing your SDKs so you'll share our pain and give us some freaking extension points.

Dependency Inversion Principle One should "depend upon abstractions, not on concretions."

Please, don't give us the Unmockable<πŸ–•>.

Support

Please, retweet to support this petition and @mention your vendor.

Unmockable

Imagine you need a dependency on a 3rd party SDK where all types carry no interfaces, and all methods are not virtual. Your only option is writing a wrapper that either implements an interface or has its methods marked virtual and does nothing more than passing through calls to the underlying object.

That's where this tiny library comes in. It acts as that handwritten wrapper for you.

Not a replacement

For dependencies under control, introduce interfaces, and use a regular mocking framework like NSubstitute or Moq. Kindly send an email to the vendor of the SDK you're using if they could pretty please introduce some interfaces. It is a no-brainer to extract an interface, and it helps us to be SOLID.

Feature slim

This library has a particular purpose and is deliberately not a full-fledged mocking framework. Therefore I try to keep its features as slim as possible, meaning:

  • All mocks are strict; each invocation requires explicit setup.
  • There are are no wild card argument matchers.
  • The API is straightforward and sparse.
  • Wrapping static classes is not supported.

That being said, I genuinely believe in pure TDD, so everything is written from a red-green-refactor cycle, and refactoring is done with SOLID principles and Design Patterns in hand. If you spot a place where another pattern could be applied, don't hesitate to let me know.

Different

What makes it different from Microsoft Fakes, Smocks, or Pose is that it only uses C# language constructs. There is no runtime rewriting or reflection/emit under the hood. Of course, this impacts the way you wrap and use your dependency, but please, don't let us clean up someone else's πŸ’©.

Usage

I prefer NSubstitute over Moq for its crisp API. However, since we are (already) dealing with Expressions, I felt it was more convenient (and easier for me to implement) to resemble the Moq API.

πŸ’‰ Inject

Inject an unmockable* object:

public class SomeLogic
{
    public SomeLogic(IUnmockable<HttpClient> client)
    {
        _client = client;
    }
}

Wrap the execution of a method in an expression:

public async Task DoSomething(int input)
{
    await _client.Execute(x => x.DownloadAsync(...));
}

* The HttpClient is just a hand-picked example and not necessarily unmockable. There have been some debate around this type. Technically it is mockable, as long as you are not afraid of message handlers.

Concrete unmockable types (pun intented) I had to deal with recently are the ExtensionManagementHttpClient and the AzureServiceTokenProvider.

β†ͺ️ Intercept

Inject an interceptor from a test using Unmockable.Intercept:

var client = Interceptor.For<HttpClient>()
    .Setup(x => x.DownloadAsync(...))
    .Returns(...);

var target = new SomeLogic(client);
await target.DoSomething(3);

client.Verify();

Note: Since v3 the API has changed in Interceptor.For to generate an interceptor for some unmockable.

Only strict 'mocks' are supported, meaning all invocations require setup, and all setups demand invocation. Using strict mocks saves you from NullReferenceExceptions and makes verification easy.

If you really want a stub instead of a mock, I'd recommend auto-mocking with AutoFixture:

var fixture = new AutoFixture();
fixture
    .Customize(new AutoConfiguredNSubstituteCustomization());

var client = fixture
    .Create<IUnmockable<HttpClient>>();

var target = new SomeLogic(client);
await target.DoSomething(3);

🎁 Wrap

Inject the wrapper object using Unmockable.Wrap:

services
    .AddTransient<IUnmockable<HttpClient>, Wrap<HttpClient>>();
services
    .AddScoped<HttpClient>();

Or wrap an existing object:

var client = new HttpClient().Wrap();

Or add wrappers for all services with Unmockable.DependencyInjection:

services
    .AddScoped<HttpClient>();
services
    .AddUnmockables();

Remark: The expressions are compiled at runtime on every invocation, so there is a performance penalty. I tried to add caching here, but that turns out not to be a sinecure.

Matchers

Collection arguments get unwrapped when matching the actual call with provided setups! Value types, anonymous types and classes with a custom GetHashCode() & Equals() should be safe. You can do custom matching with Arg.Ignore<T>(), Arg.Where<T>(x => ...) and Arg.With<T>(x => ...), though the recommendation is to be explicit as possible.

matcher description
Ignore ignore the actual value
Where match the actual value using the predicate
With do something like an assertion on the actual value

Using explicit values in the setup:

Interceptor
    .For<SomeUnmockableObject>()
    .Setup(m => m.Foo(3))
    .Returns(5);

When the actual value doesn't matter or is hard or impossible to setup:

Interceptor
    .For<SomeUnmockableObject>()
    .Setup(m => m.Foo(Arg.Ignore<int>()))
    .Returns(5);

If you need some more complex matching:

Interceptor
    .For<SomeUnmockableObject>()
    .Setup(m => m.Foo(Arg.Where<int>(x => x > 5 && x <= 10)))
    .Returns(5);

Assertion on the arguments is done using the With matcher. This is a bit of a combination of Ignore and Where, since you receive the value in the lambda but do not provide a result for the matcher.

The assertion should throw an exception when the actual value does not meet your expectations.

Interceptor
    .For<SomeUnmockableObject>()
    .Setup(m => m.Foo(Arg.With<int>(x => x.Should().Be(3, ""))))
    .Returns(5);

Mind that you need to specify values for all optionals also since expression trees may not contain calls that uses optional arguments.

Optional arguments not allowed in expressions

An expression tree cannot contain a call or invocation that uses optional arguments

You can use the default literal for all arguments (in C# 7.1.), but be aware that this is the default value of the type, which is not necessarily the same as the default value specified for the argument!

client.Execute(x => x.InstallExtensionByNameAsync("some-value", "some-value", default, default, default));

On the plus side, you now have to make it explicit both on the Execute and the Setup end making it less error-prone.

Unmockable unmockables

What if your mocked unmockable object returns an unmockable object?! Just wrap the (in this case) data fetching functionality in a separate class, test it heavily using integration tests, and inject that dependency into your current system under test.

Static classes

At first, I added, but then I removed support for 'wrapping' static classes and invoking static methods. In the end, it is not an unmockable object! If you're dependent, let's say, on DateTime.Now you can add a method overload that accepts a specific DateTime. You don't need this framework for that.

public void DoSomething(DateTime now)
{
    if (now ...) {}
}

public void DoSomething() => DoSomething(DateTime.Now)

Or with a factory method if it has to be more dynamic.

public void DoSomething(Func<DateTime> now)
{
    while (now() <= ...) {}
}

public void DoSomething() => DoSomething(() => DateTime.Now)

If you don't like this change in your public API, you can extract an interface and only include the second method (which is a good idea anyway), or you mark the overloaded method internal and expose it to your test project with the [InternalsVisibleTo] attribute.


Happy coding!

About

πŸ’‰ β†ͺ️ 🎁 Unmockable objects wrapping in .NET

Topics

Resources

Stars

Watchers

Forks

Packages

No packages published

Languages