简体   繁体   中英

Constructor uses the mock object, how do I test a method in isolation?

I have a class that looks like this:

class MyClass {

  private IDependency dep;

  public MyClass(IDependency dep) {
    this.dep = dep;
    this.dep.Reset();
  }

  public void Clear() {
    this.dep.Reset();
  }
}

How do I test that the Reset method gets called properly in the Clear method while ignoring what the constructor does?

My Moq test looks like this:

MockRepository mocks = new MockRepository(MockBehavior.Default);
var dep = mocks.Create<IDependency>();

dep.Setup(s => s.Reset());

MyClass myclass = new MyClass(dep.Object);
myclass.Clear():

state.Verify(s => s.Reset(), Times.Exactly(1));

It fails because Reset has been called twice (once in the constructor, and once in the Clear method).

I hoped there would be a better way of doing it, but the mock will record all calls to Reset so using a standard Verify call will always return 2. The following maintains a separate counter, which isn't very elegant. If there's a built-in way of doing this with Moq, I'd love to know.

int clearResetCount = 0;

Mock<IDependency> dep = new Mock<IDependency>();

MyClass myclass = new MyClass(dep.Object);

dep.Setup(s => s.Reset()).Callback(() => clearResetCount++);

Assert.AreEqual(0, clearResetCount, "Expected clear reset count - before.");

myclass.Clear();

Assert.AreEqual(1, clearResetCount, "Expected clear reset count - after.");

As suggested by others, you can roll your own mock or you can set a number of expectations on the dependency.

For example, you can verify that your method was called:

var mock = new Mock<IDependency>();
var subject = new MyClass(mock.Object);

subject.Clear();

mock.Verify( dep => dep.Reset(), Times.AtMost(2));

However it's worth pointing out that work within the constructor is a known code smell , and this smell is exacerbated when you try to write tests.

The fact that your constructor needs to call this method on the dependency suggests that this object knows too much information about the implementation details of the dependency. This violates the Open Closed Principle and closes you off from scenarios where you don't want the Reset method from being called when it's initialized.

Also consider that any class or test that uses the MyClass concrete object as an dummy parameter will need a Mock initialized or you'll get a NullReferenceException. This adds considerable overhead to writing your tests and adds a level of fragility that equates to long term maintenance and false negatives in your tests. The only way around this is to make everything an interface which although effective isn't the best long term strategy either.

As per http://googletesting.blogspot.com/2009/07/separation-anxiety.html , the use of a Factory would reduce some of this coupling and open you up to better reuse of this object.

I came across the same problem.

Do the following to have the mock only recording the behavior of the Clear method:

MockRepository mocks = new MockRepository(MockBehavior.Default);
var dep = mocks.Create<IDependency>();

MyClass myclass = new MyClass(dep.Object);

// setting up the mock just before calling the method under test
// will ignore any prior call to IDependency.Reset
int resetTimes = 0;
dep.Setup(s => s.Reset()).Callback(() => resetTimes++);

myclass.Clear();

mocks.VerifyAll();
Assert.That(resetTimes, Is.EqualTo(1));

Instead of using a mock object, you could write a spy. Requires a bit more coding, but the test is easier to read.

class DependencySpy : IDependency {
    public int ResetCallCount { get; private set; }
    public void Reset() { ResetCallCount++; }
    public void ClearResetCallCount() { ResetCallCount = 0; }
}

The test could the be written as

// Setup
var spy = new DependencySpy;
MyClass myclass = new MyClass(spy);
spy.ClearResetCallCount();
// Exercise
myclass.Clear();
// Validate
Assert.AreEqual(1, spy.ResetCallCount);

You can use reflection to set the private field dep to your mocked object. Then just call the Clear method and test the dependency call.

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