3

I have a method that accepts an IEnumerable:

MyMethod(IEnumerable<MyClass> myParameter)

Now I am writing this code to mock the service:

var array1 = new MyClass[0];
var array2 = new MyClass[0];

_service
   .Setup(s => s.MyMethod(array1))
   .Returns(value1);

_service
   .Setup(s => s.MyMethod(array2))
   .Returns(value2);

And finally I am doing two calls to the service with both arrays inside system under test:

_service.MyMethod(array1);
_service.MyMethod(array2);

What I do expect is to get value1 and value2 from these calls, but in practice the latter call overrides the first one and I only get value2 from both calls.

Is this a bug in Moq or is this a feature that setup treats IEnumerable not as a separate object but rather tries to expand it and compare all elements or something (resulting in two empty arrays being the same setup call)?

Ilya Chernomordik
  • 27,817
  • 27
  • 121
  • 207
  • I don't think it is about `IEnumerable` it is supposed to behave the same for every object if the following condition is met, `array1.Equals(array2)` You can think like; if you send the same parameter, you get the same result (I don't use *same* here as object-oriented context) – ilkerkaran May 03 '19 at 10:33
  • 1
    @ilkerkaran: except, of course, that it is not true that `array1.Equals(array2)`, as they are different objects. Array instances are compared by reference, not structurally. (Though Moq may do so for usability purposes, I don't know.) – Jeroen Mostert May 03 '19 at 10:35
  • I suppose it does behave in a similar way to Assert an treats them the same – Ilya Chernomordik May 03 '19 at 10:35
  • @IlyaChernomordik I was trying to explain that actually. And that behavior what it should be actually. – ilkerkaran May 03 '19 at 10:37
  • For me that was not natural, two arrays are two different objects and should not be treated as one by default, but apparently Moq took a different approach :) – Ilya Chernomordik May 03 '19 at 10:38

2 Answers2

2

When you create multiple setups on a method with Moq, each subsequent setup will replace the previous setup unless the setups are conditional (they specify certain conditions on the arguments). See this answer.

You can fix your code by specifying that the arguments must match the ones you intend to pass:

[Test]
public void MyTest()
{
    var service = new Mock<MyClass>();
    var array1 = new MyClass[0];
    var array2 = new MyClass[0];

    var value1 = "value1";
    var value2 = "value2";

    service.Setup(s => s.MyMethod(It.Is<IEnumerable<MyClass>>(e => e == array1))).Returns(value1);          
    service.Setup(s => s.MyMethod(It.Is<IEnumerable<MyClass>>(e => e == array2))).Returns(value2);

    Assert.AreEqual(value1, service.Object.MyMethod(array1));
    Assert.AreEqual(value2, service.Object.MyMethod(array2));
}
Owen Pauling
  • 11,349
  • 20
  • 53
  • 64
  • It will replace only when there is same value that is used, and that's where array "same" definition pops up. Moq thinks that two empty arrays are same. Moq does not override non-conditional setups that have different values – Ilya Chernomordik May 03 '19 at 12:51
2

The behaviour you describe is the default behaviour of the moq, you can see it here. It indeed unfold enumerable and invoke IEnumerable.SequenceEqual. However that is default behaviour(if you setup using an instance, Constant matcher) and you could override it. The one approach is what Owen suggested to use It.Is<T> matcher, e.g.

service.Setup(s => s.MyMethod(It.Is<IEnumerable<MyClass>>(e => e == array1)))
service.Setup(s => s.MyMethod(It.Is<IEnumerable<MyClass>>(e => e == array2)))

Notice that == by default do ReferenceEquals() so this will make different non overridable setups.

Johnny
  • 8,939
  • 2
  • 28
  • 33