C# 单元测试 ASP.NET MVC5 应用程序

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/19624140/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-10 15:27:17  来源:igfitidea点击:

Unit Testing ASP.NET MVC5 App

c#asp.netunit-testingasp.net-mvc-5

提问by Ron Jacobs

I'm extending the ApplicationUser class by adding a new property (as shown in the tutorial Create an ASP.NET MVC 5 App with Facebook and Google OAuth2 and OpenID Sign-on (C#))

我通过添加一个新属性来扩展 ApplicationUser 类(如教程 Create an ASP.NET MVC 5 App with Facebook and Google OAuth2 and OpenID Sign-on (C#) 所示

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

Now I want to create a Unit Test to verify that my AccountController is correctly saving the BirthDate.

现在我想创建一个单元测试来验证我的 AccountController 是否正确保存了出生日期。

I've created an in-memory user store named TestUserStore

我创建了一个名为 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);
}

The controller.Register method is boilerplate code generated by MVC5 but for reference purposes I'm including it here.

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

When I call Register, it calls SignInAsync which is where the trouble will occur.

当我调用 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);
}

At the lowest layer, the boilerplate code includes this tidbit

在最低层,样板代码包括这个花絮

private IAuthenticationManager AuthenticationManager
{
    get
    {
        return HttpContext.GetOwinContext().Authentication;
    }
}

This is where the root of the problm occurs. This call to GetOwinContext is an extension method which I cannot mock and I cannot replace with a stub (unless of course I change the boilerplate code).

这是问题的根源发生的地方。这个对 GetOwinContext 的调用是一个扩展方法,我不能模拟它,也不能用存根替换(除非我更改样板代码)。

When I run this test I get an exception

当我运行此测试时,出现异常

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

In prior releases the ASP.NET MVC team worked very hard to make the code testable. It seems on the surface that now testing an AccountController is not going to be easy. I have some choices.

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

I can

我可以

  1. Modify the boiler plate code so that it doesn't call an extension method and deal with this problem at that level

  2. Setup the OWin pipeline for testing purposes

  3. Avoid writing testing code that requires the AuthN / AuthZ infrastructure (not a reasonable option)

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

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

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

I'm not sure which road is better. Either one could solve this. My question boils down to which is the best strategy.

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

Note: Yes, I know that I don't need to test code that I didn't write. The UserManager infrastructure provided MVC5 is such a piece of infrastructure BUT if I want to write tests that verify my modifications to ApplicationUser or code that verifies behavior that depends upon user roles then I must test using UserManager.

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

采纳答案by Ron Jacobs

I'm answering my own question so I can get a sense from the community if you think this is a good answer.

我正在回答我自己的问题,因此如果您认为这是一个很好的答案,我可以从社区中了解一些信息。

Step 1: Modify the generated AccountController to provide a property setter for the AuthenticationManager using a backing field.

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

Step 2:Modify the unit test to add a mock for the Microsoft.OWin.IAuthenticationManager interface

第二步:修改单元测试,为Microsoft.OWin.IAuthenticationManager接口添加一个mock

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

Now the test passes.

现在测试通过了。

Good idea? Bad idea?

好主意?馊主意?

回答by Blisco

I've used a solution similar to yours - mocking an IAuthenticationManager - but my login code is in a LoginManager class that takes the IAuthenticationManager via constructor injection.

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

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

I'm using Unityto register my dependencies:

我正在使用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...
    }

However, I'd like to test my Unity registrations, and this has proved tricky without faking (a) HttpContext.Current (hard enough) and (b) GetOwinContext() - which, as you've found, is impossible to do directly.

但是,我想测试我的 Unity 注册,事实证明,如果不伪造 (a) HttpContext.Current(足够难)和 (b) GetOwinContext(),这很棘手 - 正如您所发现的,这是不可能直接做到的.

I've found a solution in the form of Phil Haack's HttpSimulatorand some manipulation of the HttpContext to create a basic Owin environment. So far I've found that setting a single dummy Owin variable is enough to make GetOwinContext() work, but YMMV.

我找到了 Phil Haack 的HttpSimulator形式的解决方案,并对 HttpContext 进行了一些操作以创建基本的Owin 环境。到目前为止,我发现设置单个虚拟 Owin 变量足以使 GetOwinContext() 工作,但 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 may be overkill if your SetFakeControllerContext() method does the job, but it looks like a useful tool for integration testing.

如果您的 SetFakeControllerContext() 方法完成这项工作,HttpSimulator 可能会有点过头,但它看起来像是一个有用的集成测试工具。

回答by Pierre America

My needs are similar, but I realized that I don't want a pure unit test of my AccountController. Rather I want to test it in an environment that is as close as possible to its natural habitat (integration test, if you want). So I don't want to mock the surrounding objects, but use the real ones, with as little of my own code as I can get away with.

我的需求是相似的,但我意识到我不想对我的 AccountController 进行纯单元测试。相反,我想在尽可能接近其自然栖息地的环境中对其进行测试(集成测试,如果需要)。所以我不想模拟周围的物体,而是使用真实的物体,尽可能少地使用我自己的代码。

The HttpContextBaseExtensions.GetOwinContext method also got in my way, so I was very happy with Blisco's hint. Now the most important part of my solution looks like this:

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

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

I have not yet succeeded to get everything working (in particular, I could not get UrlHelper to produce a proper URL in the ForgotPassword method), but most of my needs are covered now.

我还没有成功地让一切正常工作(特别是,我无法让 UrlHelper 在 ForgotPassword 方法中生成正确的 URL),但现在我的大部分需求都得到了满足。