Как вы можете имитировать зависимость, которая является конкретным классом, если вы не управляете кодом?

#c# #unit-testing #nunit #moq #ihttpmodule

#c# #модульное тестирование #nunit #moq #ihttpmodule

Вопрос:

Я внедряю IHttpModule и пытаюсь написать модульные тесты для него (используя NUnit и Moq). У меня возникла проблема с имитацией HttpApplication зависимости для Init метода:

 void Init(HttpApplication context);
  

Обычно ASP.NET управляет HttpApplication экземпляром и передает его Init методу. В Init методе пользовательский IHttpModule подписывается на события, опубликованные HttpApplication экземпляром (например, BeginRequest и EndRequest ).

Мне нужен какой-то способ имитировать HttpApplication , чтобы я мог вызывать события и проверять, работает ли моя реализация IHttpModule обработчиков событий.

Я пытался создать макет 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;
    }
 }   
  

Комментарии:

1. Вы можете попробовать обойти это с помощью интерфейса.

2. @cdev Спасибо за ответ, но мне трудно понять, что вы имеете в виду. Можете ли вы объяснить это более подробно или на примере?

3. Переместите методы, используемые HttpApplication, в class и interface, и вы сможете имитировать ожидаемое поведение этого класса. На самом деле я предпочитаю интеграционное тестирование в сценариях, подобных этому.

4. @cdev Я все еще не понимаю, что вы имеете в виду. HttpApplication это класс от Microsoft в System.Web . Я не могу заставить его наследовать новый интерфейс или изменяться HttpApplication вообще.

5. Интересно, никто не предложил написать интеграционные тесты. Модульные тесты имеют хорошую особенность — если вам трудно писать тесты. Архитектура не предназначена для модульных тестов, если вы не можете изменить дизайн и все еще хотите автоматически протестировать его — используйте интеграционные тесты, которые охватят весь конвейер вашего приложения.

Ответ №1:

Я не думаю, что обычно это рекомендуется, но вы можете использовать отражение, чтобы имитировать непубличных / виртуальных членов.

Для текущей реализации, например, так

 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];
    }
}

  

Комментарии:

1. Это заставляет модульный тест работать, проголосовал за. Можете ли вы немного объяснить код и подробнее рассказать о том, почему, по вашему мнению, это обычно не рекомендуется?

2. Глядя на внутреннюю реализацию HttpApplication , обработчики событий BeginRequest и EndRequest хранятся в Events свойстве с ключевыми объектами, которые определены как частное поле. Используя отражение, вы можете получить доступ к этим элементам и запустить событие вручную. Конечно, эти реализации не являются общедоступными API и, возможно, когда-нибудь будут изменены, поэтому я сказал, что это не рекомендуется.