0

I am using Xamarin Studio 5.2 on Mac OS X 10.9.4 with NUnit 2.6.3 and FakeItEasy 1.23.0.

When I run tests for this code:

using System;
using ValueSet = System.Collections.Generic.HashSet<uint>;
using NUnit.Framework;
using FakeItEasy;

namespace SetTest
{
    [TestFixture]
    class TestFixture
    {
        [Test]
        public void CallsUsersWithSetAndReducedSet()
        {
            var values = new ValueSet { 1, 2, 3 };

            var setUser = A.Fake<SetUser>();

            ClassUnderTest testInstance = new ClassUnderTest();

            using (var scope = Fake.CreateScope())
            {
                testInstance.RunWith(setUser);

                using (scope.OrderedAssertions())
                {
                    A.CallTo(() => setUser.Use(A<ValueSet>.That.IsEqualTo(values))).MustHaveHappened(Repeated.Exactly.Once);
                    A.CallTo(() => setUser.Use(A<ValueSet>.That.Matches(set =>
                        set.Count == 2 && set.Contains(1)))).MustHaveHappened(Repeated.Exactly.Once);
                }
            }
        }
    }

    public class SetUser
    {
        public virtual void Use(ValueSet set)
        {
        }
    }

    class ClassUnderTest
    {
        public static void Main(string[] arguments)
        {
        }

        public void RunWith(SetUser setUser)
        {
            var values = new ValueSet { 1, 2, 3 };
            setUser.Use(values);

            values.Remove(3);
            setUser.Use(values);
        }
    }
}

I get the following error output:

FakeItEasy.ExpectationException: Assertion failed for the following call: SetTest.SetUser.Use(1[System.UInt32]>) Expected to find it exactly once but found it #0 times among the calls: 1. SetTest.SetUser.Use(set: System.Collection.Generic.HashSet1[System.UInt32]) repeated 2 times

I don't understand what is causing this failure and how to fix it. What is needed to get this type of test to pass?

Blair Conrad
  • 233,004
  • 25
  • 132
  • 111
Tron Thomas
  • 871
  • 7
  • 20
  • I can't see anywhere that `values` is passed into any of the objects, so how would it be possible for your verifier to match? If another `ValueSet` is being created somewhere in code that youhaven't provided, then perhaps `ThatIsEqualTo()` is testing reference equality? How is equality defined for a `ValueSet`? – Tim Long Aug 10 '14 at 01:45
  • There are two ValueSet variables. One is in the test method and the other is in the RunWith method of the tested class. They are both setting the same values so it seems that the expectation should past from that standpoint. – Tron Thomas Aug 11 '14 at 03:10

1 Answers1

2

@Tim Long is on the right track in his comment.
Here's a little more detail, as well as updates to respond to your comments of 2014-08-11 03:25:56:

The first reason the first MustHaveHappened fails:

According to the FakeItEasy argument constraints documentation, That.IsEqualTo tests for "object equality using object.Equals". That's what's causing the unexpected behaviour.

Not passing values into the method isn't necessarily a problem, or wouldn't be if ValueSet.Equals performed a value comparison, but ValueSet is a HashSet<uint>, so you can see from that class's method documentation that it doesn't—it uses object.Equals, which tests for reference equality. Thus, your IsEqualTo assertion fails. If you use a more sophisticated matcher that performed a value-type comparison for HashSet, perhaps something closer to what you use in your second A.CallTo, or maybe something using That.Contains, I think you'll have better success.

You may think to use That.IsSameSequenceAs, but be careful if doing so: the HashSet doesn't guarantee the order of the elements in the enumeration, so even if the set has the same elements, you may get a failure.

The second reason the first MustHaveHappened fails:

RunWith changes the contents of the values set between calls to setUser.Use. So the same set is used in two calls, first with 3 elements, then when it has only 2 elements. This means that by the time the first MustHaveHappened call is made, the set has only 2 elements, so the comparison fails. You could see this more clearly by writing an argument formatter for the ValueSet. That would provide more information.

The cause of the mismatch is that when a call is made to a faked method, FakeItEasy captures the arguments. However, for reference types, such as ValueSet (HashSet), only the reference to the argument is kept. Thus, if the object is modified later, in particular between the execution and the verification stages of the test, the object will look different than it did at the time of the faked call. See @jimmy_keen's answer to MustHaveHappened fails when called twice on the same object. There's a little more discussion over at FakeItEasy Issue 306 - Verifying multiple method calls with reference parameters.

In this case, the usual approach is to do as he suggests—provide code to capture the important state of the incoming argument at call time, and then query that saved state later.

You might be able to use something like this:

[Test]
public void CallsUsersWithSetAndReducedSet()
{
    var capturedValueSets = new List<List<uint>>();

    var setUser = A.Fake<SetUser>();
    A.CallTo(() => setUser.Use(A<ValueSet>._)) // matches any call to setUser.Use
        .Invokes((ValueSet theSet) => capturedValueSets.Add(theSet.ToList()));

    ClassUnderTest testInstance = new ClassUnderTest();

    testInstance.RunWith(setUser);

    Assert.That(capturedValueSets, Has.Count.EqualTo(2),
        "not enough calls to setUser.Use");
    Assert.That(capturedValueSets[0], Is.EquivalentTo(new uint[] {1, 2, 3}),
        "bad set passed to first call to setUser.Use");
    Assert.That(capturedValueSets[1], Has.Count.EqualTo(2) & Has.Member(1),
        "bad set passed to second call to setUser.Use");
}

You can see that each time Use is called, we add the contents of the ValueSet argument to capturedValueSets. Then at the end we

  1. make sure 2 calls were made, by checking the length of capturedValueSets
  2. make sure that the first time Use was called, the set had the elements 1, 2, and 3. Is.EquivalentTo checks the two lists but ignores order
  3. make sure that the second time Use was called, the set had 2 elements, one of which was 1

By checking the two captured value sets in turn, all the bits about the scopes and ordered assertions became unnecessary.

Community
  • 1
  • 1
Blair Conrad
  • 233,004
  • 25
  • 132
  • 111
  • ThatIsEqualTo was a typo I was trying to tweak the code after I had pasted it to use something that I thought would properly compare the values. It is confusing to know the right thing to call. – Tron Thomas Aug 11 '14 at 03:24
  • I changed the comparison so in the expectation so it now calls That.IsSameSequenceAs(values). If I comment out the second expectation and the last two lines in the RunWith method, the test passes. When I re-enable everything, the test fails with this error: – Tron Thomas Aug 11 '14 at 03:25
  • FakeItEasy.ExpectationException: Assertion failed for the following call: SetTest.SetUser.Use() Expected to find it exactly once but found it #0 times among the calls: 1: SetTest.SetUser.Use(set: System.Collections.HashSet`1[System.UInt32]) repeated 2 times – Tron Thomas Aug 11 '14 at 03:25
  • Did the expanded answer help at all? If not, I'm happy to keep working at it. – Blair Conrad Aug 14 '14 at 23:00
  • Okay, I tried what you suggested and that works. Thanks – Tron Thomas Aug 16 '14 at 04:31