Kevin Cruijssen
Kevin Cruijssen

Reputation: 9326

FakeItEasy Action parameter in UnitTest, but still execute inner Action code

I'm currently making some UnitTests for some new features I've added to our ASP.NET project (no it's not test-driving design). We use the NHibernate framework and use the UnitTest Mock-ing library FakeItEasy.

I have the following class & method which I want to test:

public class Round
{
    public static Round Create(List<Company> activeCompanies, Period period,
                               BusinessUser user, BusinessUser systemUser, 
                               ISession session, IEntityQuery entityQuery,
                               RoundProcessBuilder processBuilder)
    {
        var round = new Round
        {
            Processes = new List<Process>();
            Period = period,
            CreationDate = DateTime.Now,
            CreatedBy = user
        };

        // Save the Round in the DB so we can use it's Id in the Processes:
        session.Save(round);             

        foreach (var company in activeCompanies)
        {
            var companyData = session.Get<CompanyData>(company.Id);
            var processResult = 
                  roundProcessBuilder.Build(
                       systemUser, 
                       new CreateRoundProcessData(company, round, companyData),
                       entityQuery, 
                       session);

            processResult.HandleProcess(process =>
                {
                    // serviceBus can stay null
                    process.Create(systemUser, DateTime.Now, session, null); 
                    // No need to save the session here. If something went 
                    // wrong we don't want halve of the processes being saved
                    round.Processes.Add(process);

                    // It's all or nothing
                });
        }

        return round;
    }
}

What I mainly want to test: When I use this Round#Create method with let's say 100 active companies, it should create 100 processes, and each of those processes should contain the RoundId.

This is my UnitTest so far:

[TestFixture]
public class RoundTest
{
    private BusinessUser _systemUser;
    private DateTime _creationDateRound1;
    private List<Company> _activeCompanies;
    private RoundProcessBuilder _roundProcessBuilder;
    private ISession _session;

    [SetUp]
    public void Setup()
    {
        _creationDateRound1 = new DateTime(2015, 10, 5);
        _systemUser = TestHelper.CreateBusinessUser(Role.Create("systemuser", "test", 
            Int32.MaxValue));
        _activeCompanies = new List<Company>
        {
            TestHelper.CreateCompany();
        };
        _roundProcessBuilder = A.Fake<RoundProcessBuilder>();
        _session = A.Fake<ISession>();
    }

    [Test]
    public void TestCreateRoundWithoutPreviousRound()
    {
        var fakeExpectedRound = Round.Create(_activeCompanies, DateTime.Now.ToPeriod(),
            _systemUser, _systemUser, _session, null, _roundProcessBuilder);
        var fakeExpectedRoundData = RoundProcessData.Create(TestHelper.CreateCompany(),
            fakeExpectedRound, new CompanyData());
        var fakeExpectedProcess = new Process(_systemUser, null, "processName", null,
            fakeExpectedRoundData, "controllerName", null);
        var processSuccessResult = new ProcessSuccessResult(fakeExpectedProcess);

        A.CallTo(() => _roundProcessBuilder.Build(null, null, null, null))
            .WithAnyArguments()
            .Returns(processSuccessResult);

        A.CallTo(() => processSuccessResult.HandleProcess(A<Action<Process>>.Ignored))
            .Invokes((Action<Process> action) => action(fakeExpectedProcess));
        var round = Round.Create(_activeCompanies, _ceationDateRound1.ToPeriod(),
            _systemUser, _systemUser, _session, null, _roundProcessBuilder);

        Assert.AreEqual(_activeCompanies.Count, round.Processes.Count, "Number of processes");
        Assert.AreEqual(round.Period.Quarter, Math.Ceiling(_creationDateRound1.Month / 3.0m), "Quarter");
        Assert.AreEqual(round.Period.Year, round.Year, "Year");

        // Test if each of the processes knows the RoundId, have the proper state,
        // and are assigned to the systemuser
        //foreach (var process in round.Processes)
        //{
        //    var roundProcessData = process.ProcessData as RoundProcessData;
        //    Assert.IsNotNull(roundProcessData, "All processes should have RoundProcessData-objects as their data-object");
        //    Assert.AreEqual(roundProcessData.Round.Id, round.Id, "RoundId");
        //    Assert.AreEqual(process.Phase.State, PhaseState.Start, "Process state should be Start");
        //    Assert.AreEqual(process.AssignedTo, _systemUser, "AssignedTo should be systemuser");
        //}
    }

    ... // More tests
}

My problem lies in the following code:

A.CallTo(() => processSuccessResult.HandleProcess(A<Action<Process>>.Ignored))
    .Invokes((Action<Process> action) => action(fakeExpectedProcess));

It gives an "The specified object is not recognized as a fake object." error.

The reason I have this part of the code is because the process in the following part was null without it:

processResult.HandleProcess(process => // <- this was null
    {
        process.Create(systemUser, DateTime.Now, session, null);
        round.Processes.Add(process);
    });

PS: I uncommented the foreach with additional checks in my UnitTest because it most likely is pretty useless anyway when I mock the process itself.. My main test is if processes are created and added to the list based on the active companies given.

Upvotes: 1

Views: 1053

Answers (1)

Kjartan
Kjartan

Reputation: 19111

Your problem seems to be that you are trying to add "fake" logic to an object that is not in fact, a fake:

// You create this as an instance of ProcessSuccessResult:
var processSuccessResult = new ProcessSuccessResult(fakeExpectedProcess);

...then proceed to attempt to add a condition to it here:

A.CallTo(() => 
     processSuccessResult
         .HandleProcess(A<Action<Process>>.Ignored))
         .Invokes((Action<Process> action) => action(fakeExpectedProcess));

In order to do this last bit, the variable processSuccessResult will need to be a fake instance of an interface, so that FakeItEasy can work with it, and apply the logic you want.

I'm assuming ProcessSuccessResult is a class you have access to, and are able to edit? If so, you should be able to add an interface to it, that will contain the methods you need, so you can work against that later.

Once you've defined that, you should be able to create your fake object as follows, where IProcessSuccessResult will be a fake implementation of your interface, provided by FakeItEasy:

var processSuccessResult = A.Fake<IProcessSuccessResult>();

Now you should be able to add logic to that fake object using A.CallTo(...).

Of course, this will imply that the real implementation of your class ProcessSuccessResult is not included or called via the variable processSuccessResult. If part of it needs to be, then you might try to either:

  • Add logic similar to it, or calls to it from the fake object using FakeItEasy's set up code (although this might get overly complicated), OR:
  • Add a separate variable to contain an instance of the real class (i.e. two variables fakeProcessSuccessResult and processSuccessResult, respectively), and use separate tests for testing separate aspects of your both this class, and it's usages.

I would recommend the latter, if possible.

I hope this is clear enough, and that this will be useful to you. I know it can be quite complicated sometimes, to find the optimal strategy for testing things like this.

Upvotes: 2

Related Questions