【问题标题】:Using Application Insights with Unit Tests?将 Application Insights 与单元测试一起使用?
【发布时间】:2016-02-22 06:35:33
【问题描述】:

我有一个 MVC Web 应用程序,并且我正在使用 Simple Injector for 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 是否足够聪明,可以知道它正在单元测试中运行,而不是发送数据?

对此问题的任何“见解”将不胜感激!

谢谢

【问题讨论】:

  • 我无法帮助解决问题的 AI 方面,但只需注册一个针对特定构造函数的委托即可完成注册:container.Register(() =&gt; new TelemetryClient(/*whatever constructor you want to target*/), requestOrTransientLifestyle);。另请查看DefaultScopedLifestyle
  • 对于单元测试,您应该真正定义自己的 TelemetryClient 抽象,您可以根据需要进行模拟。单元测试不应该与 Azure 对话。
  • 您的自定义混合范围让我担心。将 Web 请求生活方式与短暂的生活方式混合在一起通常不是一个好的做法。它可以解释为什么你需要这种混合生活方式?
  • @Steven - 创建混合范围是为了处理 signalR 需要访问控制器正在使用的相同服务,但该服务已经超出范围。这是由另一位开发人员完成的,由于我的应用未使用 signalR,因此我已将其删除。

标签: c# unit-testing azure simple-injector azure-application-insights


【解决方案1】:

Application Insights 有一个 example 通过模拟 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);

【讨论】:

  • 运行时代码你会用什么?我尝试使用ServerTelemetryChannel,但收到跟踪遥测“服务器遥测通道未初始化。因此永久存储已关闭。您需要调用 ServerTelemetryChannel.Initialize()。”但是Initialize() 上的智能感知说它是供内部使用的。
  • 更正:但Initialize() 接受TelemetryConfiguration,这是我们在这里尝试创建的。
  • @TsahiAsher,我建议为此发布一个新问题。
  • stackoverflow.com/a/33820340/141022 有一个有说服力的论据,可以围绕 3rd 方依赖创建合理的抽象,值得注意的是 AppInsights 团队(尽管是旧帖子)也建议使用通道路由 github.com/Microsoft/ApplicationInsights-dotnet/issues/342(在至少目前,他们可能正在考虑新的方法)。
【解决方案2】:

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

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

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

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

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

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

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

不太可能。如果你想测试依赖于这个TelemetryClient 的类,你最好使用一个假实现,以防止你的单元测试变得脆弱、缓慢或污染你的 Insight 数据。但即使测试不是问题,根据Dependency Inversion Principle,您也应该依赖(1)由您自己的应用程序定义的(2)抽象。使用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());

【讨论】:

  • 感谢您的详细回答。正如史蒂夫和@qujck 所建议的那样,我的主要问题得到了一个简单的回答,即用我选择的构造函数注册遥测客户端(尽管史蒂夫的回答有一个错字——container.Register 是重复的)。在发布这个问题后,我对单元测试问题得出了几乎相同的解决方案——将 TelemetryClient 包装在我自己设计的一个简单类中,公开我需要的一个方法,为其创建一个接口等。然后在单元测试中,那个简单的包装类是用 Moq 模拟的。
【解决方案3】:

使用 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);
}

【讨论】:

  • 感谢您提供这个非常干净且易于实施的解决方案!另外感谢您对答案的不聪明和友好的措辞。每个其他答案都有一些我不明白的“容器”代码:我的单元测试从来没有任何“容器”来注册类型。这个答案应该是公认的答案。
  • 没错。我同意这个答案更直接。我也使用非常相似的方法取得了成功。
【解决方案4】:

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

appInsightsSettings.json

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

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

【讨论】:

    【解决方案5】:

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

    TelemetryConfiguration.Active.DisableTelemetry = true;

    【讨论】:

    • 这需要使用 Microsoft.ApplicationInsights.Extensibility,但我们应该注意 TelemetryConfiguration.Active 在 .Net Core 中已过时。
    【解决方案6】:

    基于这里的其他工作;

    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. 利润

    【讨论】:

      猜你喜欢
      • 2021-09-18
      • 2011-01-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-10-26
      • 2014-04-09
      • 1970-01-01
      相关资源
      最近更新 更多