Moq: модульное тестирование метода, основанного на HttpContext

Рассмотрим метод в сборке .NET:

public static string GetSecurityContextUserName()
{             
 //extract the username from request              
 string sUser = HttpContext.Current.User.Identity.Name;
 //everything after the domain     
 sUser = sUser.Substring(sUser.IndexOf("\\") + 1).ToLower();

 return sUser;      
}

Я хотел бы вызвать этот метод из модульного теста с использованием фреймворка Moq. Эта сборка является частью решения веб-форм. Модульный тест выглядит так, но мне не хватает кода Moq.

//arrange 
 string ADAccount = "BUGSBUNNY";
 string fullADName = "LOONEYTUNES\BUGSBUNNY"; 

 //act    
 //need to mock up the HttpContext here somehow -- using Moq.
 string foundUserName = MyIdentityBL.GetSecurityContextUserName();

 //assert
 Assert.AreEqual(foundUserName, ADAccount, true, "Should have been the same User Identity.");

Вопрос:

  • Как я могу использовать Moq для создания поддельного объекта HttpContext с некоторым значением, например MyDomain \ MyUser?
  • Как мне связать эту фальшивку с моим вызовом в мой статический метод на MyIdentityBL.GetSecurityContextUserName()?
  • У вас есть предложения по улучшению этого кода / архитектуры?

person p.campbell    schedule 31.07.2009    source источник


Ответы (7)


Webforms, как известно, нельзя тестировать именно по этой причине - большая часть кода может полагаться на статические классы в конвейере asp.net.

Чтобы проверить это с помощью Moq, вам необходимо реорганизовать ваш GetSecurityContextUserName() метод, чтобы использовать внедрение зависимостей с объектом HttpContextBase.

HttpContextWrapper находится в System.Web.Abstractions, который поставляется с .Net 3.5. Это оболочка для класса HttpContext и расширяет HttpContextBase, и вы можете построить HttpContextWrapper следующим образом:

var wrapper = new HttpContextWrapper(HttpContext.Current);

Более того, вы можете смоделировать HttpContextBase и настроить на него свои ожидания с помощью Moq. Включая вошедшего в систему пользователя и т. Д.

var mockContext = new Mock<HttpContextBase>();

Имея это на месте, вы можете вызывать GetSecurityContextUserName(mockContext.Object), и ваше приложение гораздо менее связано со статическим HttpContext WebForms. Если вы собираетесь проводить множество тестов, основанных на фиктивном контексте, я настоятельно рекомендую взять взгляните на класс MvcMockHelpers Скотта Хансельмана, у которого есть версия для использования с Moq. Он удобно выполняет множество необходимых настроек. И, несмотря на название, вам не нужно делать это с помощью MVC - я успешно использую его с приложениями веб-форм, когда я могу реорганизовать их для использования HttpContextBase.

person womp    schedule 31.07.2009
comment
Вы не можете использовать new HttpContextBase (), поскольку это абстрактный класс (как следует из названия). Вместо этого вам нужно использовать новый HttpContextWrapper (). - person Luke Bennett; 19.11.2009
comment
Спасибо ... Думаю, когда я ответил на этот вопрос, мой мозг опередил мои руки;) - person womp; 19.11.2009

Как правило, для модульного тестирования ASP.NET вместо доступа к HttpContext.Current необходимо иметь свойство типа HttpContextBase, значение которого устанавливается путем внедрения зависимости (например, в ответе, предоставленном Womp).

Однако для тестирования функций, связанных с безопасностью, я бы рекомендовал использовать Thread.CurrentThread.Principal (вместо HttpContext.Current.User). Использование Thread.CurrentThread имеет то преимущество, что его можно многократно использовать вне веб-контекста (и работает так же в веб-контексте, поскольку платформа ASP.NET всегда устанавливает одинаковые значения для обоих).

Чтобы затем протестировать Thread.CurrentThread.Principal, я обычно использую класс области видимости, который устанавливает для Thread.CurrentThread тестовое значение, а затем сбрасывает его при удалении:

using (new UserResetScope("LOONEYTUNES\BUGSBUNNY")) {
    // Put test here -- CurrentThread.Principal is reset when PrincipalScope is disposed
}

Это хорошо сочетается со стандартным компонентом безопасности .NET, где компонент имеет известный интерфейс (IPrincipal) и местоположение (Thread.CurrentThread.Principal), и будет работать с любым кодом, который правильно использует / проверяет Thread.CurrentThread.Principal .

Базовый класс области видимости будет примерно таким (при необходимости отрегулируйте такие вещи, как добавление ролей):

class UserResetScope : IDisposable {
    private IPrincipal originalUser;
    public UserResetScope(string newUserName) {
        originalUser = Thread.CurrentPrincipal;
        var newUser = new GenericPrincipal(new GenericIdentity(newUserName), new string[0]);
        Thread.CurrentPrincipal = newUser;
    }
    public IPrincipal OriginalUser { get { return this.originalUser; } }
    public void Dispose() {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
    protected virtual void Dispose(bool disposing) {
        if (disposing) {
            Thread.CurrentPrincipal = originalUser;
        }
    }
}

Другой альтернативой является вместо использования стандартного расположения компонентов безопасности напишите свое приложение, чтобы использовать введенные данные безопасности, например добавьте свойство ISecurityContext с помощью метода GetCurrentUser () или аналогичного, а затем используйте его последовательно во всем приложении - но если вы собираетесь делать это в контексте веб-приложения, вы также можете использовать предварительно созданный внедренный контекст , HttpContextBase.

person Sly Gryphon    schedule 30.01.2012

[TestInitialize]
public void TestInit()
{
  HttpContext.Current = new HttpContext(new HttpRequest(null, "http://tempuri.org", null), new HttpResponse(null));
}

Также вы можете moq, как показано ниже

var controllerContext = new Mock<ControllerContext>();
      controllerContext.SetupGet(p => p.HttpContext.Session["User"]).Returns(TestGetUser);
      controllerContext.SetupGet(p => p.HttpContext.Request.Url).Returns(new Uri("http://web1.ml.loc"));
person PUG    schedule 16.04.2014

Взгляните на этот http://haacked.com/archive/2007/06/19/unit-tests-web-code-without-a-web-server-using-httpsimulator.aspx

Используя класс httpSimulator, вы сможете передать HttpContext обработчику

HttpSimulator sim = new HttpSimulator("/", @"C:\intepub\?")
.SimulateRequest(new Uri("http://localhost:54331/FileHandler.ashx?
ticket=" + myticket + "&fileName=" + path));

FileHandler fh = new FileHandler();
fh.ProcessRequest(HttpContext.Current);

HttpSimulator реализует то, что нам нужно, чтобы получить экземпляр HttpContext. Таким образом, вам не нужно использовать здесь Moq.

person Davut Gürbüz    schedule 13.03.2012

Если вы используете модель безопасности CLR (как и мы), вам нужно будет использовать некоторые абстрактные функции для получения и установки текущего принципала, если вы хотите разрешить тестирование, и использовать их при получении или установке принципала. Это позволяет вам получить / установить принципала везде, где это необходимо (обычно в HttpContext в Интернете, и в текущем потоке в другом месте, например, в модульных тестах). Это выглядело бы примерно так:

public static IPrincipal GetCurrentPrincipal()
{
    return HttpContext.Current != null ?
        HttpContext.Current.User :
        Thread.CurrentThread.Principal;
}

public static void SetCurrentPrincipal(IPrincipal principal)
{
     if (HttpContext.Current != null) HttpContext.Current.User = principal'
     Thread.CurrentThread.Principal = principal;
}

Если вы используете настраиваемый принципал, они могут быть довольно хорошо интегрированы в его интерфейс, например, ниже Current вызовет GetCurrentPrincipal, а SetAsCurrent вызовет SetCurrentPrincipal.

public class MyCustomPrincipal : IPrincipal
{
    public MyCustomPrincipal Current { get; }
    public bool HasCurrent { get; }
    public void SetAsCurrent();
}
person Greg Beech    schedule 31.07.2009
comment
В этом коде нет необходимости, потому что, если вы поразмыслите над кодом ASP.NET, вы увидите, что HttpContext.Current.User и Thread.CurrentThread.Principal всегда имеют одно и то же значение, поэтому вам нужно только когда-либо проверять Thread.CurrentThread. Это даже видно в приведенном выше коде, где SetCurrentPrincipal всегда устанавливает Thread.CurrentPrincipal, а также обновляет HttpContext (если он существует) таким же образом; ни разу у них нет разных ценностей. (Другими словами, просто всегда используйте Thread.CurrentThread.) - person Sly Gryphon; 31.01.2012
comment
@Sly - Они такие же, кроме тех случаев, когда это не так. hanselman.com/blog/ - person Greg Beech; 31.01.2012

На самом деле это не связано с использованием Moq для модульного тестирования того, что вам нужно.

Обычно у нас на работе многоуровневая архитектура, где код на уровне представления на самом деле предназначен только для упорядочивания вещей для отображения в пользовательском интерфейсе. Такой код не покрывается модульными тестами. Вся остальная логика находится на бизнес-уровне, который не обязательно должен иметь какую-либо зависимость от уровня представления (т.е. ссылки на конкретные пользовательские интерфейсы, такие как HttpContext), поскольку пользовательский интерфейс также может быть приложением WinForms, а не обязательно веб-приложением. .

Таким образом, вы можете избежать возиться с фреймворками Mock, пытаясь имитировать HttpRequests и т. Д., Хотя часто это может быть необходимо.

person Juri    schedule 31.07.2009

В ASP.NET MVC Core я использую следующий код для тестирования контроллеров, которые зависят от HttpContext:

var controller = new HomeController();
controller.ControllerContext.HttpContext = new DefaultHttpContext();

Это образец модульного теста:

[Test]
public void Test_HomeController_Index()
{
    // Arrange
    var controller = new HomeController();
    controller.ControllerContext.HttpContext = new DefaultHttpContext();

    // Act
    var result = controller.Index();

    // Assert
    var viewResult = result as ViewResult;
    Assert.IsNotNull(viewResult);
}
person Svetlin Nakov    schedule 22.04.2021