2013-11-15 37 views
1

我有一个基本的CMS系统中使用的HTML Helper,它调用控制器和特定内容对象的动作。一个减少的例子如下:如何单元测试执行调用Action的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请求和控制器本身。

+0

你测试了什么行为? –

+0

HTML帮助程序将使用正确的操作名称和控制器名称执行操作。实际的帮手方法比示例更复杂,但我简化了它以仅显示需要断言的区域。 – makit

+0

通常情况下,单元测试用于验证除简单参数传递之外的某些行为。这就是为什么我们通常不测试自动属性的原因;我们只是假设他们工作。集成测试将涵盖这一点。 –

回答

2

有没有简单的方法来测试helper.Action它是否调用了正确的参数。这是因为一些评论指出HtmlHelper不是可以嘲笑的,Action也是一个静态扩展。但是你可以使用一个解决方法来测试特定的场景,例如根据你的问题。

它只是需要一个测试,可能会嘲笑操作方法返回 什么特别,因此可以断言它被称为与 正确的参数。

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

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

首先我会创建一个类,允许我重写helper.Action方法。

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

然后在你的HTML辅助类,我想补充的静态构造函数和一个代表/ FUNC,让我在测试执行过程中设置自己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将执行一次,producion代码执行过程中,一旦设定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); 
    } 
+0

谢谢,我喜欢这个解决方案,我已经实现了这一点,并用它来创建一个类似的解决方案,但没有委托要求我将作为另一种替代机制发布。 – makit

0

实施Spock的回答后,我想出了另一种选择类似,但使用的代表,它使用一个单例和一个调用者的接口。它也使用Moq而不是HtmlHelperActionInvoker的扩展。

Helper类

利用静态构造函数和公共财产拿着一个包装动作调用一个调用类的单例。

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

祈求接口和类

调用助手的操作方法简单的界面和类,具体是非常基本的,因此不需要进行测试。

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

单元测试

嘲笑起订量使用返回一个字符串,然后在最后断言调用。模拟类在辅助程序执行前通过属性注入注入帮助程序。

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

这是一个很好的观点,我认为这对于简单的帮手来说是最好的选择。如果帮助程序稍微复杂一些,它会成为一个问题,例如有一个帮助程序接收一个对象列表并将它们全部调用Action并作为单个MvcHtmlString返回。有一个选项可以模拟助手Action使其更灵活,可用于更多用例。 – makit

相关问题