如何模拟和设置局部变量

发布于 2025-01-14 02:37:43 字数 1307 浏览 0 评论 0原文

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

var telemetry = telemetryInit as TelemetryInitializerStandard;

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

公开课ABC { 私有ILogger日志; 私有只读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 
    }
}

测试

[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>();
}

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

一杯敬自由 2025-01-21 02:37:43

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

然后,我们将传入的实例转换为具体类 TelemetryInitializerStandard 并使用具体类上但未包含在接口中的某些属性(TenantId?)。这打破了“ITelemetryInitializer”是一个足够参数的“约定”。

在完美的世界中,解决方案是扩展 ITelemetryInitializer 接口以包含我们需要的 TelemetryInitializerStandard 中的属性,或者替换 ITelemetryInitializer参数与 TelemetryInitializerStandard 之一。 (问题不在于我们使用 TelemetryInitializerStandard,而是 ITelemetryInitializerTelemetryInitializerStandard 之间不匹配)。

很多时候,这不是一个完美的世界,我们无法完全控制我们使用的代码(例如,其他人拥有该接口,我们无法更改它)

可以模拟一个具体的类(至少使用 Moq 4.17.2)但我们只能模拟虚拟的属性/方法

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

使用上面的代码我得到

A

!!抽象泄漏!!

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

C
Foo(嘲笑)
Bar (Mocked)

最重要的是,如果您幸运并且您在 TelemetryInitializerStandard 上使用的属性是虚拟的,那么您可以 Mock 它们,但是,如果您可以控制代码并有时间这样做,我将扩展 ITelemetryInitializer 以包含所需的属性。

可以传入 TelemetryInitializerStandard,但这意味着对于单元测试,您需要将该类上的所有内容都设置为虚拟,这看起来就像是尾巴在摇狗。

将接口转换为特定类以利用该类上的功能是一种非常强烈/糟糕的代码味道,应该避免。如果我们无法控制 ITelemetryInitializer 接口,也许我们可以将其子类化

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

并将其传递给 ABC 。

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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文