Thursday 23 June 2016

ApexMocks Capture Support

Capture Support

Today's post focuses on the new Capture feature in ApexMocks. This is an awesome contribution from my talented FinancialForce colleague Vincenzo Denti. This is another feature ported over from Mockito, filling out our ever growing unit testing toolbelt.

All code samples from this post can be found in this Github repo.

ApexMocks already allowed you to verify that a method was called a certain number of times, with a specific set of arguments.

Say you want to inspect one of those arguments to verify its state. You need to get a hold of the argument. In many cases, you supply the argument into the method under test, or you can stub the argument as a return value for a method.

Equally, there are instances when you don't have a reference to the method, perhaps because it is created inside the method under test. Maybe it generates a random token so you can't predict the value, or it mutates the object so that it is no longer equal to the Object you had a reference to.

Capturing exposes argument values for a particular method call, allowing you to:

  1. Perform further asserts.
  2. (Bonus) dump the argument while you write your unit test - making it easier to declare the expected argument values or debug failing tests.

Let's see this in action!

Example code

Let's construct a contrived sample application. The application is inspired by that classic disco tune "Get Down Saturday Night". You can thank me later for getting the song stuck in your head.

I've built this on top of ApexMocks and ApexCommon, so if you want to deploy this into an org you will have to deploy those first:

  1. ApexMocks
  2. ApexCommon


Let's take a look at the AwesomeService. This class has a dependency on the DiscoService class, which is injected using the Application factory pattern (discussed at length here). Given a user and a date, AwesomeService will either ask the DiscoService to assist the user in 'getting down' or notify the user by creating a DiscoService.Event.

AwesomeService
public with sharing class AwesomeService
{
    //Don't match to 'Sat' - instead format a known Saturday, to avoid localisation issues
    private static final String FMT = 'EEE';
    private static final String SATURDAY = Datetime.newInstance(1970, 1, 3).format(FMT);

    private static IDiscoService disco = (IDiscoService)Application.Service.newInstance(IDiscoService.class);
 
    public static void getDownSaturdayNight(User u, Datetime now)
    {
        if (now.format(FMT) == SATURDAY
            && u.LikesToParty__c //(everybody does)
            && u.CantWaitForTheWeekend__c)
        {
            disco.getDown(u);
        }
        else
         {
            DiscoService.Event event = new DiscoService.Event();
            event.EventTarget = u;
            event.EventType = 'Uncool';
            event.Message = 'It\'s hip to be a square';

            disco.notifyUser(event);
        }
    }
}

IDiscoService
public interface IDiscoService
{
    void getDown(User u);
    void notifyUser(DiscoService.Event event);
}

DiscoService.Event
public class Event
{
    public User EventTarget;
    public String EventType;
    public String Message;
}

Unit Tests

Unit testing the behaviour where the User is up for a party.
This is a simple enough case.

private static final DateTime KNOWN_SATURDAY = Datetime.newInstance(2000, 1, 1);

@isTest
private static void getDownSaturdayNight_getsDown_IfAppropriate()
{
    fflib_ApexMocks mocks = new fflib_ApexMocks();
    IDiscoService mockDisco = new Mocks.DiscoService(mocks);
    Application.Service.setMock(IDiscoService.class, mockDisco);

    //Given
    User u = new User(
        FirstName = 'Duff',
        LastName = 'Man',
        LikesToParty__c = true,
        CantWaitForTheWeekend__c = true
    );
 
    //When
    AwesomeService.getDownSaturdayNight(u, KNOWN_SATURDAY);

    //Then
    ((IDiscoService)mocks.verify(mockDisco)).getDown(u);
}

Unit testing the behaviour where the User refuses to party
private static final DateTime KNOWN_SUNDAY = KNOWN_SATURDAY.addDays(1);

@isTest
private static void getDownSaturdayNight_doesntGetDown_OnASchoolNight_WithMatchers()
{
    fflib_ApexMocks mocks = new fflib_ApexMocks();
    IDiscoService mockDisco = new Mocks.DiscoService(mocks);
    Application.Service.setMock(IDiscoService.class, mockDisco);

    //Given
    User u = new User(
        FirstName = 'Buzz',
        LastName = 'Killington',
        LikesToParty__c = false,
        CantWaitForTheWeekend__c = false
    );
 
    //When
    AwesomeService.getDownSaturdayNight(u, KNOWN_SUNDAY);

    //Then
    ((IDiscoService)mocks.verify(mockDisco)).notifyUser(matchesEvent( ??? )); //How do we verify the behaviour??
}

Captors to the rescue
We create an ArgumentCaptor, and supply it to the verify method. This brings the actual argument value into scope, allowing us to assert the state is as we expect it to be.

//Then
fflib_ArgumentCaptor argument = fflib_ArgumentCaptor.forClass(DiscoService.Event.class);
((IDiscoService)mocks.verify(mockDisco)).notifyUser((DiscoService.Event)argument.capture());

DiscoService.Event actual = (DiscoService.Event)argument.getValue();
System.assertEquals(actual.EventTarget, u);
System.assertEquals(actual.EventType, 'Uncool');
System.assertEquals(actual.Message, 'It\'s hip to be a square');

Alternatives

There are of course other ways to test this code.
  • Override equals/hashcode on the DiscoService.Event class - Example.
    • This approach relies on the vanilla ApexMocks matching behaviour - argument values are considered equal if their equals method says they are.
    • We leverage this by overriding equals and hashcodes on the DiscoService.Event class. 
    • This means we have to write and maintain extra production code for the benefit of test classes, which feels inherently wrong. We have to ensure that these methods work accurately as well, in case non-test code comes to rely on this behaviour.
  • Create a custom matcher - Example.
    • We can define an fflib_IMatcher instance, in which we can provide our own definition of whether or not argument values should be considered equal.
    • The trouble with this approach is that it is very verbose. It takes about 50 lines of code to achieve the same thing that captors achieved in 5 lines.

Conclusions

Captors are another tool in your arsenal of testing strategies. They can't be used to stub method return values, and in some situations it may be more appropriate to use matchers for behaviour verification (e.g. where a common matcher already exists).

But hopefully you can see the advantages they offer, and consider using them in your tests in the future.

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.