Sunday 13 March 2016

Apex Mocks Matchers

Unit testing in Apex can be tough. But ApexMocks makes your tests faster to write, and faster to run.

I love ApexMocks, and it's more than likely they'll pop up in my future blog posts. If you want the low down on ApexMocks, start with the public ApexMocks GitHub repo or check out the talk I gave at London's Calling 2016.

Recently, ApexMocks had a substantial upgrade with the addition of matchers. But it didn't come with much documentation... leaving people to fumble through and fend for themselves under the shadowy veil of uncertain uncertainty. So for my very first foray into the world of blogging, let's sort that out!

Introduction

Old school mocks
Traditionally, apex mocks has matched method calls to method counts and return values by comparing actual method argument values with expected method argument values.

Apex mocks stored the lists of expected argument values internally in a Map. As a consequence, ApexMocks implicitly matched arg values using hashcode/equals methods defined on the arg value classes.

This has several limitations:

  • If you add an SObject record as the key in a map, and then change any of its field values, you will no longer be able to get it back out of the map. This post by Stephen Willcock goes into more detail: http://foobarforce.com/2013/09/10/sobject-secret-life-equality-sets-maps/
  • Lists, Maps and Sets all suffer from this problem too.
  • If you want to use custom classes as method arguments, you have to ensure there are correct equals and hashcode implementations for that class. This may mean implementing your own equals/hashcode methods in production code, only for the benefit of tests, which is a code smell.
  • If you want to stub a return value for a range of argument values, you need to add a stub for each argument permutation.
  • You cannot verify a method has been called with a range of argument values. E.g. if I have a method called doStuff(Integer x), I cannot verify it was called with any Integer between 1 and 10. The best I can do is verify it was called with a specific Integer value.

Matchers

Matchers allow you to invoke custom matching logic for method argument values.
For example, you may want to stub a method when it is called with a specific SObject. This would fail using the concrete SObject record if its field values are changed before the method is called.

Using matchers, you can instead stub a method when it is called with a specific SObject reference (i.e. using === rather than == in argument matching). Alternatively, you can match an sobject record with a given ID or name.

Matchers offer more flexibility in defining when a method argument is a match. Even better, you can chain matchers, combine matchers and implement your own.

Verifying

Use this to verify a method has been called with a given set of arguments N times.

fflib_ApexMocks mocks = new fflib_ApexMocks();
fflib_MyList.IList mockList = new fflib_Mocks.Mockfflib_MyList(mocks);
 
mockList.get(2);
mockList.get(4);
 
//Old school mocks
((fflib_MyList.IList) mocks.verify(mockList, 1)).get(2);
//Using integerBetween matcher
((fflib_MyList.IList) mocks.verify(mockList, 2)).get(fflib_Match.integerBetween(1, 10));

Stubbing

Use this to return a specific value when a method is called with a given set of arguments.

fflib_ApexMocks mocks = new fflib_ApexMocks();
fflib_MyList.IList mockList = new fflib_Mocks.Mockfflib_MyList(mocks);

mocks.startStubbing();

//Old school mocks
mocks.when(mockList.get(1)).thenReturn('One');
//Using integerMoreThan Matcher
mocks.when(mockList.get(fflib_Match.integerMoreThan(3))).thenReturn('>3'); 
mocks.stopStubbing();

System.assertEquals('One', mockList.get(1));
System.assertEquals('>3', mockList.get(1337));

Throw exceptions

If you want to throw an exception from a non-void method, just stub the method as above but use an exception as the return value.

fflib_ApexMocks mocks = new fflib_ApexMocks();
fflib_MyList.IList mockList = new fflib_Mocks.Mockfflib_MyList(mocks);

MyException ce = new MyException('Concrete exception'));
MyException me = new MyException('Matcher exception'));

mocks.startStubbing();
//Old school mocks
mocks.when(mockList.get(1)).thenReturn(ce);
//Using integerMoreThan Matcher
mocks.when(mockList.get(fflib_Match.integerMoreThan(5))).thenReturn(me);
mocks.stopStubbing();

try
{
    mockList.get(1);
    System.assert(false, 'Expected exception');
}
catch (MyException e)
{
    System.assertEquals(e.getMessage(), 'Concrete exception');
}

try
{
    mockList.get(23931);
    System.assert(false, 'Expected exception');
}
catch (MyException e)
{
    System.assertEquals(e.getMessage(), 'Matcher exception');
}


If you want to throw an exception from a void method, the syntax is similar to old school mocks.

fflib_ApexMocks mocks = new fflib_ApexMocks();
fflib_MyList.IList mockList = new fflib_Mocks.Mockfflib_MyList(mocks);

MyException ce = new MyException('Concrete exception'));
MyException me = new MyException('Matcher exception'));

mocks.startStubbing();
//Old school mocks
((fflib_MyList.IList) mocks.doThrowWhen(ce), mockList)).add('concrete');
//Using stringContains Matcher
((fflib_MyList.IList) mocks.doThrowWhen(me), mockList)).add(fflib_Match.stringContains('matchers'));
mocks.stopStubbing();

try
{
    mockList.add('concrete');
    System.assert(false, 'Expected exception');
}
catch (MyException e)
{
    System.assertEquals(e.getMessage(), 'Concrete exception');
}

try
{
    mockList.add('matchers are good');
    System.assert(false, 'Expected exception');
}
catch (MyException e)
{
    System.assertEquals(e.getMessage(), 'Matcher exception');
}

Combined matchers

Use this when you want to combine matchers. This can allow you to create ‘compound’ matchers, rather than proliferating matchers.
For example, anyInteger would not match to ‘null.’
If you want to match to anyInteger or null, you can combine the matchers using fflib_Match.anyOf(..) - instead of creating an anyIntegerOrNull custom matcher.

To invoke a combined matcher, call the combined matcher helper, passing in further matchers as method args (see the code sample below)
Combined matcher helpers


  • fflib_Match.allOf(..)
    • Match if the arg value matches ALL internal matchers.
  • fflib_Match.anyOf(..)
    • Match if the arg value matches ONE OR MORE internal matchers.
  • fflib_Match.noneOf(..)
    • Match if the arg value matches NO internal matchers.
  • fflib_Match.isNot(..)
    • Special case of fflib_Match.noneOf, with one internal matcher. Match if the arg value DOESN’T match the internal matcher.


fflib_ApexMocks mocks = new fflib_ApexMocks();
fflib_MyList.IList mockList = new fflib_Mocks.Mockfflib_MyList(mocks);

mocks.startStubbing();

mocks.when(mockList.get(1)).thenReturn('Concrete');
mocks.when(mockList.get((Integer)fflib_Match.anyOf(
        fflib_Match.isNull(), fflib_Match.integerMoreThan(3)
    ))).thenReturn('Matcher');
mocks.stopStubbing();

System.assertEquals('Matcher', mockList.get(null));
System.assertEquals('Matcher', mockList.get(84579));
System.assertEquals('Concrete', mockList.get(1));

Overloading Matchers

As with old school mocks, you can stub/verify the same method with multiple sets of argument values.

So if I have a method called Integer doStuff(Integer x), and I want to stub different values if it is called with 0, 1 or 2, I simply declare multiple when/thenReturns (see code sample below).

I can do exactly the same thing with matchers.

When you overload a method with matchers or concrete argument return values, actual argument values are compared to the expected argument values in REVERSE ORDER. This is consistent with behaviour in Mockito. This is the case even if you use concrete expected values, then matchers, then concrete, then matchers again.

fflib_ApexMocks mocks = new fflib_ApexMocks();
fflib_MyList.IList mockList = new fflib_Mocks.Mockfflib_MyList(mocks);

mocks.startStubbing();
mocks.when(mockList.get(1)).thenReturn('One'); //Concrete
mocks.when(mockList.get(fflib_Match.integerBetween(0, 10))).thenReturn('0..10'); //Matchers
mocks.when(mockList.get(3)).thenReturn('Three'); //Concrete again
mocks.stopStubbing();

System.assertEquals('Three', mockList.get(3)); //NOT 0..10!
System.assertEquals('0..10', mockList.get(2));
System.assertEquals('0..10', mockList.get(1)); //NOT One!

Implementing custom matchers

1. Considerations

Do you really need a new custom matcher?
Can you reuse one of the ApexMocks standard matchers, or other matchers defined in your product already?
Can you combine matchers to achieve the matching behaviour you want?
Do you really need a new custom matcher definition?
Can you get the same result by adding a new helper to register an existing matcher definition?
For example, in Apex an Integer is a kind of Decimal. A Long is a kind of Decimal. So in fflib_MatcherDefinitions, there are Decimal matchers. In fflib_Match, there are matcher registration helpers for integer, long and decimal, but they all use the Decimal matcher definitions.

2. Implement fflib_IMatcher

To create custom matchers, define a class that implements fflib_IMatcher.
You will need to implement the Boolean matches(Object arg) method defined on that interface.

Simple example:

//You can make this inner class private if it's only used in this test class
private class IntegerIsOdd implements fflib_IMatcher
{
    public Boolean matches(Object arg)
    {
        //Custom matching logic.
        //Make sure you think about null args, or args of the wrong type.
        return (arg != null && arg instanceof Integer) 
            ? Math.mod((Integer)arg, 2) != 0 : false;
    }
}

More complicated example:
This matcher requires supplementary information to determine if an argument is a match, which is specified in the constructor and stored as an internal variable.

private class IntegerIsMultipleOf implements fflib_IMatcher
{
    //Consider making internal variables final
    private final Integer toMatch;

    IntegerIsMultipleOf(Integer toMatch)
    {
        //Make sure you validate supplied args
        if (toMatch == null)
        {
            throw new fflib_ApexMocks.MockException('Arg cannot be null: toMatch');
        }

        this.toMatch = toMatch;
    }

    public Boolean matches(Object arg)
    {
        return (arg != null && arg instanceof Integer)
            ? Math.mod((Integer)arg, toMatch) == 0 : false;
    }
}

3. Add helper to register the matcher

You must call fflib_Match.matches(fflib_IMatcher matcher). This returns an Object whose value is null.
As Apex has no primitives, everything is a kind of Object. This means you can safely cast the returned value to the right argument type for your method argument.


For example, if I want to stub fflib_MyList.IList.get(Integer myIndex), I need to ensure I call the method with an Integer argument.
I can create helper methods to construct and register the appropriate matcher type and return an Object of the correct type for the method argument, which means the cast isn’t required in the actual test. This makes the test more readable (see code sample below).

private static Integer integerIsOdd()
{
    return (Integer)fflib_Match.matches(new IntegerIsOdd());
}

private static String integerIsMultipleOf(Integer toMatch)
{
    return (Integer)fflib_Match.matches(new IntegerIsMultipleOf(toMatch));
}

// Given
fflib_ApexMocks mocks = new fflib_ApexMocks();
fflib_MyList.IList mockList = new fflib_Mocks.Mockfflib_MyList(mocks);

// When
mockList.get(1);
mockList.get(2);
mockList.get(3);
mockList.get(4);
mockList.get(5);

// Then
((fflib_MyList.IList) mocks.verify(mockList, 3)).get(isOdd());

//Is easier to read but entirely equivalent to...
((fflib_MyList.IList) mocks.verify(mockList, 3)).get((Integer)fflib_Match.matches(new isOdd()));

Limitations

Mixing matcher and non-matcher args

You cannot call a method with a mix of matcher and non-matcher arguments. The code will compile, but will throw an exception at runtime. The exception message will tell you that you cannot mix matchers and non-matchers in a single method call.

This behaviour matches Mockito.
//All matchers - fine
mocks.when(mockList.get2(fflib_Match.anyInteger(), fflib_Match.anyString())).thenReturn('matcher');

//All concrete - fine
mocks.when(mockList.get2(1, 'hello')).thenReturn('concrete');

//Mix of matchers and non-matchers - not fine
mocks.when(mockList.get2(1, fflib_Match.anyString())).thenReturn('mix');

//All matchers again - fine
mocks.when(mockList.get2(fflib_Match.eqInteger(1), fflib_Match.anyString())).thenReturn('mix');

Registering matchers up-front, rather than as the method is being called.

You can declare matcher definitions up front, but you must register them during the method calls.

Matchers rely on the registration occurring immediately before the method to be stubbed/verified is called. After the method is called in stub/verify mode, it clears the matchers so the next method has its own set.

So this code will fail:
// Given
fflib_ApexMocks mocks = new fflib_ApexMocks();
fflib_MyList.IList mockList = new fflib_Mocks.Mockfflib_MyList(mocks);

//Registers the matcher and returns null, but the framework doesn't know
//which method the call is associated with
Integer intBetween = fflib_Match.integerBetween(1, 10);

// When
mockList.get(2);

// Then
//Not using the matcher, instead supplying a concrete argument whose value is null
((fflib_MyList.IList) mocks.verify(mockList)).get(intBetween);

But this code will pass:
//Declare the matcher definition but DON'T register it.
fflib_IMatcher hello1To3 = new fflib_MatcherDefinitions.Combined(fflib_MatcherDefinitions.Connective.AT_LEAST_ONE, new fflib_IMatcher[]{
    new fflib_MatcherDefinitions.StringContains('Hello1'),
    new fflib_MatcherDefinitions.StringContains('Hello2'),
    new fflib_MatcherDefinitions.StringContains('Hello3')
});

fflib_ApexMocks mocks = new fflib_ApexMocks();
fflib_MyList.IList mockList = new fflib_Mocks.Mockfflib_MyList(mocks);

mocks.startStubbing();
mocks.when(
        mockList.get2(fflib_Match.anyInteger(),
        (String)fflib_Match.matches(hello1To3)))
    .thenReturn('any');
mocks.when(
        mockList.get2(fflib_Match.integerLessThan(5),
        (String)fflib_Match.matches(hello1To3)))
    .thenReturn('<5');
mocks.stopStubbing();

System.assertEquals('any', mockList.get2(8, 'Hello1'));
System.assertEquals('any', mockList.get2(8, 'Hello2'));
System.assertEquals('<5', mockList.get2(3, 'Hello1'));
System.assertEquals('<5', mockList.get2(3, 'Hell
You may find this useful if you are setting up complicated matcher definitions, and want to reuse them. In this example, you could create a constant for the hello1To3 matcher definition.

Reference

Standard matchers in ApexMocks


Matcherfflib_Match helpers
Equalseq, refEq, (eq… all primitive types)
Anyany, anyBLA, (any… all primitive types)
DatedateAfter, dateBefore, dateBetween
DatetimedateTimeAfter, dateTimeBefore, dateTimeBetween
DecimaldecimalBetween, decimalLessThan, decimalMoreThan
DoubledoubleBetween, doubleLessThan, doubleMoreThan
FieldSetfieldSetEquivalentTo
IntegerintegerBetween, integerLessThan, integerMoreThan
NullchecksisNotNull, isNull
ListlistContains, listIsEmpty, listIsNullOrEmpty
LonglongBetween, longLessThan, longMoreThan
SObjectsObjectOfType, sObjectWith, sObjectWithId, sObjectWithName
StringstringContains, stringEndsWith, stringIsBlank, stringIsNotBlank, stringMatches, stringStartsWith

Note, some of these methods are overloaded rather than proliferating helper methods.
//Instead of having...
integerBetweenInclusiveLowerInclusiveUpper(Integer lower, Integer upper)
integerBetweenInclusiveLowerExclusiveUpper(Integer lower, Integer upper)
integerBetweenExclusiveLowerInclusiveUpper(Integer lower, Integer upper)
integerBetweenExclusiveLowerExclusiveUpper(Integer lower, Integer upper)

//We actually have:
integerBetween(Integer lower, Integer upper)

//Which calls into this method, passing in false for inclusiveUpper and inclusiveLower
integerBetween(Integer lower, Boolean inclusiveLower, Integer upper, Boolean inclusiveUpper)

More code examples

Search for the fflib_Match in fflib_ApexMocksTest.

2 comments:

  1. This is very helpful. I was/am puzzled as to why the library doesn't include matchers for setContains or mapContainsKey (or for that matter, any set/map matchers)?

    ReplyDelete
  2. There is a syntax error in the code sample for "If you want to throw an exception from a void method,..." Line 9 and 11 have an extra parenthesis in the arguments to doThrowWhen. Specifically, line 9 should be: "((fflib_MyList.IList) mocks.doThrowWhen(ce, mockList)).add('concrete');"

    ReplyDelete