Тестирование модуля ASP.NET MVC5 App
Я расширяю класс ApplicationUser, добавляя новое свойство (как показано в учебнике
Создать приложение ASP.NET MVC 5 с Facebook и Google OAuth2 и OpenID Sign-on (С#))
public class ApplicationUser : IdentityUser
{
public DateTime BirthDate { get; set; }
}
Теперь я хочу создать Unit Test, чтобы убедиться, что мой AccountController правильно сохраняет BirthDate.
Я создал хранилище пользователей в памяти с именем TestUserStore
[TestMethod]
public void Register()
{
// Arrange
var userManager = new UserManager<ApplicationUser>(new TestUserStore<ApplicationUser>());
var controller = new AccountController(userManager);
// This will setup a fake HttpContext using Moq
controller.SetFakeControllerContext();
// Act
var result =
controller.Register(new RegisterViewModel
{
BirthDate = TestBirthDate,
UserName = TestUser,
Password = TestUserPassword,
ConfirmPassword = TestUserPassword
}).Result;
// Assert
Assert.IsNotNull(result);
var addedUser = userManager.FindByName(TestUser);
Assert.IsNotNull(addedUser);
Assert.AreEqual(TestBirthDate, addedUser.BirthDate);
}
Метод controller.Register - это шаблонный код, сгенерированный MVC5, но для справочных целей я включаю его здесь.
// POST: /Account/Register
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Register(RegisterViewModel model)
{
if (ModelState.IsValid)
{
var user = new ApplicationUser() { UserName = model.UserName, BirthDate = model.BirthDate };
var result = await UserManager.CreateAsync(user, model.Password);
if (result.Succeeded)
{
await SignInAsync(user, isPersistent: false);
return RedirectToAction("Index", "Home");
}
else
{
AddErrors(result);
}
}
// If we got this far, something failed, redisplay form
return View(model);
}
Когда я вызываю Register, он вызывает SignInAsync, в котором возникнет проблема.
private async Task SignInAsync(ApplicationUser user, bool isPersistent)
{
AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie);
var identity = await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie);
AuthenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent }, identity);
}
В самом нижнем слое код шаблона включает этот лакомый кусочек
private IAuthenticationManager AuthenticationManager
{
get
{
return HttpContext.GetOwinContext().Authentication;
}
}
Здесь происходит корень проблемы. Этот вызов GetOwinContext - это метод расширения, который я не могу высмеять, и я не могу заменить заглушкой (если, конечно, я не изменяю шаблонный код).
Когда я запускаю этот тест, я получаю исключение
Test method MVCLabMigration.Tests.Controllers.AccountControllerTest.Register threw exception:
System.AggregateException: One or more errors occurred. ---> System.NullReferenceException: Object reference not set to an instance of an object.
at System.Web.HttpContextBaseExtensions.GetOwinEnvironment(HttpContextBase context)
at System.Web.HttpContextBaseExtensions.GetOwinContext(HttpContextBase context)
at MVCLabMigration.Controllers.AccountController.get_AuthenticationManager() in AccountController.cs: line 330
at MVCLabMigration.Controllers.AccountController.<SignInAsync>d__40.MoveNext() in AccountController.cs: line 336
В предыдущих выпусках команда ASP.NET MVC работала очень усердно, чтобы проверить код. На первый взгляд кажется, что теперь проверка AccountController не будет простой. У меня есть выбор.
Я могу
-
Измените код плиты котла, чтобы он не вызывал метод расширения и не справлялся с этой проблемой на этом уровне
-
Настройка конвейера OWin для целей тестирования
-
Избегайте писать код тестирования, требующий инфраструктуры AuthN/AuthZ (не разумный вариант)
Я не уверен, какая дорога лучше. Либо можно решить это. Мой вопрос сводится к тому, что является лучшей стратегией.
Примечание. Да, я знаю, что мне не нужно проверять код, который я не писал. Инфраструктура UserManager, предоставляемая MVC5, является такой частью инфраструктуры, но если я хочу написать тесты, которые подтверждают мои изменения в ApplicationUser или код, который проверяет поведение, зависящее от ролей пользователя, тогда я должен протестировать с помощью UserManager.
Ответы
Ответ 1
Я отвечаю на свой вопрос, поэтому я могу понять смысл сообщества, если вы считаете, что это хороший ответ.
Шаг 1. Измените сгенерированный AccountController, чтобы предоставить установщик свойств для AuthenticationManager с использованием поля поддержки.
// Add this private variable
private IAuthenticationManager _authnManager;
// Modified this from private to public and add the setter
public IAuthenticationManager AuthenticationManager
{
get
{
if (_authnManager == null)
_authnManager = HttpContext.GetOwinContext().Authentication;
return _authnManager;
}
set { _authnManager = value; }
}
Шаг 2:
Измените unit test, чтобы добавить макет для интерфейса Microsoft.OWin.IAuthenticationManager
[TestMethod]
public void Register()
{
// Arrange
var userManager = new UserManager<ApplicationUser>(new TestUserStore<ApplicationUser>());
var controller = new AccountController(userManager);
controller.SetFakeControllerContext();
// Modify the test to setup a mock IAuthenticationManager
var mockAuthenticationManager = new Mock<IAuthenticationManager>();
mockAuthenticationManager.Setup(am => am.SignOut());
mockAuthenticationManager.Setup(am => am.SignIn());
// Add it to the controller - this is why you have to make a public setter
controller.AuthenticationManager = mockAuthenticationManager.Object;
// Act
var result =
controller.Register(new RegisterViewModel
{
BirthDate = TestBirthDate,
UserName = TestUser,
Password = TestUserPassword,
ConfirmPassword = TestUserPassword
}).Result;
// Assert
Assert.IsNotNull(result);
var addedUser = userManager.FindByName(TestUser);
Assert.IsNotNull(addedUser);
Assert.AreEqual(TestBirthDate, addedUser.BirthDate);
}
Теперь тест проходит.
Хорошая идея? Плохая идея?
Ответ 2
Мои потребности похожи, но я понял, что мне не нужен чистый unit test моего AccountController. Скорее, я хочу протестировать его в среде, максимально приближенной к ее естественной среде обитания (если вам нужен тест интеграции). Поэтому я не хочу издеваться над окружающими объектами, но использую настоящие, с небольшим количеством собственного кода, так как я могу сойти с рук.
Метод HttpContextBaseExtensions.GetOwinContext также мешал мне, поэтому я был очень доволен намеком Блиско.
Теперь самая важная часть моего решения выглядит так:
/// <summary> Set up an account controller with just enough context to work through the tests. </summary>
/// <param name="userManager"> The user manager to be used </param>
/// <returns>A new account controller</returns>
private static AccountController SetupAccountController(ApplicationUserManager userManager)
{
AccountController controller = new AccountController(userManager);
Uri url = new Uri("https://localhost/Account/ForgotPassword"); // the real string appears to be irrelevant
RouteData routeData = new RouteData();
HttpRequest httpRequest = new HttpRequest("", url.AbsoluteUri, "");
HttpResponse httpResponse = new HttpResponse(null);
HttpContext httpContext = new HttpContext(httpRequest, httpResponse);
Dictionary<string, object> owinEnvironment = new Dictionary<string, object>()
{
{"owin.RequestBody", null}
};
httpContext.Items.Add("owin.Environment", owinEnvironment);
HttpContextWrapper contextWrapper = new HttpContextWrapper(httpContext);
ControllerContext controllerContext = new ControllerContext(contextWrapper, routeData, controller);
controller.ControllerContext = controllerContext;
controller.Url = new UrlHelper(new RequestContext(contextWrapper, routeData));
// We have not found out how to set up this UrlHelper so that we get a real callbackUrl in AccountController.ForgotPassword.
return controller;
}
Мне все еще не удалось все работать (в частности, я не смог заставить UrlHelper создать правильный URL-адрес в методе ForgotPassword), но большинство моих потребностей теперь рассматриваются.
Ответ 3
Я использовал решение, подобное вашему, - издеваясь над IAuthenticationManager, но мой код входа находится в классе LoginManager, который принимает IAuthenticationManager через инъекцию конструктора.
public LoginHandler(HttpContextBase httpContext, IAuthenticationManager authManager)
{
_httpContext = httpContext;
_authManager = authManager;
}
Я использую Unity для регистрации моих зависимостей:
public static void RegisterTypes(IUnityContainer container)
{
container.RegisterType<HttpContextBase>(
new InjectionFactory(_ => new HttpContextWrapper(HttpContext.Current)));
container.RegisterType<IOwinContext>(new InjectionFactory(c => c.Resolve<HttpContextBase>().GetOwinContext()));
container.RegisterType<IAuthenticationManager>(
new InjectionFactory(c => c.Resolve<IOwinContext>().Authentication));
container.RegisterType<ILoginHandler, LoginHandler>();
// Further registrations here...
}
Тем не менее, я бы хотел проверить свои регистрации Unity, и это оказалось сложным без подделки (a) HttpContext.Current(достаточно сложно) и (b) GetOwinContext() - который, как вы нашли, невозможно делать напрямую.
Я нашел решение в форме Phil Haack HttpSimulator и некоторые манипуляции с HttpContext для создания базового Owin environment. До сих пор я обнаружил, что для установки GetOwinContext() достаточно установить одиночную переменную Owin, но YMMV.
public static class HttpSimulatorExtensions
{
public static void SimulateRequestAndOwinContext(this HttpSimulator simulator)
{
simulator.SimulateRequest();
Dictionary<string, object> owinEnvironment = new Dictionary<string, object>()
{
{"owin.RequestBody", null}
};
HttpContext.Current.Items.Add("owin.Environment", owinEnvironment);
}
}
[TestClass]
public class UnityConfigTests
{
[TestMethod]
public void RegisterTypes_RegistersAllDependenciesOfHomeController()
{
IUnityContainer container = UnityConfig.GetConfiguredContainer();
HomeController controller;
using (HttpSimulator simulator = new HttpSimulator())
{
simulator.SimulateRequestAndOwinContext();
controller = container.Resolve<HomeController>();
}
Assert.IsNotNull(controller);
}
}
HttpSimulator может быть излишним, если ваш метод SetFakeControllerContext() выполняет задание, но он выглядит как полезный инструмент для тестирования интеграции.