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

Meh*_*deh 7 c# tdd moq

我是 TDD 新手,我正在使用它Moq作为我的模拟框架。我正在尝试检查我的类中是否已调用某个方法。该类没有实现任何接口。

 var mockFooSaverService = new Mock<FooSaverService>();
 mockFooSaverService.Verify(service => service.Save(mockNewFoo.Object));
Run Code Online (Sandbox Code Playgroud)

为了使这项工作正常进行,我发现必须将该Save()方法作为一种Virtual方法。

问题:

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

Stu*_*tLC 3

长话短说

根据评论,对 virtual 关键字的需要表明您的类层次结构耦合过于紧密,您应该应用SOLID 原则来将它们彼此解耦。这具有“令人高兴”的副作用,使您的类层次结构更容易进行单元测试,因为可以通过接口抽象来模拟依赖项。

更详细

需要将所有公共方法虚拟化以允许 Moq 覆盖它们,这通常表明关注点分离或类耦合气味。例如,这个场景需要虚拟方法,因为被测类有多个关注点,并且需要模拟一个方法并实际调用同一被测系统中的另一个方法。

根据 @JonSkeet 的评论,将依赖项抽象为接口是常见的可靠最佳实践。就目前情况而言,您的测试类(我可以称之为“控制器”吗?)依赖于具体的内容FooSaverService来保存 Foos。

通过应用依赖倒置原则,可以通过仅将外部有用的方法、属性和事件抽象FooSaverService到接口(IFooSaverService)来放松这种耦合,然后

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

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

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

回答你的问题涉及以下含义Virtual(希望现在不太相关):

代码示例

如果将所有这些放在一起,您将得到如下所示的类层次结构:

// 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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

然后您将能够测试具有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);
    }
}
Run Code Online (Sandbox Code Playgroud)