简体   繁体   中英

Consequences of using Virtual keyword on all methods in a class?

I am new to TDD and I am using Moq as my mocking framework. I am trying to check if a method has been called in my class. The class is not implementing any Interface.

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

to make this work I found here that I have to make the Save() method as a Virtual method.

Question:

What are the consequences of using Virtual keyword for all methods in a class just for the sake of making it testable?

TL;DR

As per the comments, the need for the virtual keyword indicates that your class hierarchy is too tightly coupled, and you should apply SOLID principles to decouple them from eachother. This has the "happy" side effect of making your class hierarchy easier to Unit Test, as dependencies can be mocked via the interface abstraction.

In more Detail

The need make all public methods virtual to allow Moq to override them is frequently indicative of a separation of concerns or class coupling smell. eg this scenario needed virtual methods because class under test has multiple concerns, and there was a need to mock one method and actually invoke another method in the same system under test.

As per @JonSkeet's comment, it is commonplace SOLID best practice to abstract dependencies as interfaces. As it stands, your class under test (May I call it "Controller"?) is dependent on the concrete FooSaverService to save Foos.

By applying the Dependency Inversion Principle , this coupling can be loosened, by abstracting just the externally useful methods, properties and events of FooSaverService to an interface ( IFooSaverService ), and then

  • FooSaverService implements IFooSaverService
  • Controller depends only on IFooSaverService

(Obviously, there are likely other optimizations, eg to make the IFooSaverService generic, eg ISaverService<Foo> but not in scope here)

Re : Mock<Foo> - it is fairly uncommon to need to Mock simple data storage classes (POCO's, Entities, DTO's etc) - since these will typically retain data stored in them and can be reasoned over directly in unit tests.

To answer your question re implications of Virtual (which is hopefully less relevant now):

  • You are breaking the (polymorphic) Open and Closed Principle - it is inviting others to override behaviour without deliberately designing for this - there may be unintended consequence.
  • As per Henk's comment, there will be a small performance impact in administering the virtual method table

A code example

If you put all this together, you'll wind up with a class hierarchy like so:

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

And then you'll be able to test your class which has the IFooSaverService dependency as follows:

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

TL;DR; Another good answer is - making classes extendable, and providing virtual methods (ie the possibility to extend them) is "feature" of that class. And this feature needs to be supported and tested as any other feature.

Much better explanation can be read on Eric Lippert's blog .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM