Getting arguments passed to a FakeItEasy-mock without using magic strings?
Asked Answered
R

2

13

I have been using Moq for my mocking needs the last years, but after looking at FakeItEasy i wanted to give it a try.

I often want to test that a method have been called with the correct parameters, but i found no satisfactory way to do this with FakeItEasy.

I have the following code to test:

    public class WizardStateEngine : IWizardStateEngine
{
    private readonly IWorkflowInvoker _workflowInvoker;
    private List<CustomBookmark> _history;

    public WizardStateEngine(IWorkflowInvoker workflowInvoker)
    {
        _workflowInvoker = workflowInvoker;
    }

    public void Initialize(List<CustomBookmark> history)
    {
        _history = history;
    }

    public WizardStateContext Execute(Command command, WizardStateContext stateContext, CustomBookmark step)
    {
        Activity workflow = new MyActivity();
        var input = new Dictionary<string, object>();
        input["Action"] = command;
        input["Context"] = stateContext;
        input["BookmarkHistory"] = _history;

        var output = _workflowInvoker.Invoke(workflow, input);

        _history = output["BookmarkHistory"] as List<CustomBookmark>;

        return output["Context"] as WizardStateContext;
    }

    public List<CustomBookmark> GetBookmarkHistory()
    {
        return _history;
    }
}

I want to write some tests that verifies the input to _workflowInvoker.Invoke(). My TestInitialize method sets up the needed resources and save the input dictionary to _workflowInvoker.Invoke() as a local field _wfInput.

    [TestInitialize]
    public void TestInitialize()
    {
        _wizardStateContext = new WizardStateContext();
        _workflowInvoker = A.Fake<IWorkflowInvoker>();
        _wizardStateEngine = new WizardStateEngine(_workflowInvoker);

        _outputContext = new WizardStateContext();
        _outputHistory = new List<CustomBookmark>();
        _wfOutput = new Dictionary<string, object>
                        {{"Context", _outputContext}, {"BookmarkHistory", _outputHistory}};

        _history = new List<CustomBookmark>();

        A.CallTo(() =>
                 _workflowInvoker.Invoke(A<Activity>.Ignored, A<Dictionary<string, object>>.Ignored))
            .Invokes(x => _wfInput = x.Arguments.Get<Dictionary<string, object>>("input"))
            .Returns(_wfOutput);

        _wizardStateEngine.Initialize(_history);
    }

After the setup i have multiple tests like this:

    [TestMethod]
    public void Should_invoke_with_correct_command()
    {
        _wizardStateEngine.Execute(Command.Start, null, null);

        ((Command) _wfInput["Action"]).ShouldEqual(Command.Start);
    }

    [TestMethod]
    public void Should_invoke_with_correct_context()
    {
        _wizardStateEngine.Execute(Command.Start, _wizardStateContext, null);

        ((WizardStateContext) _wfInput["Context"]).ShouldEqual(_wizardStateContext);
    }

    [TestMethod]
    public void Should_invoke_with_correct_history()
    {
        _wizardStateEngine.Execute(Command.Start, _wizardStateContext, null);

        ((List<CustomBookmark>) _wfInput["BookmarkHistory"]).ShouldEqual(_history);
    }

I do not like the magic string "input" in the TestInitialize for getting the passed argument (or magic number). I can write the tests without the local field like this:

    [TestMethod]
    public void Should_invoke_with_correct_context()
    {
        _wizardStateEngine.Execute(Command.Start, _wizardStateContext, null);

        A.CallTo(() =>
                 _workflowInvoker.Invoke(A<Activity>._,
                                         A<Dictionary<string, object>>.That.Matches(
                                             x => (WizardStateContext) x["Context"] == _wizardStateContext)))
            .MustHaveHappened();
    }

But i find the tests with the local field more readable.

Are there any way to setup saving of the input as a field i my test class without magic numbers or strings?

I hope the updated example in the question shows why i would like to use the local field. I am more than willing to write my tests without the local field if i can find a nice readable way to do it.

Rummer answered 20/8, 2011 at 16:49 Comment(1)
Regarding Darin Dimitrov and Patrik Hägne answers: If your unit test structure uses a distinct test fixture per method being tested (as Phil Haack shows in this post), it makes perfect sense to put setup logic into a setup method. In order to test many distinct facts over a single method, you'll use basically the same setup, with tiny changes for each fact being tested. See also Brian Rigsby approach here.Bandung
G
25
A.CallTo(() => service.DoSomething(A<int>.That.Matches(x => x == 100)))
 .MustHaveHappened();
Geocentric answered 20/8, 2011 at 16:59 Comment(8)
Thanks for the answer, but it is not quite what im looking for. I would like to save the input as a field in my test class. This way i can set it up once in SetUp, and then use the local variable in multiple tests. With complex objects the expressions can be quite hard to read. Ill update my question..Rummer
@Olsenius, why would you need to do that? You could perform the necessary assertions inside the lambda. You no longer need to call inputNumber.ShouldEqual(100);.Geocentric
i updated my question. I want to save the input as a field for multiple tests, and do setup only once.Rummer
@Olsenius, what you are trying to do is bad practice IMHO. Unit tests should be independent one from another and you should not have one test initialize data that should be used by another test. On the other hand you could write a function which will be invoked in the A<int>.That.Matches constraint on each test to verify the data.Geocentric
the tests are independent. I updated my question to show the exact class im trying to test. I might be testing wrong, but i find the tests with a local field for the input easier to read.Rummer
Actually there's no reason to use the argument constraint "A<int>.That.Matches(x => x == 100)" in this case. Just use "A.CallTo(() => service.DoSomething(100)) .MustHaveHappened();"Churinga
Ultimately the important thing to understand is that FIE is testing things 'inline'. It intercepts the call, applies the assertions you provided, and then continues to the original call. The only way FIE could 'return' the parameter to your tests in a meaningful way is for it to make a deep copy at that point (in case it is altered later) store it somewhere, and retrieve it after all execution is complete. It's perhaps possible... but ugly.Nesline
I did it with an object A.CallTo(() => _service.DoSomething(A<CustomType>.That.Matches(x => x.Property == null))).MustHaveHappenedOnceExactly();Victorious
R
21

I agree with everything Darin says, it seems like a bad practice to do what you're doing. You say that it looks "stupid" in this trivial example, could you provide an example where it looks smart?

Anyhow, the following test would have exactly the same behaviour as the Moq-test:

[Test]
public void Should_do_something_with_correct_input()
{
    int inputNumber = 0;

    var service = A.Fake<IService>();
    A.CallTo(() => service.DoSomething(A<int>._))
        .Invokes((int x) => inputNumber = x);

    var system = new System(service);
    system.InvokeService();

    inputNumber.ShouldEqual(100);
}
Refund answered 20/8, 2011 at 18:24 Comment(2)
I updated my question with the actual class I'm trying to tests. I would also prefer to not use magic numbers.Rummer
The Moq implementation is exactly equivalent, it's just as magic. It requires that the first argument is an int, just as FIE does.Churinga

© 2022 - 2024 — McMap. All rights reserved.