Unmockable 3.0.132
dotnet add package Unmockable --version 3.0.132
NuGet\Install-Package Unmockable -Version 3.0.132
<PackageReference Include="Unmockable" Version="3.0.132" />
paket add Unmockable --version 3.0.132
#r "nuget: Unmockable, 3.0.132"
// Install Unmockable as a Cake Addin #addin nuget:?package=Unmockable&version=3.0.132 // Install Unmockable as a Cake Tool #tool nuget:?package=Unmockable&version=3.0.132
📢 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.
- <s>There are are no</s> 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!
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. |
.NET Core | netcoreapp1.0 was computed. netcoreapp1.1 was computed. netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
.NET Standard | netstandard1.3 is compatible. netstandard1.4 was computed. netstandard1.5 was computed. netstandard1.6 was computed. netstandard2.0 was computed. netstandard2.1 was computed. |
.NET Framework | net46 was computed. net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
MonoAndroid | monoandroid was computed. |
MonoMac | monomac was computed. |
MonoTouch | monotouch was computed. |
Tizen | tizen30 was computed. tizen40 was computed. tizen60 was computed. |
Universal Windows Platform | uap was computed. uap10.0 was computed. |
Xamarin.iOS | xamarinios was computed. |
Xamarin.Mac | xamarinmac was computed. |
Xamarin.TVOS | xamarintvos was computed. |
Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 1.3
- NETStandard.Library (>= 1.6.1)
NuGet packages (2)
Showing the top 2 NuGet packages that depend on Unmockable:
Package | Downloads |
---|---|
Unmockable.Wrap
Wrap unmockable objects. |
|
Unmockable.Intercept
Intercept calls on wrappers around unmockable objects. |
GitHub repositories
This package is not used by any popular GitHub repositories.
Version | Downloads | Last updated |
---|---|---|
3.0.132 | 3,696 | 4/19/2023 |
3.0.131 | 377 | 4/19/2023 |
3.0.120 | 6,495 | 2/25/2021 |
3.0.99 | 793 | 2/25/2021 |
3.0.93 | 885 | 12/18/2020 |
3.0.92 | 840 | 12/18/2020 |
3.0.91 | 1,160 | 11/2/2020 |
3.0.89 | 1,157 | 11/21/2019 |
3.0.88 | 932 | 11/21/2019 |
2.2.86 | 1,000 | 10/10/2019 |
2.2.85 | 1,004 | 10/10/2019 |
2.2.84 | 988 | 10/10/2019 |
2.1.80 | 980 | 10/9/2019 |
2.1.79 | 950 | 10/8/2019 |
2.0.77 | 925 | 10/8/2019 |
2.0.76 | 927 | 10/4/2019 |
2.0.75 | 947 | 10/4/2019 |
2.0.73 | 949 | 10/3/2019 |
2.0.72 | 945 | 10/3/2019 |
1.0.71 | 1,059 | 10/1/2019 |
0.0.70 | 969 | 9/24/2019 |
0.0.68 | 1,010 | 9/20/2019 |
0.0.66 | 975 | 9/5/2019 |
0.0.65 | 994 | 9/5/2019 |
0.0.64 | 1,026 | 8/29/2019 |
0.0.63 | 1,001 | 8/29/2019 |
0.0.62 | 984 | 8/12/2019 |
0.0.61 | 1,074 | 7/29/2019 |
0.0.59 | 1,021 | 7/29/2019 |
0.0.55 | 1,016 | 7/29/2019 |
0.0.51 | 1,012 | 7/29/2019 |
0.0.50 | 1,064 | 4/4/2019 |
0.0.49 | 1,040 | 3/22/2019 |
0.0.48 | 1,043 | 3/22/2019 |
0.0.47 | 1,058 | 3/20/2019 |
0.0.46 | 1,009 | 3/20/2019 |
0.0.45 | 1,024 | 3/19/2019 |
0.0.44 | 1,065 | 3/19/2019 |
0.0.43 | 1,025 | 3/19/2019 |
0.0.42 | 1,064 | 3/19/2019 |
0.0.41 | 989 | 3/19/2019 |
0.0.40 | 1,154 | 3/19/2019 |
0.0.39 | 1,009 | 3/15/2019 |
0.0.38 | 1,035 | 3/15/2019 |
0.0.37 | 1,038 | 3/14/2019 |
0.0.36 | 1,028 | 3/14/2019 |
0.0.35 | 1,039 | 3/14/2019 |
0.0.34 | 1,012 | 3/14/2019 |
0.0.33 | 916 | 3/14/2019 |
0.0.31 | 598 | 3/14/2019 |
0.0.30 | 573 | 3/14/2019 |
0.0.29 | 592 | 3/14/2019 |
0.0.28 | 582 | 3/12/2019 |
0.0.27 | 596 | 3/12/2019 |
0.0.26 | 586 | 3/12/2019 |
0.0.25 | 572 | 3/12/2019 |
0.0.24 | 581 | 3/12/2019 |
0.0.23 | 584 | 3/12/2019 |
0.0.22 | 587 | 3/12/2019 |
0.0.21 | 600 | 3/11/2019 |
0.0.20 | 589 | 3/11/2019 |
0.0.19 | 567 | 3/11/2019 |
0.0.18 | 593 | 3/9/2019 |
0.0.17 | 580 | 3/9/2019 |
0.0.16 | 575 | 3/9/2019 |
0.0.14 | 571 | 3/8/2019 |
0.0.13 | 592 | 3/8/2019 |
0.0.11 | 579 | 3/8/2019 |
0.0.10 | 581 | 3/8/2019 |
0.0.9 | 589 | 3/8/2019 |
0.0.8 | 617 | 3/8/2019 |
0.0.7 | 580 | 3/8/2019 |
0.0.5 | 591 | 3/8/2019 |
removed the wrapping of unmockables.