如何测试抽象类的受保护的抽象方法?

3

我一直在研究如何测试一个名为TabsActionFilter的抽象类的最佳方法。我已经保证了继承自TabsActionFilter的类将拥有一个名为GetCustomer的方法。在实践中,这种设计似乎运作良好。

我的问题在于,我不知道如何测试基类的OnActionExecuted方法。此方法依赖于受保护的抽象GetCustomer方法的实现。我曾尝试使用Rhino Mocks模拟类,但似乎无法模拟从GetCustomer返回一个虚假的客户。很明显,将该方法转换为公共方法将使模拟可用,但由于protected是更适当的访问级别,所以并未采取此方式。

目前,在我的测试类中,我添加了一个具体的私有类,该类继承自TabsActionFilter并返回一个虚假的Customer对象。

  • 具体类是唯一的选项吗?
  • 有我所忽略的简单的模拟机制可以允许Rhino Mocks提供GetCustomer的返回值吗?

作为注释Anderson Imes在关于Moq的答案中谈到了他的意见,我可能会遗漏一些关键的信息,但它似乎不适用于这里。

需要测试的类:

public abstract class TabsActionFilter : ActionFilterAttribute
{
    public override void OnActionExecuted(ActionExecutedContext filterContext)
    {
        Customer customer = GetCustomer(filterContext);

        List<TabItem> tabItems = new List<TabItem>();
        tabItems.Add(CreateTab(customer, "Summary", "Details", "Customer",
            filterContext));
        tabItems.Add(CreateTab(customer, "Computers", "Index", "Machine",
            filterContext));
        tabItems.Add(CreateTab(customer, "Accounts", "AccountList",
            "Customer", filterContext));
        tabItems.Add(CreateTab(customer, "Actions Required", "Details",
            "Customer", filterContext));

        filterContext.Controller.ViewData.PageTitleSet(customer.CustMailName);
        filterContext.Controller.ViewData.TabItemListSet(tabItems);
    }

    protected abstract Customer GetCustomer(ActionExecutedContext filterContext);
}

"Mocking"的测试类和私有类

public class TabsActionFilterTest
{
    [TestMethod]
    public void CanCreateTabs()
    {
        // arrange
        var filterContext = GetFilterContext(); //method omitted for brevity

        TabsActionFilterTestClass tabsActionFilter =
            new TabsActionFilterTestClass();

        // act
        tabsActionFilter.OnActionExecuted(filterContext);

        // assert
        Assert.IsTrue(filterContext.Controller.ViewData
            .TabItemListGet().Count > 0);
    }

    private class TabsActionFilterTestClass : TabsActionFilter
    {
        protected override Customer GetCustomer(
            ActionExecutedContext filterContext)
        {
            return new Customer
            {
                Id = "4242",
                CustMailName = "Hal"
            };
        }
    }
}
2个回答

2
我认为你目前遇到的问题是你的类不可测试或者不够可测试。当然,这是在假设你已经正确识别出了GetCustomer确实需要被模拟以便能够正确地进行隔离测试。
如果GetCustomer是一个需要被模拟以便能够正确地隔离和测试TabsActionFilter的东西,那么你需要将GetCustomer的实现作为类的组成部分而不是继承方法。最常见的实现方式是使用控制反转/依赖注入。
当然,你也可以使用TypeMock之类的工具来实现这一点。但是,当你遇到这种难以测试的情况时,通常意味着你的类有太多的责任,需要将其分解为更小的组件。
(就个人而言,我不喜欢使用TypeMock。)

Phil,GetCustomer方法的实现是由继承类负责的。不幸的是,通过构造函数进行依赖注入在这里行不通,因为TabsActionFilter继承自ActionFilterAttribute,这限制了我只能使用默认构造函数。我猜我可以在继承类上使用属性注入来添加一个服务,以允许获取客户。但这真的是更好的设计吗? - ahsteele
仅仅因为继承类有责任并不意味着他们必须自己实现。你关于构造函数受限的说法是不正确的,你可以拥有比你所派生的类更宽泛的构造函数。请查看我的回答。 - Alex Lo
@Alex 我完全同意在“正常”情况下这不是真的,但我正在使用MVC框架中的属性类,因此无法使用DI。请参考Jimmy Bogard的文章:http://www.lostechies.com/blogs/jimmy_bogard/archive/2010/05/03/dependency-injection-in-asp-net-mvc-filters.aspx - ahsteele
你是说因为必须有一个无参构造函数,所以你不能这样做吗?不是真的。我正在更新我的答案。 - Alex Lo

1

Phil的回答是正确的。如果您不是使用抽象类,而是使用需要注入客户端getter(或工厂等)的类,则可以很好地进行测试。抽象类是测试(和良好设计)的敌人。

public class TabsActionFilter : ActionFilterAttribute 
{ 
    private readonly ICustomerGetter _getter;
    public TabsActionFilter(ICustomerGetter getter)
    { _getter = getter; }

    public override void OnActionExecuted(ActionExecutedContext filterContext) 
    { 
        Customer customer = _getter.GetCustomer(filterContext); 

        ...
    } 
} 
public interface ICustomerGetter
{ 
     Customer GetCustomer(ActionExecutedContext filterContext);
}

在你的测试中,你实例化了现在非抽象的TabsActionFilter并为其提供了一个Mock getter,这应该很容易进行模拟。

编辑:似乎有人担心你必须要有一个无参构造函数。这很简单。根据上面的代码,你可以将你的“真正”的过滤器实现为

public class MyFilter : TabsActionFilter
{
    public MyFilter() : base(new MyGetter()) {}
}

你仍然可以以相同的方式测试你的基类。


Alex,我明白你和Phil的想法。但是,我的继承来自System.Web.MvcActionFilterAttribute有限制。因此,我无法进行依赖注入。我可以进行属性注入,但是每个应用这些属性的Actiongetter都不同。 - ahsteele
是的,你需要发布有关限制你的构造函数的详细信息。这是可以完成的。什么是“属性注入”?你最后一句话没有意义。 - Alex Lo
1
@Alex,你能提供一下“抽象类是好设计的敌人”的说法的文献资料吗?如果没有抽象类,你如何实现DRY原则? - sgriffinusa
@ahsteele,getter有什么不同吗?它是否依赖于Filter的状态?如果是这样,请将其作为参数传递。请演示为什么此解决方案无法工作。 - Alex Lo
抽象类是测试(和良好设计)的敌人。在C#中,接口在幕后被视为纯抽象类。我不认为抽象类是良好设计的敌人。它们只是强制代码使用者遵守约束的另一种方式。 - Mark Lauter
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接