unit testingASP.NET MVC5应用程序

我正在通过添加一个新属性来扩展ApplicationUser类(如教程中所示, 使用Facebook和Google OAuth2和OpenID登录创建一个ASP.NET MVC 5应用程序(C#) )

public class ApplicationUser : IdentityUser { public DateTime BirthDate { get; set; } } 

现在我想创建一个unit testing来validation我的AccountController是否正确保存了BirthDate。

我创建了一个名为TestUserStore的内存用户存储

 [TestMethod] public void Register() { // Arrange var userManager = new UserManager(new TestUserStore()); 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 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.d__40.MoveNext() in AccountController.cs: line 336 

在以前的版本中,ASP.NET MVC团队非常努力地使代码可以测试。 从表面上看,现在测试AccountController并不容易。 我有一些选择。

我可以

  1. 修改锅炉板代码,使其不调用扩展方法并在该级别处理此问题

  2. 设置OWin管道以进行测试

  3. 避免编写需要AuthN / AuthZ基础设施的测试代码(不是合理的选择)

我不确定哪条路更好。 任何人都可以解决这个问题。 我的问题归结为哪个是最好的策略。

注意:是的,我知道我不需要测试我没写的代码。 UserManager基础设施提供MVC5就是这样一个基础设施但是如果我想编写validation我对ApplicationUser的修改的测试或validation依赖于用户角色的行为的代码那么我必须使用UserManager进行测试。

我正在回答我自己的问题,所以如果你认为这是一个很好的答案,我可以从社区中得到一个感觉。

步骤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 testing以为Microsoft.OWin.IAuthenticationManager接口添加模拟

 [TestMethod] public void Register() { // Arrange var userManager = new UserManager(new TestUserStore()); var controller = new AccountController(userManager); controller.SetFakeControllerContext(); // Modify the test to setup a mock IAuthenticationManager var mockAuthenticationManager = new Mock(); 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); } 

现在测试通过了。

好主意? 馊主意?

我的需求很相似,但我意识到我不想要对我的AccountController进行纯粹的unit testing。 相反,我想在尽可能接近其自然栖息地的环境中进行测试(如果需要,可以进行集成测试)。 所以我不想嘲笑周围的物体,而是使用真实的物体,只需要我自己的代码,就像我可以侥幸逃脱一样。

HttpContextBaseExtensions.GetOwinContext方法也阻碍了我,所以我对Blisco的提示非常满意。 现在,我的解决方案中最重要的部分如下所示:

 ///  Set up an account controller with just enough context to work through the tests.  ///  The user manager to be used  /// A new account controller 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 owinEnvironment = new Dictionary() { {"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在ForgotPassword方法中生成一个正确的URL),但我现在已经涵盖了我的大部分需求。

我使用了类似于你的解决方案 – 模拟IAuthenticationManager – 但我的登录代码位于LoginManager类中,该类通过构造函数注入获取IAuthenticationManager。

  public LoginHandler(HttpContextBase httpContext, IAuthenticationManager authManager) { _httpContext = httpContext; _authManager = authManager; } 

我正在使用Unity来注册我的依赖项:

  public static void RegisterTypes(IUnityContainer container) { container.RegisterType( new InjectionFactory(_ => new HttpContextWrapper(HttpContext.Current))); container.RegisterType(new InjectionFactory(c => c.Resolve().GetOwinContext())); container.RegisterType( new InjectionFactory(c => c.Resolve().Authentication)); container.RegisterType(); // Further registrations here... } 

但是,我想测试我的Unity注册,这已经被certificate是棘手的,没有伪造(a)HttpContext.Current(足够硬)和(b)GetOwinContext() – 正如你所发现的那样,直接做不到。

我找到了Phil Haack的HttpSimulatorforms的解决方案以及对HttpContext的一些操作来创建一个基本的Owin环境 。 到目前为止,我发现设置一个虚拟的Owin变量足以使GetOwinContext()工作,但是YMMV。

 public static class HttpSimulatorExtensions { public static void SimulateRequestAndOwinContext(this HttpSimulator simulator) { simulator.SimulateRequest(); Dictionary owinEnvironment = new Dictionary() { {"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(); } Assert.IsNotNull(controller); } } 

如果您的SetFakeControllerContext()方法完成工作,HttpSimulator可能会过度杀伤,但它看起来像是集成测试的有用工具。