Подтвердить что ты не робот

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

Я хотел бы назвать этот метод из unit test, используя фреймворк Moq. Эта сборка является частью решения webforms. unit test выглядит так, но мне не хватает кода 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()?
  • Есть ли у вас предложения по улучшению этого кода/архитектуры?
4b9b3361

Ответ 1

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), и ваше приложение гораздо менее связано с статическим WebForms HttpContext. Если вы собираетесь делать много тестов, которые полагаются на насмешливый контекст, я предлагаю взглянуть на класс Scott Hanselman MvcMockHelpers, который имеет версию для использования с Moq. Он удобно обрабатывает множество необходимых настроек. И, несмотря на название, вам не нужно делать это с помощью MVC - я использую его успешно с приложениями веб-форм, когда я могу реорганизовать их для использования HttpContextBase.

Ответ 2

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

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

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

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.

Ответ 3

Взгляните на это 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.

Ответ 4

[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"));

Ответ 5

Если вы используете модель безопасности 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();
}

Ответ 6

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

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

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