Mocking GetEnumerator() method of an IEnumerable

2019-03-18 16:55发布

问题:

The following test case fails in rhino mocks:

[TestFixture] 
    public class EnumeratorTest 
    { 
        [Test] 
        public void Should_be_able_to_use_enumerator_more_than_once() 
        { 
            var numbers = MockRepository.GenerateStub<INumbers>(); 
            numbers.Stub(x => x.GetEnumerator()).Return(new List<int> 
{ 1, 2, 3 }.GetEnumerator()); 
            var sut = new ObjectThatUsesEnumerator(); 
            var correctResult = sut.DoSomethingOverEnumerator2Times 
(numbers); 
            Assert.IsTrue(correctResult); 
        } 
    } 
    public class ObjectThatUsesEnumerator 
    { 
        public bool DoSomethingOverEnumerator2Times(INumbers numbers) 
        { 
            int sum1 = numbers.Sum(); // returns 6 
            int sum2 = numbers.Sum(); // returns 0 =[ 
            return sum1 + sum2 == sum1 * 2; 
        } 
    } 
    public interface INumbers : IEnumerable<int> { } 

I think there is something very subtle about this test case, and I think it is from me not thinking through how Rhino Mocks stubbing actually works. Typically, when you enumerate over an IEnumerable, you are starting with a fresh IEnumerator. In the example above, it looks like I could be re-using the same enumerator the second time I am calling sum, and if the enumerator is already at the end of its sequence, that would explain why the second call to Sum() returns 0. If this is the case, how could I mock out the GetEnumerator() in such a way that it behaves in the way that I am wanting it to (e.g. new enumerator or same enumerator reset to position 0)?

How would you modify the above test case so that the second .Sum() call actually returns 6 instead of 0?

回答1:

The WhenCalled() api lets you dynamically resolve return values.

Changing the test case to the following will allow it to pass:

numbers.Stub(x => x.GetEnumerator())
                     .Return(null)
                     .WhenCalled(x => x.ReturnValue = 
                                    new List<int> { 1, 2, 3 }.GetEnumerator()
                                 );

So instead of returning the same enumerator, the stubbed behavior will always return a new enumerator.



回答2:

The statement

numbers.Stub(x => x.GetEnumerator()).Return(new List<int> { 1, 2, 3 }.GetEnumerator());

is identical to

var enumerator = new List<int> { 1, 2, 3 }.GetEnumerator();
numbers.Stub(x => x.GetEnumerator()).Return(enumerator);

In your test, you are telling Rhino Mocks to give the identical IEnumerator<int> instance enumerator twice. That's not what you intended. A single instance of IEnumerator<int> is good only for one enumeration, not two enumerations (Reset() is not supported, typically). You intended Rhino Mocks to give two different instances of IEnumerator<int>, so that they can be summed separately, just as any call to any other GetEnumerator<int>() function would do.



回答3:

Disclaimer: I work at Typemock

I don't know if you can use Rhino to do this but you can use Isolator with AAA API that has exactly what you're looking for -

[Test] 
public void Should_be_able_to_use_enumerator_more_than_once() 
{
   var numbers = Isolate.Fake.Instance<INumbers>();
   Isolate.WhenCalled(() => numbers).WillReturnCollectionValuesOf(new List<int>{ 1, 2, 3 });
   var sut = new ObjectThatUsesEnumerator(); 
   var correctResult = sut.DoSomethingOverEnumerator2Times(numbers); 
   Assert.IsTrue(correctResult); 
}

For more info see Managing Collections in the Developer Guide.