1

I have an object under test with a method called void Print(string msg).

The object invoke the method multiple times and passes a different message.

For example...

Strait forward of the usage:

public interface IMyPrinter
{
   void Print(string msg);
}

public class Printer : IMyPrinter
{
    public void Print(string msg)
    {
         // print to console
    }
}

public class MyObject
{
    public IMyPrinter Printer { get; set; }
    
    public void foo()
    {
        for (var count = 0; count < 4; count++)
        {
            Printer.Print($"abc_{count}");
        }
    }
}

When I want to test foo method, how can setup the Mock object to capture the different Print methods calls?

I tried:

var printerMock = new Mock<IMyPrinter>(MockBehavior.Loose);
for (var count = 0; count < 4; count++)
{
    printerMock.Setup(_ => _.Print($"abc_{count}");
}

var underTest = new MyObject();
underTest.Printer = printerMock.Object;

underTest.foo();
printerMock.VerifyAll();

but this of course made only the last setup effective (when count = 3). How can this be done?

NirMH
  • 4,769
  • 3
  • 44
  • 69

2 Answers2

1

Another option, since in this case the mocked member has no expected behavior in a loose mock, would be to use the loop for verification instead of setup.

For example

// Arrange
var printerMock = new Mock<IMyPrinter>(MockBehavior.Loose);
var underTest = new MyObject();
underTest.Printer = printerMock.Object;

// Act
underTest.foo();

// Assert
for (int count = 0; count < 4; count++) {
    string msg = $"abc_{count}";
    printerMock.Verify(_ => _.Print(msg), Times.Once);
}

If you want to capture the actual arguments passed, then a call back can be used

// Arrange
List<string> expected = new List<string>() { ... }; //populated
List<string> actual = new List<string>();
var printerMock = new Mock<IMyPrinter>(MockBehavior.Loose);
printerMock
    .Setup(_ => _.Print(It.IsAny<string>()))
    .Callback(string msg => actual.Add(msg));
var underTest = new MyObject();
underTest.Printer = printerMock.Object;

// Act
underTest.foo();

// Assert
//..If messages are known before hand then they can be used to assert the
//the captured messages from when the mock was invoked.
actual.Should().BeEquivalentTo(expected); //Using FluentAssertions
Nkosi
  • 235,767
  • 35
  • 427
  • 472
0

but this of course made only the last setup effective (when count = 3).

actually that is not true, if you look closely to the test output you will see that it contains 4 calls with the value of 4!:

IMyPrinter _ => _.Print("abc_4")
IMyPrinter _ => _.Print("abc_4")
IMyPrinter _ => _.Print("abc_4")
IMyPrinter _ => _.Print("abc_4")

this is the value that count has after the loop has ended. Since you are using a lambda expression, your count variable is caught in closure and the last value 4 was captured. Only this value is used when the lambda expression is evaluated later on in your code when the loop has finished a long time ago. You need to create a temporaty variable and capture the index in it. And your test will run green:

var printerMock = new Mock<IMyPrinter>(MockBehavior.Loose);
for (var count = 0; count < 4; count++)
{
    int i = count; // <-- this is the main change
    printerMock.Setup(_ => _.Print($"abc_{i}")); // <-- use "i" in lambda
}

var underTest = new MyObject();
underTest.Printer = printerMock.Object;

underTest.foo();
printerMock.VerifyAll();

EDIT:

for further reading on closures I recommend this article by Jon Skeet

Mong Zhu
  • 23,309
  • 10
  • 44
  • 76
  • btw: if i replace the `for` loop with `foreach` over a range starting with 0 till 3 - I don't need the local variable. Can you explain? – NirMH Oct 25 '21 at 10:15
  • @NirMH foreach is also a valid solution. " Can you explain?" not really. for that I would have to look into the foreach code. I guess it is using the iterator of the colelction behind the curtains which is not part of the lambda expression and hence does not get caught in closure. – Mong Zhu Oct 25 '21 at 10:18
  • @NirMH [Jon Skeet](https://stackoverflow.com/a/7133857/5174469) can explain it much better. Or even better [this answer](https://stackoverflow.com/a/16264354/5174469) – Mong Zhu Oct 25 '21 at 10:19