-1

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>();
}
K.Z
  • 5,201
  • 25
  • 104
  • 240
  • This `var telemetry = telemetryInit as TelemetryInitializerStandard;` is something that you should definitely avoid in your code. – Eldar Mar 14 '22 at 21:33
  • I am doing migration but why should I avoid and how I should implement then? – K.Z Mar 14 '22 at 21:35
  • Your method should not be aware of the concrete implementation. The interface should be enough for the code to run. If your interface doesn't meet the requirements you should modify it. – Eldar Mar 14 '22 at 21:37
  • I just change code to pass ITelemetryInitializer in constructor – K.Z Mar 14 '22 at 21:43
  • How I can mock and setup local variable `var telemetryMoq` var telemetryMoq = fixture.Create(); ??? – K.Z Mar 14 '22 at 21:44
  • You can not (You can not mock a Class unless it is an `abstract` class) and you should not (a local variable is out of scope in your test). Why do you need that local variable as a concrete implementation? – Eldar Mar 14 '22 at 21:49
  • ok I am getting null value for `var telemetry = telemetryInit as TelemetryInitializerStandard;` so when I assign value `telemetry.TenantId = tenant.Id;` inside the WhenAll thread.. it throw null exception in test – K.Z Mar 14 '22 at 21:54
  • what you suggest, how should I solve above issue, Many thanks for the support in advance – K.Z Mar 14 '22 at 21:54
  • Just use what the `ITelemetryInitializer` interface exposes. Don't cast it to anything just use the field as is. And if it is not enough expose more methods or properties. – Eldar Mar 14 '22 at 22:00

1 Answers1

1

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.

AlanT
  • 3,627
  • 20
  • 28