【问题标题】:Nunit Testing MVC SiteNunit 测试 MVC 站点
【发布时间】:2011-09-19 19:11:14
【问题描述】:

我在尝试对我拥有的 MVC 站点进行单元测试时遇到了一些问题:我需要运行大量 ASP.NET 环境(生成 httpcontexts、会话、cookie、成员资格等)全面测试一切。

即使是为了测试一些不那么前端的东西也需要会员资格才能正常工作,而手动欺骗这一切是很挑剔的。

有没有办法在 NUnit 测试中启动应用程序池?这似乎是最简单的方法。

【问题讨论】:

  • System.Web 引入了 HttpContextBase 等类,这些类是专门为允许您在单元测试期间进行模拟而创建的。您的代码应该使用基类的实例,然后模拟 那些

标签: c# asp.net testing nunit


【解决方案1】:

如果编写得当,您应该不需要真正的上下文、真正的会话、cookie 等。默认情况下,MVC 框架提供了一个可以模拟/存根的 HttpContext。我建议使用 Moq 或 Rhino Mocks 之类的模拟框架,并创建一个 MockHttpContext 类,该类创建一个模拟上下文,其中包含您需要针对设置进行测试的所有属性。这是一个使用Moq的模拟HttpContext@

/// <summary>
/// Mocks an entire HttpContext for use in unit tests
/// </summary>
public class MockHttpContextBase
{
    /// <summary>
    /// Initializes a new instance of the <see cref="MockHttpContextBase"/> class.
    /// </summary>
    public MockHttpContextBase() : this(new Mock<Controller>().Object, "~/")
    {
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="MockHttpContextBase"/> class.
    /// </summary>
    /// <param name="controller">The controller.</param>
    public MockHttpContextBase(Controller controller) : this(controller, "~/")
    {
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="MockHttpContextBase"/> class.
    /// </summary>
    /// <param name="url">The URL.</param>
    public MockHttpContextBase(string url) : this(new Mock<Controller>().Object, url)
    {              
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="MockHttpContextBase"/> class.
    /// </summary>
    /// <param name="controller">The controller.</param>
    /// <param name="url">The URL.</param>
    public MockHttpContextBase(ControllerBase controller, string url)
    {
        HttpContext = new Mock<HttpContextBase>();
        Request = new Mock<HttpRequestBase>();
        Response = new Mock<HttpResponseBase>();
        Output = new StringBuilder();

        HttpContext.Setup(x => x.Request).Returns(Request.Object);
        HttpContext.Setup(x => x.Response).Returns(Response.Object);
        HttpContext.Setup(x => x.Session).Returns(new FakeSessionState());

        Request.Setup(x => x.Cookies).Returns(new HttpCookieCollection());
        Request.Setup(x => x.QueryString).Returns(new NameValueCollection());
        Request.Setup(x => x.Form).Returns(new NameValueCollection());
        Request.Setup(x => x.ApplicationPath).Returns("~/");
        Request.Setup(x => x.AppRelativeCurrentExecutionFilePath).Returns(url);
        Request.Setup(x => x.PathInfo).Returns(string.Empty);

        Response.Setup(x => x.Cookies).Returns(new HttpCookieCollection());
        Response.Setup(x => x.ApplyAppPathModifier(It.IsAny<string>())).Returns((string path) => path);
        Response.Setup(x => x.Write(It.IsAny<string>())).Callback<string>(s => Output.Append(s));

        var requestContext = new RequestContext(HttpContext.Object, new RouteData());
        controller.ControllerContext = new ControllerContext(requestContext, controller);
    }

    /// <summary>
    /// Gets the HTTP context.
    /// </summary>
    /// <value>The HTTP context.</value>
    public Mock<HttpContextBase> HttpContext { get; private set; }

    /// <summary>
    /// Gets the request.
    /// </summary>
    /// <value>The request.</value>
    public Mock<HttpRequestBase> Request { get; private set; }

    /// <summary>
    /// Gets the response.
    /// </summary>
    /// <value>The response.</value>
    public Mock<HttpResponseBase> Response { get; private set; }

    /// <summary>
    /// Gets the output.
    /// </summary>
    /// <value>The output.</value>
    public StringBuilder Output { get; private set; }
}

/// <summary>
/// Provides Fake Session for use in unit tests
/// </summary>
public class FakeSessionState : HttpSessionStateBase
{
    /// <summary>
    /// backing field for the items in session
    /// </summary>
    private readonly Dictionary<string, object> _items = new Dictionary<string, object>();

    /// <summary>
    /// Gets or sets the <see cref="System.Object"/> with the specified name.
    /// </summary>
    /// <param name="name">the key</param>
    /// <returns>the value in session</returns>
    public override object this[string name]
    {
        get
        {
            return _items.ContainsKey(name) ? _items[name] : null;
        }
        set
        {
            _items[name] = value;
        }
    }
}

您可以进一步添加一些内容,例如 HTTP 标头集合,但希望它可以展示您可以做什么。

使用

var controllerToTest = new HomeController();
var context = new MockHttpContextBase(controllerToTest);

// do stuff that you want to test e.g. something goes into session

Assert.IsTrue(context.HttpContext.Session.Count > 0); 

关于会员资格提供者或其他提供者,您遇到了一些难以测试的问题。我将抽象接口背后提供程序的使用,以便您在测试依赖它的组件时可以为接口提供假冒。但是,您仍然会在对使用提供程序的接口的具体实现进行单元测试时遇到麻烦,但是您的里程可能会因您希望/必须在单元测试和代码覆盖方面走多远而有所不同。

【讨论】:

  • >"MVC框架默认提供了一个可以mock/stubbed的HttpContext。",我mock的时候会自动替换HttpContext吗?还是我必须使用抽象代码?主要问题是倾注了一年多的代码并在需要时将它们全部更改。
  • @StangeWill = casperOne 对您的问题的评论就是我所指的。如上所示,HttpContextBase 可以很容易地模拟。
【解决方案2】:

我不知道有什么方法可以做到这一点,因为您的代码不在该进程中,并且需要一个也不在 aspnet 中的主机。 (虽然之前我错了哈哈)

Phil Haack 有一个较旧的 HttpSimulator,你试过了吗?

http://haacked.com/archive/2007/06/19/unit-tests-web-code-without-a-web-server-using-httpsimulator.aspx

【讨论】:

  • 是的,我有,它有助于解决一些问题,但会员资格完全无法使用,web.config 没有加载并且是手动强制进入的(这可能是我的许多问题的根源) ,它要求我调整我的 HttpApplication 类,以便我可以强制上下文/会话进入 MVC 应用程序类。这里有很多强迫和潜在的破坏,我想知道是否有更好的方法。
  • 更好的方法是真正构建您的应用程序,以便您可以模拟包含您需要的信息的设置类。成员资格提供程序也是如此,您应该围绕其功能包装一个接口,然后您可以伪造该接口。如果您想查看它,MVC3 默认 Internet 应用程序模板会执行此操作。
  • ha.. 每个人基本上都和我最后的评论说的一样。我应该说'参考上面':)
【解决方案3】:

您需要为这些服务构建包装接口。最初的 MVC2 和 MV3 入门项目模板默认执行此操作,但出于某种原因,他们在最新版本中放弃了此操作。

您可以尝试查找原始 AccountController 代码的示例,为您提供一个起点。他们使用了 IMembershipService 和 IFormsAuthenticationService

模拟会话、上下文等相对简单。

【讨论】:

    【解决方案4】:

    看看 MVCContrib 项目 (http://mvccontrib.codeplex.com/),因为他们有一个帮助器,用于创建填充了所有各种上下文对象的控制器(如 HttpContext)。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-08-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多