在类中的所有方法上使用Virtual关键字的后果?

时间:2015-03-20 08:30:45

标签: c# tdd moq

我是TDD的新手,我使用Moq作为我的模拟框架。 我试图检查是否在我的班级中调用了一个方法。 该类没有实现任何接口。

 var mockFooSaverService = new Mock<FooSaverService>();
 mockFooSaverService.Verify(service => service.Save(mockNewFoo.Object));

为了完成这项工作,我发现here我必须将Save()方法作为Virtual方法。

问题:

为了使其可测试而对类中的所有方法使用Virtual关键字会产生什么后果?

2 个答案:

答案 0 :(得分:3)

<强> TL; DR

根据评论,对virtual关键字的需求表明您的类层次结构过于紧密,您应该应用SOLID principles将它们彼此分离。这具有“快乐”的副作用,使您的类层次结构更容易进行单元测试,因为依赖项可以通过接口抽象进行模拟。

更多细节

需要使所有公共方法虚拟以允许Moq覆盖它们通常表示关注点或类别耦合气味的分离。 例如this scenario needed virtual methods因为测试中的类有多个问题,并且需要模拟一个方法并实际调用同一系统中的另一个方法。

根据@ JonSkeet的评论,将依赖关系抽象为接口是常见的SOLID最佳实践。目前,您正在测试的课程(我称之为“控制器”?)取决于具体FooSaverService以节省Foos。

通过应用Dependency Inversion Principle,可以通过将FooSaverService的外部有用方法,属性和事件抽象到接口(IFooSaverService),然后

  • FooSaverService实施IFooSaverService
  • Controller仅取决于IFooSaverService

(显然,可能还有其他一些优化措施,例如IFooSaverService通用,例如ISaverService<Foo>,但不在此范围内)

Re:Mock<Foo> - 需要模拟简单数据存储类(POCO,实体,DTO等)是非常罕见的 - 因为这些通常会保留存储在其中的数据,并且可以直接在单元测试中进行推理

回答你的问题Virtual的影响(希望现在不太相关):

  • 你打破了(多态)Open and Closed Principle - 它正在邀请其他人覆盖行为,而不是故意为此设计 - 可能会产生意想不到的后果。
  • 根据Henk的评论,管理virtual method table
  • 会对业绩产生轻微影响

代码示例

如果你把所有这些放在一起,你最终会得到一个类层次结构:

// Foo is assumed to be an entity / POCO
public class Foo
{
    public string Name { get; set; }
    public DateTime ExpiryDate { get; set; }
}

// Decouple the Saver Service dependency via an interface
public interface IFooSaverService
{
    void Save(Foo aFoo);
}

// Implementation
public class FooSaverService : IFooSaverService
{
    public void Save(Foo aFoo)
    {
        // Persist this via ORM, Web Service, or ADO etc etc.
    }
    // Other non public methods here are implementation detail and not relevant to consumers
}

// Class consuming the FooSaverService
public class FooController
{
    private readonly IFooSaverService _fooSaverService;

    // You'll typically use dependency injection here to provide the dependency
    public FooController(IFooSaverService fooSaverService)
    {
        _fooSaverService = fooSaverService;
    }

    public void PersistTheFoo(Foo fooToBeSaved)
    {
        if (fooToBeSaved == null) throw new ArgumentNullException("fooToBeSaved");
        if (fooToBeSaved.ExpiryDate.Year > 2015)
        {
            _fooSaverService.Save(fooToBeSaved);
        }
    }
}

然后您将能够测试具有IFooSaverService依赖关系的类,如下所示:

[TestFixture]
public class FooControllerTests
{
    [Test]
    public void PersistingNullFooMustThrow()
    {
        var systemUnderTest = new FooController(new Mock<IFooSaverService>().Object);
        Assert.Throws<ArgumentNullException>(() => systemUnderTest.PersistTheFoo(null));
    }

    [Test]
    public void EnsureOldFoosAreNotSaved()
    {
        var mockFooSaver = new Mock<IFooSaverService>();
        var systemUnderTest = new FooController(mockFooSaver.Object);
        systemUnderTest.PersistTheFoo(new Foo{Name = "Old Foo", ExpiryDate = new DateTime(1999,1,1)});
        mockFooSaver.Verify(m => m.Save(It.IsAny<Foo>()), Times.Never);
    }

    [Test]
    public void EnsureNewFoosAreSaved()
    {
        var mockFooSaver = new Mock<IFooSaverService>();
        var systemUnderTest = new FooController(mockFooSaver.Object);
        systemUnderTest.PersistTheFoo(new Foo { Name = "New Foo", ExpiryDate = new DateTime(2038, 1, 1) });
        mockFooSaver.Verify(m => m.Save(It.IsAny<Foo>()), Times.Once);
    }
}

答案 1 :(得分:0)

TL; DR; 另一个好的答案是 - 使类可扩展,并提供虚拟方法(即扩展它们的可能性)是&#34;特征&#34;那个班。此功能需要像任何其他功能一样得到支持和测试。

可以在Eric Lippert's blog上阅读更好的解释。