Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I test a class that takes a factory?

I'm trying to test a class that takes a factory (Func<T>) and I'm using Moq and AutoFixture.

What is the best way to setup the "environment" to see if the factory has been used and how many times and what methods have been used on the returned instances?

Currently I'm Mock'ing the T and Injecting a Func<T> that keeps count of all returned Mock instances:

public class SystemUnderTest {
    public SystemUnderTest(Func<IMyClass> c)
    {
        try {
            var c1 = c();
            c1.Name="n1";
            c1.Send();
        }
        catch(Exception){
            var c2 = c();
            c2.Name="n2";
            c2.Send();
        }
    }
}
private Mock<IMyClass> MockFactory()
{
   var m = new Mock<IMyClass>();
   m.SetupProperty(mc=>mc.Name);
   _returnedStubs.Add(m);
   return m;
}  
[Test]
public void TestSomething()
{
    var fixture = new Fixture();
    fixture.Inject(()=>MockFactory().Object)
    var sut = fixture.CreateAnonymous<SystemUnderTest>();
    Assert.That(_returnedStubs.Count,Is.Equal(1));
    _returnedStubs[0].Verify(m=>m.Send(),Times.Exactly(1));
    _returnedStubs[0].Verify(m=>m.Name = "n1");
}

But it feels kinda iffy/ugly to me. And I'm pretty sure that an instance variable in a test class is a dangerous thing

like image 285
svrist Avatar asked Feb 22 '23 01:02

svrist


2 Answers

Since AutoFixture is able to create anonymous delegates, when asked to create an anonymous instance of SystemUnderTest, it will also automatically provide an anonymous Func<IMyClass> delegate, which in turn returns an anonymous instance of IMyClass when invoked.

This means that, given this scenario:

public class SystemUnderTest
{
    public SystemUnderTest(Func<IMyClass> c)
    {
        try
        {
            var c1 = c();
            // ...
        }
        catch (Exception)
        {
            var c2 = c();
            // ...
        }
    }
}

the following code:

var fixture = new Fixture();
var sut = fixture.CreateAnonymous<SystemUnderTest>();

will assign the c1 and c2 variables with anonymous instances of IMyClass. Furthermore, if you configure AutoFixture to work as an auto-mocking container, for example using the AutoMoqCustomization, those anonymous instances of IMyClass will also happen to be Moq proxies:

var fixture = new Fixture();
fixture.Customize(new AutoMoqCustomization());
var sut = fixture.CreateAnonymous<SystemUnderTest>();

This information however, although useful, doesn't really help you in your particular case since you need to get a hold of the mock objects returned by the Func<IMyClass> factory method in your test, in order to configure their behavior and make some assertions on how they've been interacted with.

The best solution, in my opinion, is to change the implementation of the factory method from Func<IMyClass> to an interface. This way you can create a fake factory that returns different mocks of the IMyClass interface when the Create method is invoked multiple times in a sequence:

So, given this example:

public interface IFactory<T>
{
    T Create();
}

public class SystemUnderTest
{
    public SystemUnderTest(IFactory<IMyClass> factory)
    {
        try
        {
            var c1 = factory.Create();
            // ...
        }
        catch (Exception)
        {
            var c2 = factory.Create();
            // ...
        }
    }
}

You can setup your test scenario as follows:

    // Given
    var c1 = new Mock<IMyClass>();
    var c2 = new Mock<IMyClass>();
    // TODO: setup the behavior of the mock objects
    var factory = new Mock<IFactory<IMyClass>>();
    factory.Setup(s => s.Create()).ReturnsInOrder(c1.Object, c2.Object);

    // When
    var fixture = new Fixture();
    fixture.Inject(() => factory.Object)
    var sut = fixture.CreateAnonymous<SystemUnderTest>();

    // Then
    // TODO: verify the expectations on the mock objects

Note that the ReturnsInOrder is a custom extension method that uses the Callback method in Moq to return different values from a stubbed method when it gets invoked multiple times in a row.

like image 152
Enrico Campidoglio Avatar answered Feb 23 '23 14:02

Enrico Campidoglio


One of the best ways is to create your own function and pass it and then setup expectations or state there.

int numberOfTimesUsed = 0;
myObject.Func = (x) => 
{ 
   numberOfTimesUsed++;
   Assert.IsNotNull(x); // checks if x passed was not null
};

...

Assert.AreEqual(2, numberOfTimesUsed); // checks if called twice
like image 39
Aliostad Avatar answered Feb 23 '23 15:02

Aliostad