在单元测试中使用Application Insights

本文关键字:Application Insights 单元测试 | 更新日期: 2023-09-27 18:22:40

我有一个MVC web应用程序,我正在使用Simple Injector进行DI。单元测试几乎涵盖了我所有的代码。然而,现在我已经在一些控制器中添加了一些遥测调用,在设置依赖关系时遇到了问题。

遥测调用用于将度量发送到Microsoft Azure托管的Application Insights服务。该应用程序没有在Azure中运行,只是在具有ISS的服务器上运行。AI门户告诉您应用程序的所有内容,包括您使用遥测库发送的任何自定义事件。因此,控制器需要一个Microsoft.ApplicationInsights.TelemetryClient的实例,该实例没有接口,是一个带有2个构造函数的密封类。我试着这样注册它(混合生活方式与这个问题无关,我只是为了完整起见将其包含在内):

// hybrid lifestyle that gives precedence to web api request scope
var requestOrTransientLifestyle = Lifestyle.CreateHybrid(
    () => HttpContext.Current != null,
    new WebRequestLifestyle(),
    Lifestyle.Transient);
container.Register<TelemetryClient>(requestOrTransientLifestyle);

问题是,由于TelemetryClient有2个构造函数,SI会抱怨并失败验证。我发现一篇文章展示了如何覆盖容器的构造函数解析行为,但这似乎相当复杂。首先,我想回过头来问这个问题:

如果我不将TelemetryClient作为一个注入的依赖项(只需在类中创建一个新的依赖项),那么在每次运行单元测试时,该遥测会被发送到Azure,从而创建大量错误数据吗?或者Application Insights是否足够聪明,可以知道它正在单元测试中运行,而不发送数据?

任何对这个问题的"见解"都将不胜感激!

感谢

在单元测试中使用Application Insights

Application Insights提供了一个通过模拟TelemetryChannelTelemetryClient进行单元测试的示例。

TelemetryChannel实现了ITelemetryChannel,因此模拟和注入非常容易。在本例中,您可以记录消息,然后稍后从Items收集消息以进行断言。

public class MockTelemetryChannel : ITelemetryChannel
{
    public IList<ITelemetry> Items
    {
        get;
        private set;
    }
    ...
    public void Send(ITelemetry item)
    {
        Items.Add(item);
    }
}
...
MockTelemetryChannel = new MockTelemetryChannel();
TelemetryConfiguration configuration = new TelemetryConfiguration
{
    TelemetryChannel = MockTelemetryChannel,
    InstrumentationKey = Guid.NewGuid().ToString()
};
configuration.TelemetryInitializers.Add(new OperationCorrelationTelemetryInitializer());
TelemetryClient telemetryClient = new TelemetryClient(configuration);
container.Register<TelemetryClient>(telemetryClient);

Microsoft.ApplicationInsights.TelemetryClient,它没有接口,是一个密封类,有2个构造函数。

TelemetryClient是一个框架类型,您的容器不应自动连接框架类型。

我发现一篇文章展示了如何覆盖容器的构造函数解析行为,但这似乎相当复杂。

是的,这种复杂性是经过深思熟虑的,因为我们想阻止人们创建具有多个构造函数的组件,因为这是一种反模式。

正如@qujck已经指出的那样,您可以简单地进行以下注册,而不是使用自动布线:

container.Register<TelemetryClient>(() => 
    new TelemetryClient(/*whatever values you need*/),
    requestOrTransientLifestyle);

或者Application Insights是否足够聪明,可以知道它正在单元测试中运行,而不发送数据?

不太可能。如果你想测试依赖于这个TelemetryClient的类,你最好使用一个假的实现,以防止你的单元测试变得脆弱、缓慢或污染你的Insight数据。但是,即使测试不是一个问题,根据依赖反转原则,您也应该依赖(1)由您自己的应用程序定义的抽象。使用TelemetryClient时,两点都不合格。

相反,您应该在TelemetryClient上定义一个(甚至多个)抽象,这些抽象是专门为您的应用程序定制的。因此,不要试图用它可能的100个方法来模仿TelemetryClient的API,而是只在控制器实际使用的接口上定义方法,并使它们尽可能简单,这样你就可以使控制器的代码更简单,也可以使单元测试更简单。

定义了一个好的抽象之后,就可以创建一个在内部使用TelemetryClient的适配器实现。我想你注册这个适配器如下:

container.RegisterSingleton<ITelemetryLogger>(
    new TelemetryClientAdapter(new TelemetryClient(...)));

在这里,我假设TelemetryClient是线程安全的,并且可以作为单例工作。否则,你可以这样做:

container.RegisterSingleton<ITelemetryLogger>(
    new TelemetryClientAdapter(() => new TelemetryClient(...)));

这里,适配器仍然是一个单例,但提供了一个允许创建TelemetryClient的委托。另一种选择是让适配器在内部创建(也许还可以处理)TelemetryClient。这可能会使注册更加简单:

container.RegisterSingleton<ITelemetryLogger>(new TelemetryClientAdapter());

我在使用Josh Rostad的文章编写模拟TelemetryChannel并将其注入测试中方面取得了很大成功。这是模拟对象:

public class MockTelemetryChannel : ITelemetryChannel
{
    public ConcurrentBag<ITelemetry> SentTelemtries = new ConcurrentBag<ITelemetry>();
    public bool IsFlushed { get; private set; }
    public bool? DeveloperMode { get; set; }
    public string EndpointAddress { get; set; }
    public void Send(ITelemetry item)
    {
        this.SentTelemtries.Add(item);
    }
    public void Flush()
    {
        this.IsFlushed = true;
    }
    public void Dispose()
    {
    }
}    

然后在我的测试中,一种本地方法来旋转模拟:

private TelemetryClient InitializeMockTelemetryChannel()
{
    // Application Insights TelemetryClient doesn't have an interface (and is sealed)
    // Spin -up our own homebrew mock object
    MockTelemetryChannel mockTelemetryChannel = new MockTelemetryChannel();
    TelemetryConfiguration mockTelemetryConfig = new TelemetryConfiguration
    {
        TelemetryChannel = mockTelemetryChannel,
        InstrumentationKey = Guid.NewGuid().ToString(),
    };
    TelemetryClient mockTelemetryClient = new TelemetryClient(mockTelemetryConfig);
    return mockTelemetryClient;
}

最后,运行测试!

[TestMethod]
public void TestWidgetDoSomething()
{            
    //arrange
    TelemetryClient mockTelemetryClient = this.InitializeMockTelemetryChannel();
    MyWidget widget = new MyWidget(mockTelemetryClient);
    //act
    var result = widget.DoSomething();
    //assert
    Assert.IsTrue(result != null);
    Assert.IsTrue(result.IsSuccess);
}

如果您不想走抽象/包装路径。在测试中,您可以简单地将AppInsights端点指向一个模拟的轻量级http服务器(这在ASP.NET Core中是微不足道的)。

appInsightsSettings.json

    "ApplicationInsights": {
    "Endpoint": "http://localhost:8888/v2/track"
}

如何在ASP.NET Core中设置"TestServer"http://josephwoodward.co.uk/2016/07/integration-testing-asp-net-core-middleware

另一个不走抽象路线的选项是在运行测试之前禁用遥测:

TelemetryConfiguration.Active.DisableTelemetry = true;

基于此处的其他工作;

  1. 创建通道-如果需要,您可以将其用于测试遥测
    public class MockTelemetryChannel : ITelemetryChannel
    {
        public ConcurrentBag<ITelemetry> SentTelemtries = new();
        public bool IsFlushed { get; private set; }
        public bool? DeveloperMode { get; set; }
        public string EndpointAddress { get; set; }
        public void Send(ITelemetry item)
        {
            this.SentTelemtries.Add(item);
        }
        public void Flush()
        {
            this.IsFlushed = true;
        }
        public void Dispose()
        {
        }
    }
  1. 使用一个不错的静态工厂类
    public static class MockTelemetryClient
    {
        public static TelemetryClient Create()
        {
            var mockTelemetryChannel = new MockTelemetryChannel();
            var mockTelemetryConfig = new TelemetryConfiguration
            {
                TelemetryChannel = mockTelemetryChannel,
                InstrumentationKey = Guid.NewGuid().ToString()
            };
            var mockTelemetryClient = new TelemetryClient(mockTelemetryConfig);
            return mockTelemetryClient;
        }
    }
  1. 致电MockTelemetryClient.Create()获取您的TelemetryClient
  2. 利润

我的一位同事编写了这个有用的库,它引入了一些核心遥测类型(例如ITelemetryClientIMetric)的抽象。

https://github.com/thomhurst/ApplicationInsights.TelemetryLogger

非常容易实现。您几乎不需要更改生产代码中的任何内容,在测试中进行嘲讽就变得轻而易举了。以下是自述文件的摘录:

依赖项注入

正常呼叫AddApplicationInsightsTelemetry(),然后呼叫AddApplicationInsightsTelemetryClientInterfaces()

public void ConfigureServices(IServiceCollection services)
{
    services
        .AddApplicationInsightsTelemetry()
        .AddApplicationInsightsTelemetryClientInterfaces();
}

ITelemetryClient

想要与TelemetryClient相同的用法吗?将ITelemetryClient注入到您的类中。它拥有TelemetryClient的所有可用方法(除了任何不应该调用的方法,例如internal或弃用的方法)。

public class MyClass
{
    private readonly ITelemetryClient _telemetryClient;
    public MyClass(ITelemetryClient telemetryClient)
    {
        _telemetryClient = telemetryClient;
    }
    
    public void DoSomething()
    {
        _telemetryClient.TrackTrace("Something happened");
    }
}