Search code examples
c#nunitmoq.net-core-3.1autofixture

How to mock and setup local variable


I am working on .NET CORE 3.1 nUnit tests with mock and autoFixture library. I need to mock telemetry variable not sure how to acheive it?

var telemetry = telemetryInit as TelemetryInitializerStandard;

I am trying to mock this variable otherwise it will be null and I cannot assign the value. The telemetry require to be TelemetryInitializerStandard instance.

public class ABC { private ILogger log; private readonly ITelemetryInitializer telemetryInit;

 public ABC(ILogger logger, ITelemetryInitializer telemetryInit)
 {
        log = logger;
        this.telemetryInit = telemetryInit;
 }
 public async Task<List<Customer>> RunAsync()
    {
        var telemetry = telemetryInit as TelemetryInitializerStandard;
        // remaining code 
    }
}

Test

[TestFixture]
public class AbcTest
{
    private readonly ABC _sut;
    private readonly Mock<ILogger> _loggerMoq;
    private readonly Mock<ITelemetryInitializer> _telemetryInitializerMoq;
 
 public AbcTest()
    {
        this._loggerMoq = new Mock<ILogger>();
        this._telemetryInitializerMoq = new Mock<ITelemetryInitializer>();

        this._sut = new DiscoverFA(_loggerMoq.Object,  _telemetryInitializerMoq.Object);

[Test]
public void Test1()
{
        //Arrange
        var fixture  = new Fixture();

        var telemetryMoq = fixture.Create<TelemetryInitializerStandard>();
}

Solution

  • Whether we pass the ITelemetryInitializer in as a parameter to RunAsync() or pass it into the class via the constructor and use it in the method, the problem is the same. We have, by stating that the parameter is of type ITelemetryInitializer, told anyone using the code (including ourselves) that anything which implements that interface can be used here.

    We then cast the passed in instance to a concrete class TelemetryInitializerStandard and use some property (TenantId?) that is on the concrete class but not included in the interface. This breaks the 'contract' that ITelemetryInitializer is a sufficient parameter.

    In a perfect world, the solution would be to extend the ITelemetryInitializer interface to include the properties from TelemetryInitializerStandard that we need, or, replace the ITelemetryInitializer parameter with a TelemetryInitializerStandard one. (The problem is not that we are using the TelemetryInitializerStandard but the mismatch between ITelemetryInitializer and TelemetryInitializerStandard).

    Too often it is not a perfect world and we do not have full control over the code we use (e.g. someone else owns the interface and we cannot change it)

    It is possible to mock a concrete class (at least with Moq 4.17.2) but we can only mock properties/methods which are virtual

    internal class Program
    {
        static void Main()
        {
    
            var mockClass = new Mock<LeakyConcrete>();
            mockClass.Setup(mk => mk.Foo()).Returns("Foo (Mocked)");
            mockClass.Setup(mk => mk.Bar()).Returns("Bar (Mocked)");
    
            var driver = new Driver(new Concrete());  //A
            //var driver = new Driver(new LeakyConcrete());  //B
            //var driver = new Driver(mockClass.Object);  //C
            driver.Run();
            driver.RunWithLeak();
        }
    }
    
    public interface IAbstraction
    {
        string Foo();
    }
    
    class Driver
    {
    
        private readonly IAbstraction _abstraction;
    
        public Driver(IAbstraction abstraction)
        {
            _abstraction = abstraction;
        }
    
        public void Run()
        {
            var value = _abstraction.Foo();
            System.Console.WriteLine(value);
        }
    
        public void RunWithLeak()
        {
            var value = (_abstraction as LeakyConcrete)?.Bar() ?? "!!Abstraction Leak!!";
            System.Console.Write(value);
        }
    }
    
    public class Concrete : IAbstraction
    {
        public string Foo()
        {
            return "Foo";
        }
    }
    
    public class LeakyConcrete : IAbstraction
    {
        public virtual string Foo()
        {
            return "Foo (leaky)";
        }
    
        public virtual string Bar()
        {
            return "Bar (leaky)";
        }
    }
    

    With above code I get

    A
    Foo
    !!Abstraction Leak!!

    B
    Foo (leaky)
    Bar (leaky)

    C
    Foo (Mocked)
    Bar (Mocked)

    Bottom line, if you are lucky and the properties you are using on TelemetryInitializerStandard are virtual, then you can Mock them, but, if you have control of the code and the time to do it, I would extend ITelemetryInitializer to include the properties needed.

    It is possible to pass in a TelemetryInitializerStandard, but that would mean that for unit testing you would need to have everything on that class be virtual, which seems like the tail wagging the dog.

    Casting an interface to a specific class to make use of functionality on that class is a pretty strong/bad code smell and should be avoided. If we do not have control of the ITelemetryInitializer interface, perhaps we can sub-class it

    public interface ITelemetryInitializerStandard : ITelemetryInitializer
    {
        string TenantId { get; set; }
    }
    

    and pass that into ABC instead.