【问题标题】:How can you mock a dependency that is a concrete class if you don't control the code?如果您不控制代码,如何模拟作为具体类的依赖项?
【发布时间】:2019-03-30 01:45:18
【问题描述】:

我正在实现IHttpModule 并尝试为它编写单元测试(使用 NUnit 和 Moq)。我在模拟 Init 方法的 HttpApplication 依赖项时遇到问题:

void Init(HttpApplication context);

通常,ASP.NET 控制HttpApplication 实例并将其传递给Init 方法。在Init 方法中,自定义IHttpModule 订阅HttpApplication 实例发布的事件(如BeginRequestEndRequest)。

我需要一些方法来模拟 HttpApplication,以便我可以引发事件并测试我的 IHttpModule 事件处理程序的实现是否有效。

我尝试在我的测试中创建一个 Mock HttpApplication

// Mock for the logging dependency
var mockLogger = new Mock<ILogger>();

// My attempt at mocking the HttpApplication
var mockApplication = new Mock<HttpApplication>();

// MyModule is my class that implements IHttpModule
var myModule = new MyModule(mockLogger.Object);

// Calling Init, which subscribes my event handlers to the HttpApplication events
myModule.Init(mockApplication.Object);

// Attempting to raise the begin and end request events
mockApplication.Raise(a => a.BeginRequest += null, EventArgs.Empty);
mockApplication.Raise(a => a.EndRequest += null, EventArgs.Empty);

// RequestTime is a long property that tracks the time it took (in miliseconds) for a 
// request to be processed and is set in the event handler subscribed to EndRequest
Assert.Greater(myModule.RequestTime, 0);

...但它给出了以下错误消息:

表达式不是事件附加或分离,或者事件在类中声明但未标记为虚拟。

当我查看该错误时,我了解到 Moq 只能模拟接口和虚拟方法... 那么我如何模拟一个我无法控制的具体类?

这是MyModule 类:

public class MyModule : IHttpModule
{
    ILogger _logger;

    public long RequestTime { get; private set; }

    Stopwatch _stopwatch;

    public MyModule(ILogger logger)
    {
        _logger = logger;
    }

    public void Init(HttpApplication context)
    {
        context.BeginRequest += OnBeginRequest;
        context.EndRequest += OnEndRequest;
    }

    public void Dispose() { }

    void OnBeginRequest(object sender, EventArgs e)
    {
        _stopwatch = Stopwatch.StartNew();
    }

    void OnEndRequest(object sender, EventArgs e)
    {
        _stopwatch.Stop();
        RequestTime = _stopwatch.ElapsedMilliseconds;
    }
 }   

【问题讨论】:

  • 您可以尝试使用基于界面的解决方法。
  • @cdev 感谢您的回复,但我很难理解您的意思。你能更详细地解释一下吗?或者举个例子?
  • 将使用 HttpApplication 的方法移动到类和接口中,您可以将该类模拟为预期的行为。其实我更喜欢这种场景下的集成测试。
  • @cdev 我仍然不明白你的意思。 HttpApplication 是 Microsoft 在 System.Web 中的一个类。我不能让它继承一个新的接口或改变HttpApplication
  • 我想知道没有人建议编写集成测试。单元测试有很好的特性——如果你很难编写测试。架构不是为单元测试而设计的,如果你不能重新设计并且仍然想自动测试它 - 使用集成测试,它将覆盖你应用程序的整个管道。

标签: c# unit-testing nunit moq ihttpmodule


【解决方案1】:

我认为通常不建议这样做,但您可以使用反射来模拟非公共/虚拟成员。

对于当前的implementation,像这样

public class MockHttpApplication : HttpApplication
{
    public void RaiseBeginRequest()
    {
        FindEvent("EventBeginRequest").DynamicInvoke(this, EventArgs.Empty);
    }

    public void RaiseEndRequest()
    {
        FindEvent("EventEndRequest").DynamicInvoke(this, EventArgs.Empty);
    }

    private Delegate FindEvent(string name)
    {
        var key = typeof(HttpApplication)
                    .GetField(name, BindingFlags.Static | BindingFlags.NonPublic)
                    .GetValue(null);

        var events = typeof(HttpApplication)
                        .GetProperty("Events", BindingFlags.Instance | BindingFlags.NonPublic)
                        .GetValue(this) as EventHandlerList;

        return events[key];
    }
}

【讨论】:

  • 这使得单元测试工作,赞成。您能否稍微解释一下代码并详细说明您认为通常不推荐它的原因?
  • 查看HttpApplication 的内部实现,BeginRequestEndRequest 的事件处理程序保存在Events 属性中,其关键对象定义为私有字段。使用反射,您可以手动访问这些成员并触发事件。因为这些实现不是公共API,可能有一天会改变,所以我说不推荐。
猜你喜欢
  • 2021-04-26
  • 2021-12-13
  • 2017-09-30
  • 2020-10-17
  • 2017-01-08
  • 2015-03-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多