首页 文章

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

提问于
浏览
13

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

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

谢谢

5 回答

  • 7

    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 上定义一个(或者甚至是多个)抽象,它们是 especially tailored for your application . 所以不要试图用它可能的100种方法来模仿_1342443的API,而只是在控制器实际使用的接口上定义方法,并使它们尽可能简单,这样你就可以使控制器的代码更简单 - 和 - 你的单元测试更简单 .

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

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

    这里我假设 TelemetryClient 是线程安全的,可以作为单例工作 . 否则,您可以执行以下操作:

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

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

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

    Application Insights有example单元通过模拟 TelemetryChannel 来测试 TelemetryClient .

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

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

    如果你不想沿着抽象/包装路径走下去 . 在您的测试中,您可以简单地将AppInsights endpoints 定向到模拟轻量级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

  • 4

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

    TelemetryConfiguration.Active.DisableTelemetry = true;

相关问题