Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AutoMoq a concrete dependency #1202

Closed
Euan-McVie opened this issue Oct 1, 2020 · 15 comments
Closed

AutoMoq a concrete dependency #1202

Euan-McVie opened this issue Oct 1, 2020 · 15 comments
Labels

Comments

@Euan-McVie
Copy link

Euan-McVie commented Oct 1, 2020

Hi,

I am trying to use a Moq of a concrete dependency.

[Theory, AutoMoqData]
public async Task Can_register_a_single_service_successfully(
    [Frozen] Mock<IdentityServerOptions> identityServerOptions,
    ServiceDiscoveryUsingIdentityServer sut
)
{...}

This throws an error where AutoFixture is trying to create a real instance of IdentityServerOptions to inject into my sut instead of using the frozen mock.
If I remove the sut and manually create passing in the identityServerOptions.Object then it all works, so my working theory is that autofixture/automoq is not picking up that the frozen moq can satisfy the dependency and so tries to create a new instance.

I also tried Frozen(Matching.DirectBaseType) as the proxy generated by moq inherits from the requested value, but still no luck.

Is there any way around this as I can't change IdentityServerOptions as it's 3rd party and have more variables to inject to my sut, which makes the workaround a bit messy when I only need to access the one from my example.

Thanks
Euan

@Kralizek
Copy link
Contributor

Kralizek commented Oct 1, 2020

Just freeze IdentityServerOptions and use Mock.Get.

Check this page of the booklet I wrote for my developers: https://docs.educationsmediagroup.com/unit-testing-csharp/autofixture/combining-autofixture-with-nunit-and-moq

@Euan-McVie
Copy link
Author

Euan-McVie commented Oct 1, 2020

Unfortunately that doesn't work as IdentityServerOptions is not an abstract class nor an interface and so the AutoMoqCustomization doesn't generate a Moq for it.

For completeness the attribute is:

public AutoMoqDataAttribute()
    : base(() => new Fixture().Customize(new AutoMoqCustomization()))
{ }

Note that for interfaces and abstract class it doesn't appear to matter if you freeze the mock wrapper or the interface and then either get the mock or use .Object in your test case.

I believe it is entirely correct that a concrete class in the parameters should give you a real copy, otherwise your sut wouldn't work, but if you specify a dependency with the Mock wrapper to force a mock then Mock<IInterface> works as you would expect, but Mock<Concrete> doesn't.

@Kralizek
Copy link
Contributor

Kralizek commented Oct 1, 2020

Then I don't understand your issue. Why do you need to use Moq at all?

@Euan-McVie
Copy link
Author

IdentityServerOptions is not my class, and can't be instantiated easily from a unit test so I need to Mock it to pass into my sut. The mocking all works great, but it appears AutoFixture/AutoMoq doesn't pick up on the fact that a Mock can be used in place of a IdentityServerOptions dependency.

@Euan-McVie
Copy link
Author

Euan-McVie commented Oct 1, 2020

Current workaround:

[Theory, AutoMoqData]
public async Task Can_register_a_single_service_successfully(
    [Frozen] ILogger<ServiceDiscoveryUsingIdentityServer> logger,
    [Frozen] IAuditor<ServiceDiscoveryUsingIdentityServer> auditor,
    [Frozen] Mock<IdentityServerOptions> identityServerOptions,
    ... the other dependencies.
)
{
   //Arrange
   var sut= new ServiceDiscoveryUsingIdentityServer(logger, auditor, identityServerOptions, identityService, authorizationService, grpcService);
}

This makes it a little messy to have to list all the dependencies in every test when I'm only usually needing the identityServerOptions mock for most of the test cases.

@aivascu
Copy link
Member

aivascu commented Nov 30, 2020

@Euan-McVie sorry for a late reply.
I hope to shed some light into the issue you're experiencing and possibly a workaround that might help you.

While I was not around when AutoMoq was developed, I believe this behavior was intended.

As you probably know AutoMoq can inject mocks two ways either as a mock and as the mocked type.
The current implementation mocks by default any interface or abstract type, given no other customizations have already intercepted the request for the same type. Which is why the auto mocking occurs for requests that have passed through the entire resolution pipeline, right before throwing an exception because the request couldn't be satisfied.

If we were to move the auto-mocking customizations closer to the beginning of the pipeline, then most types would be resolved as mocks. This is not desirable because then the types dependencies would be null or the mock would throw on creation because some guard clauses prevented the creation of the object in an invalid state.

Now it might seem logical that the [Frozen] mocks would be properly resolved, but due to the internal design we cannot tell which types are frozen/injected because the conditions under which the same instance would always be returned are too complex.
A context request for a mock of a mocked type would almost always yield a value, since almost any type can be mocked and we wouldn't know whether the request was satisfied by a frozen value.

As a workaround for your issue I suggest customizing the concrete type to be created from a mock.

public class CustomCustomization : ICustomization
{
    public void Customize(IFixture fixture)
    {
        fixture.Customize<IdentityServerOptions>(
            x => x.FromFactory<IFixture>(
                f => f.Create<Mock<IdentityServerOptions>>().Object));
    }
}

Since you would have already frozen the mock in your test parameter when you'd requested it in your SUT you would receive the same instance.

[Theory, AutoMoqData]
public async Task Can_register_a_single_service_successfully(
    [Frozen] Mock<IdentityServerOptions> identityServerOptions,
    ServiceDiscoveryUsingIdentityServer sut)

I will leave this issue open for a little while, in case you have more questions on this topic.

@Euan-McVie
Copy link
Author

Thanks for the response.

Would it work if I created a custom MockAttribute : CustomizeAttribute so that I could do something like:

[Theory, AutoMoqData]
public async Task Can_register_a_single_service_successfully(
    [Frozen, Mock(typeof(IdentityServerOptions))] IdentityServerOptions identityServerOptions,
    [Frozen] ILogger<ServiceDiscoveryUsingIdentityServer> logger,
    [Frozen] IAuditor<ServiceDiscoveryUsingIdentityServer> auditor,
    ServiceDiscoveryUsingIdentityServer sut)

This would be preferred as then the standard AutoMoqData attribute works to generate the other parameters so I can control the specific mocks of concrete objects per test rather than having lots of AutoMoqWithMockXYZ style attributes for other scenarios.

Would this possibly work (not had a chance to test yet myself), or would it not combine properly with the Frozen and underlying AutoMoqData?

public AutoMoqDataAttribute()
    : base(() => new Fixture().Customize(new AutoMoqCustomization()))
{ }

Thanks

@aivascu
Copy link
Member

aivascu commented Dec 1, 2020

@Euan-McVie I suppose you could indeed do that. Here's a naive implementation of a customization that could achieve parameter specific mocking

public class MockCustomization : ICustomization
{
    public MockCustomization(ParameterInfo parameterInfo)
    {
        ParameterInfo = parameterInfo;
    }

    public ParameterInfo ParameterInfo { get; }

    public void Customize(IFixture fixture)
    {
        var mockedType = ParameterInfo.ParameterType.GetGenericArguments()[0];
        fixture.Customizations.Add(new MockRelay(new ExactTypeSpecification(mockedType)));
    }
}

You could then define an attribute that uses it

[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)]
public class MockAttribute : Attribute, IParameterCustomizationSource
{
    public ICustomization GetCustomization(ParameterInfo parameter)
    {
        return new MockCustomization(parameter);
    }
}

and finally the test would look something like this

[Theory]
[AutoData]
public void DependencyShouldBeSameAsMockedObject(
    [Frozen][Mock] Mock<DependencyClass> dependencyMock,
    SystemUnderTest sut)
{
    Assert.Same(dependencyMock.Object, sut.Dependency);
}

@aivascu
Copy link
Member

aivascu commented Dec 2, 2020

Closing the issue.
The question seems to have been answered.

@aivascu aivascu closed this as completed Dec 2, 2020
@Euan-McVie
Copy link
Author

Looks great thanks.

@Kralizek
Copy link
Contributor

Kralizek commented Dec 3, 2020

@aivascu do you think the types above could be incorporated in the AutoMoq library?

They seem generic and general purpose enough to deserve the honour.

@aivascu
Copy link
Member

aivascu commented Dec 7, 2020

@Kralizek yes it might be useful to have it in the AutoMoq lib.
I'll add an issue to the backlog, with a proposal for this feature.

@Euan-McVie
Copy link
Author

Euan-McVie commented Dec 8, 2020

Finally had a chance to test it and it works great.
I made a minor improvement (I know its still not robust enough for all uses) so that you can still request the concrete object instead of the Mock wrapper.

public void Customize(IFixture fixture)
{
    Type? mockedType = (typeof(IMock<object>).IsAssignableFrom(ParameterInfo.ParameterType))
        ? ParameterInfo.ParameterType.GetGenericArguments()[0]
        : ParameterInfo.ParameterType;
    fixture.Customizations.Add(new MockRelay(new ExactTypeSpecification(mockedType)));
}

Allows

[Theory]
[AutoData]
public void DependencyShouldBeSameAsMockedObject(
    [Frozen][Mock] DependencyClass dependencyMock,
    SystemUnderTest sut)
{
    Assert.Same(dependencyMock, sut.Dependency);
}

@Kralizek
Copy link
Contributor

@Kralizek yes it might be useful to have it in the AutoMoq lib.
I'll add an issue to the backlog, with a proposal for this feature.

Since I recently stumbled upon this very issue, do you mind me putting forward a PR to implement the MockAttribute as you specified?

@aivascu

@aivascu
Copy link
Member

aivascu commented May 14, 2021

@Kralizek yes please do if you have implemented it. AFAIR when I attempted to implemented it wasn't as trivial as the implementation from above. Lets move the discussion to #1269 .

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants