如何模拟和设置局部变量

How to mock and setup local variable

我正在使用 mock 和 autoFixture 库进行 .NET CORE 3.1 nUnit 测试。我需要模拟遥测变量不知道如何实现它?

var telemetry = telemetryInit as TelemetryInitializerStandard;

我正在尝试模拟这个变量,否则它将为空,我无法分配该值。遥测需要是 TelemetryInitializerStandard 实例。

public class 字母表 { 私有 ILogger 日志; 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>();
}

无论是将ITelemetryInitializer作为参数传入RunAsync(),还是通过构造函数传入class,在方法中使用,问题都是一样的。我们已经通过声明参数的类型 ITelemetryInitializer 告诉任何使用该代码的人(包括我们自己)任何实现该接口的东西都可以在这里使用。

然后我们将传入的实例转换为具体的 class TelemetryInitializerStandard 并使用一些 属性 (TenantId?) 在具体的 class 但不包括在内在界面中。这打破了 'contract',即 ITelemetryInitializer 是一个足够的参数。

在理想情况下,解决方案是扩展 ITelemetryInitializer 接口以包含我们需要的来自 TelemetryInitializerStandard 的属性,或者,将 ITelemetryInitializer 参数替换为 TelemetryInitializerStandard一个。 (问题不是我们使用的是 TelemetryInitializerStandard,而是 ITelemetryInitializerTelemetryInitializerStandard 之间的不匹配)。

这往往不是一个完美的世界,我们无法完全控制我们使用的代码(例如,其他人拥有界面,我们无法更改它)

可以模拟具体的 class(至少对于 Moq 4.17.2),但我们只能模拟 properties/methods 是虚拟的

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)";
    }
}

用上面的代码我得到

一个

!!抽象泄漏!!

B
Foo(泄漏)
酒吧(漏水)

C
Foo(嘲笑)
栏(模拟)

底线,如果你很幸运并且你在 TelemetryInitializerStandard 上使用的属性是虚拟的,那么你可以模拟它们,但是,如果你可以控制代码和时间来做,我将扩展 ITelemetryInitializer 以包含所需的属性。

可以传入一个 TelemetryInitializerStandard,但这意味着对于单元测试,您需要让 class 上的所有内容都是虚拟的,这就像尾巴摇狗一样.

将接口转换为特定 class 以利用该 class 上的功能是一种相当 strong/bad 的代码味道,应该避免。如果我们没有 ITelemetryInitializer 接口的控制权,也许我们可以 sub-class 它

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

并将其传递给 ABC