Code Monkey home page Code Monkey logo

unmockable's Introduction

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!

unmockable's People

Contributors

dependabot-preview[bot] avatar dependabot[bot] avatar riezebosch avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

unmockable's Issues

New User struggling to understand usage from documentation

I am trying to use Unmockable to intercept the call to MinioClient.PutObject within the UploadAsync of my S3Service class listed below.

S3Service Class

    public class S3Service : IS3Service, IDisposable
    {
        const string JpegContentType = "image/jpeg";

        private bool _Disposed { get; set; }
        private ILogger<S3Service> _Logger { get; set; }
        private MinioClient _MinioClient { get; set; }
        private S3Config _Config { get; set; }


        /// <summary>
        /// <param name="config">S3 config from appsettings</param>
        /// <param name="minioFactory">
        /// Function to create a <see cref="MinioClient"/>. Accepts the following parameters:
        /// 1. endpoint: string
        /// 2. username: string
        /// 3. password: string
        /// </param>
        /// <param name="logger">Logger instance</param>
        /// </summary>
        public S3Service(
            IOptions<S3Config> config,
            Func<string, string, string, MinioClient> minioFactory,
            ILogger<S3Service> logger
        )
        {
            _Config = config.Value;
            _Disposed = false;
            _Logger = logger;

            _MinioClient = minioFactory(_Config.Endpoint, _Config.AccessKey, _Config.SecretKey);

            _Logger.LogInformation($"Minio client created for endpoint {_Config.Endpoint}");
        }

        public async Task UploadAsync(BufferedStream byteStream, string objectName)
        {
            // validate args are not null
            if (byteStream == null)
            {
                throw new ArgumentNullException(nameof(byteStream));
            }

            if (objectName == null)
            {
                throw new ArgumentNullException(nameof(objectName));
            }

            // Try and upload, let minio handle the validation
            try
            {
                await _MinioClient.PutObjectAsync(_Config.Bucket, objectName, byteStream, byteStream.Length, JpegContentType);
            }
            catch (MinioException e)
            {
                _Logger.LogError(e, "Error occurred while uploading to S3 storage");
            }
        }
}

Test so far - Does not compile

        [Fact]
        public async Task S3Service_UploadAsync_Calls_PutObjectAsync_With_Expected_Args()
        {
            const string objName = "objName";
            const string testData = "testData";

            var config = S3TestUtils.CreateDefaultS3Config();
            var options = Options.Create<S3Config>(config);

            using (var buffer = new BufferedStream(new MemoryStream(Encoding.UTF8.GetBytes(testData))))
            {
                var mockClient = Interceptor.For<MinioClient>()
                    .Setup(m =>
                        m.PutObjectAsync(
                            config.Bucket,
                            objName,
                            buffer,
                            buffer.Length,
                            JpegContentType,
                            null, null, default(CancellationToken)
                        )
                    );

               /**
                       Compile Error: A lambda expression with a statement body cannot be converted to an expression tree

                      I need to inject client mock into S3Service via factory method
                */
                var sut = mockClient.As<IUnmockable<MinioClient>>().Execute<Task>(client =>
               {
                   using (S3Service svc = new S3Service(
                       options, (endpoint, login, passwd) => client, new NullLogger<S3Service>()))
                   {

                       return svc.UploadAsync(buffer, objName);
                   }
               });

                // test that the method call was intercepted
                mockClient.Verify();
            }
        }

Is this possible with Unmockable library as opposed to manually writing my own interface to wrap the unmockable MinioClient?

Use of mutation testing in Unmockable - Help needed

Hello there!

My name is Ana. I noted that you use the mutation testing tool strykernet in the project.
I am a postdoctoral researcher at the University of Seville (Spain), and my colleagues and I are studying how mutation testing tools are used in practice. With this aim in mind, we have analysed over 3,500 public GitHub repositories using mutation testing tools, including yours! This work has recently been published in a journal paper available at https://link.springer.com/content/pdf/10.1007/s10664-022-10177-8.pdf.

To complete this study, we are asking for your help to understand better how mutation testing is used in practice, please! We would be extremely grateful if you could contribute to this study by answering a brief survey of 21 simple questions (no more than 6 minutes). This is the link to the questionnaire https://forms.gle/FvXNrimWAsJYC1zB9.

Drop me an e-mail if you have any questions or comments ([email protected]). Thank you very much in advance!!

Unmocakable.Intercept compatibility issue

When I try to install the NuGet package for my NUnit unit test project which targets a specific platform I get the following error:
NU1202: Package Unmockable.Intercept 3.0.89 is not compatible with netcoreapp2.2 (.NETCoreApp,Version=v2.2). Package Unmockable.Intercept 3.0.89 supports: netstandard2.1 (.NETStandard,Version=v2.1)

System.InvalidOperationException : variable 'x' of type 'System.String' referenced from scope '', but it is not defined

This may be me failing to understand the API but here goes:

This test passes

var execute = Unmockable.Interceptor.For<string>()
                        .Setup(x => x.Length).Returns(1)
                        .Execute(x => x.Length);
Assert.AreEqual(1, execute);

This fails with System.InvalidOperationException : variable 'x' of type 'System.String' referenced from scope '', but it is not defined

Unmockable.Interceptor.For<string>()
          .Setup(x => x.Length)
          .Returns(1)
          .Execute(x => Console.WriteLine(x.Length));

The example is perhaps not very good, what I'm trying to do is create a mock of MouseButtonEventArgs with setup for GetPosition that is then used in the test.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.