在单元测试中使用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提供了一个通过模拟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);
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;
基于此处的其他工作;
- 创建通道-如果需要,您可以将其用于测试遥测
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()
{
}
}
- 使用一个不错的静态工厂类
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;
}
}
- 致电
MockTelemetryClient.Create()
获取您的TelemetryClient
- 利润
我的一位同事编写了这个有用的库,它引入了一些核心遥测类型(例如ITelemetryClient
和IMetric
)的抽象。
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");
}
}