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()
?
- Есть ли у вас предложения по улучшению этого кода/архитектуры?
Ответы
Ответ 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 и т.д., хотя часто это может быть необходимо.