将 Application Insights 与单元测试结合使用?

Using Application Insights with Unit Tests?

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

遥测调用用于将指标发送到 Microsoft Azure 托管的 Application Insights 服务。该应用程序不在 Azure 中 运行ning,只是一个带有 ISS 的服务器。 AI 门户告诉您有关应用程序的各种信息,包括您使用遥测库发送的任何自定义事件。因此,控制器需要 Microsoft.ApplicationInsights.TelemetryClient 的实例,它没有接口,是一个密封的 class,具有 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 抱怨并未能通过验证。我发现 post 展示了如何覆盖容器的构造函数解析行为,但这看起来相当复杂。首先我想备份并问这个问题:

如果我不使 TelemetryClient 成为注入的依赖项(只需在 class 中创建一个新的依赖项),该遥测是否会在单元测试的每个 运行 中发送到 Azure,制造大量虚假数据?或者 Application Insights 是否足够聪明,知道它正在 运行 单元测试中,而不发送数据?

如果 "Insights" 参与此问题,我们将不胜感激!

谢谢

Microsoft.ApplicationInsights.TelemetryClient, which has no Interface and is a sealed class, with 2 constructors.

这个TelemetryClient是框架类型,framework types should not be auto-wired by your container

I found a post showing how to override the container's constructor resolution behavior, but that seems pretty complicated.

是的,这种复杂性是故意的,因为我们不希望人们创建具有多个构造函数的组件,因为这是 an anti-pattern

正如@qujck 已经指出的那样,您可以不使用自动连接,只需进行以下注册:

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

Or is Application Insights smart enough to know it is running in a unit test, and not send the data?

不太可能。如果您想测试依赖于此 TelemetryClient 的 class,您最好使用伪造的实现,以防止您的单元测试变得脆弱、缓慢或污染您的 Insight 数据。但是,即使测试不是问题,根据 Dependency Inversion Principle,您也应该依赖 (1) 由您自己的应用程序定义的 (2) 抽象。使用 TelemetryClient.

时这两点都失败了

您应该做的是在 TelemetryClient 上定义一个(或什至多个)抽象,这些抽象 专门为您的应用程序量身定制 。所以不要试图模仿 TelemetryClient 的 API 及其可能的 100 种方法,而是只在您的控制器实际使用的接口上定义方法,并使它们像 一样简单可能 这样您就可以使控制器的代码更简单 - 并且您的单元测试更简单。

定义好抽象后,您可以创建一个在内部使用 TelemetryClient 的适配器实现。我想象您按如下方式注册此适配器:

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

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

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

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

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

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

appInsightsSettings.json

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

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

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

TelemetryConfiguration.Active.DisableTelemetry = true;

Application Insights 通过模拟 TelemetryChannelTelemetryClient 进行了 example 单元测试。

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

我在使用 Josh Rostad 的 article 编写我的模拟 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);
}

基于此处的其他工作;

  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. 使用一个漂亮的小静态工厂class
    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 的所有可用方法(除了不应调用的任何方法。例如内部或已弃用)。

public class MyClass
{
    private readonly ITelemetryClient _telemetryClient;

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