【问题标题】:How do I unit test a HTML Helper that does a call to an Action?如何对调用 Action 的 HTML Helper 进行单元测试?
【发布时间】:2013-11-15 22:01:09
【问题描述】:

我有一个用于基本 CMS 系统的 HTML Helper,它调用特定内容对象的控制器和操作。下面是一个精简的例子:

public static MvcHtmlString RenderCMSObject(this HtmlHelper helper, CMSObject cmsObject)
{
    var actionName = string.IsNullOrEmpty(cmsObject.ActionName)
        ? "Index"
        : cmsObject.ActionName;

    var controllerName = string.IsNullOrEmpty(cmsObject.ControllerName)
        ? "Default"
        : cmsObject.ControllerName;

    return helper.Action(actionName, controllerName);
}

如何进行单元测试?

我可以创建一个 HtmlHelper,并模拟它的某些方面(例如 ViewBag)来测试返回字符串的 HTML 助手(例如)。

对于上面的例子,它只需要一个可以模拟 Action 方法的测试来返回特定的东西,这样就可以断言它正在被正确的参数调用。

深入研究 Action 的来源,看起来测试必须是更复杂的集成测试,并模拟路由、Web 请求和控制器本身。

【问题讨论】:

  • 你在测试什么行为?
  • HTML 助手将使用正确的动作名称和控制器名称执行动作。实际的辅助方法比示例更复杂,但我对其进行了简化以仅显示需要断言的区域。
  • 通常存在单元测试来验证一些行为,而不是简单的参数传递。这就是我们通常不测试自动属性的原因。我们只是假设它们有效。集成测试将涵盖这一点。
  • 帮助器的行为包括一些空检查和默认的特定动作和控制器。检查它是否正确默认的单元测试将验证单元的行为。
  • 理想情况下,但只有一个单元测试,Action 行会失败。 HtmlHelper 是不可模拟的,因此它会尝试通过 MVC 中的 httpContext.Server.Execute 来执行 Action。

标签: c# asp.net unit-testing mocking moq


【解决方案1】:

没有简单的方法来测试 helper.Action 是否使用正确的参数调用。这是因为正如一些 cmets 指出的那样,HtmlHelper 也不是可模拟的,Action 也是静态扩展。但是您可以使用变通方法测试特定场景,例如根据您的问题。

它只需要一个可以模拟 Action 方法返回的测试 特别是某些东西,因此可以断言它正在被调用 正确的参数。

虽然由于静态特性,这并不完全可能,但我们可以使用不同的技术来验证是否使用正确的参数调用了 Action。

这只是指导,我还没有完全测试我的代码。我知道它可以在测试环境中工作,但我不完全确定这会在生产代码执行期间产生正确的结果。如果不是,我很乐意再看一遍..

首先,我将创建一个允许我覆盖 helper.Action 方法的类。

   public class HtmlHelperActionInvoker {
    public virtual MvcHtmlString InvokeAction(HtmlHelper helper, string action, string controller)      {
        return helper.Action(action, controller);
    }
}

然后在您的 Html 助手类中,我将添加静态构造函数和一个委托/函数,它允许我在测试执行期间设置我自己的 HtmlHelpActionInvoker 版本。

 public static class SomeHtmlHelperClass
 {
    static SomeHelperClass() {
        HtmlHelperActionFunc = () => new HtmlHelperActionInvoker();
    }

    public static Func<HtmlHelperActionInvoker> HtmlHelperActionFunc { get; set; }

    public static MvcHtmlString RenderCMSObject(this HtmlHelper helper, CMSObject cmsObject)
    {
        var actionName = string.IsNullOrEmpty(cmsObject.ActionName)
            ? "Index"
            : cmsObject.ActionName;

        var controllerName = string.IsNullOrEmpty(cmsObject.ControllerName)
            ? "Default"
            : cmsObject.ControllerName;

        var helperAction = HtmlHelperActionFunc();
        return helperAction.InvokeAction(helper, actionName, controllerName);
    }
  }

我相信静态 ctro 会在生产代码执行期间执行一次并设置一次 HtmlHelperActionInvoker 实例。后续请求将重用此实例。 (正如我之前提到的,您可能需要对此进行更多测试)

现在是单元测试

我们将使用一种称为Extract & Override 的技术。

在您的测试区域中创建 HtmlHelperActionInvoker 的可测试版本。这不是假对象/存根/模拟。这是 HtmlHelperActionInvoker 的可测试版本。并且只使用一个方法调用了预期的参数。

public class TesatableHtmlHelperAction : HtmlHelperActionInvoker
{
    public string Controller { get; set; }
    public string Action { get; set; }
    public override MvcHtmlString InvokeAction(HtmlHelper helper, string action, string controller) {
        Action = action;
        Controller = controller;

        return new MvcHtmlString("");
    }
}

在单元测试中,我们将设置可测试的 TesatableHtmlHelperAction,以便 SUT(被测系统)执行可测试的版本。 (注意:它仍然被认为是真实的,因为我们覆盖了真正的 HtmlHelperActionInvoker 的行为)

    [TestMethod]
    public void HtmlHelperActionRenderCMSObject_Execute_EnsureInvokeActionCalledWithExpectedControlerAndActionName()
    {            
        //Arrange
        var fakecmsObject = new CMSObject() { ActionName = "foo", ControllerName = "bar" };
        var testableHtmlHelperAction = new TesatableHtmlHelperAction();
        SomeHelperClass.HtmlHelperActionFunc = () => testableHtmlHelperAction;

        // Act
        SomeHelperClass.RenderCMSObject(null, fakecmsObject);

        // Verify
        Assert.AreEqual<string>(fakecmsObject.ActionName, testableHtmlHelperAction.Action);
        Assert.AreEqual<string>(fakecmsObject.ControllerName, testableHtmlHelperAction.Controller);
    }

【讨论】:

  • 谢谢,我喜欢这个解决方案,我已经实现了它并用它来创建一个类似的解决方案,但不需要代​​表我将作为另一种替代机制发布。
【解决方案2】:

我一直在考虑您的解决方案,虽然其他答案确实解决了问题,但令我震惊的是,您正在测试的行为可以简单地重构为它自己的可测试方法。

public static GetActionNameWithDefault(CMSObject cmsObject)
{
    return string.IsNullOrEmpty(cmsObject.ActionName)
        ? "Index"
        : cmsObject.ActionName;
}

public static GetControllerNameWithDefault(CMSObject cmsObject)
{
    return string.IsNullOrEmpty(cmsObject.ControllerName)
        ? "Default"
        : cmsObject.ControllerName
}

public static MvcHtmlString RenderCMSObject(this HtmlHelper helper, CMSObject cmsObject)
{
    return helper.Action(
        helper, 
        GetActionNameWithDefault(cmsObject), 
        GetControllerNameWithDefault(cmsObject));
}

【讨论】:

  • 这是一个很好的观点,我认为这将是一个简单助手的最佳选择。但是,如果帮助器稍微复杂一些,这将成为一个问题,例如有一个帮助器接收对象列表并对它们全部调用 Action 并以单个 MvcHtmlString 的形式返回。可以选择能够模拟辅助 Action 使其可以灵活地用于更多用例。
【解决方案3】:

在实现 Spock 的答案后,我想出了另一个类似的替代方案,但它不使用委托,而是使用单例和调用者的接口。它还使用 Moq 而不是 HtmlHelperActionInvoker 的扩展。

助手类

使用一个静态构造函数和一个公共属性,该属性包含一个调用程序类的单例,该调用程序类封装了 Action 调用。

public static class SomeHtmlHelperClass
{
    static SomeHtmlHelperClass()
    {
        ActionInvoker = new HtmlHelperActionInvoker();
    }

    public static IHtmlHelperActionInvoker ActionInvoker { get; set; }

    public static MvcHtmlString RenderCMSObject(this HtmlHelper helper, CMSObject cmsObject)
    {
        var actionName = string.IsNullOrEmpty(cmsObject.ActionName)
            ? "Index"
            : cmsObject.ActionName;

        var controllerName = string.IsNullOrEmpty(cmsObject.ControllerName)
            ? "Default"
            : cmsObject.ControllerName;

        return ActionInvoker.Action(helper, actionName, controllerName, cmsObject);
    }
}

调用者接口和类

调用helper的Action方法的简单接口和类,具体很基础,不需要测试。

public interface IHtmlHelperActionInvoker
{
    MvcHtmlString Action(HtmlHelper helper, string action, string controller, CMSObject model);
}

public class HtmlHelperActionInvoker : IHtmlHelperActionInvoker
{
    public MvcHtmlString Action(HtmlHelper helper, string action, string controller, CMSObject model)
    {
        return helper.Action(action, controller, model);
    }
}

单元测试

使用 Moq 模拟调用程序以返回一个字符串,然后在最后断言。在执行助手之前,通过属性注入将模拟类注入助手中。

[TestFixture]
public class HelperTests
{
    [Test]
    public void GivenACmsObjectWithCompletedActionAndController_WhenRenderCMSObject_ThenExpectedActionOutcomeforActionAndControllerIsGiven()
    {
        const string actionName = "foo";
        const string controllerName = "bar";
        const string expectedOutcome = "<h1>Bruce</h1>";

        // Arrange
        var cmsObject = new CMSObject { ActionName = actionName, ControllerName = controllerName };

        var mockInvoker = new Mock<IHtmlHelperActionInvoker>();
        mockInvoker.Setup(x => x.Action(null, actionName, controllerName, cmsObject)).Returns(MvcHtmlString.Create(expectedOutcome));
        SomeHtmlHelperClass.ActionInvoker = mockInvoker.Object;

        // Act
        var result = SomeHtmlHelperClass.RenderCMSObject(null, cmsObject);

        // Verify
        Assert.That(result.ToString(), Is.EqualTo(expectedOutcome));
    }
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2015-06-08
    • 2011-06-26
    • 1970-01-01
    • 1970-01-01
    • 2012-01-08
    • 2017-07-24
    • 2015-11-29
    • 1970-01-01
    相关资源
    最近更新 更多