fflib-apex-mocks icon indicating copy to clipboard operation
fflib-apex-mocks copied to clipboard

fflib_MethodVerifier could use more diagnostics

Open cropredyHelix opened this issue 5 years ago • 0 comments

the method throwException currently displays

Expected n, Actual m -- Wanted by not invoked <the qualified method> <custom message>

which can lead to head scratching when one tries to figure out why the verify fails.

  • Is the matcher wrong in the testmethod?
  • Is the code under test wrong?
  • Are the calls in the wrong order (fflib_InOrder use case)?

Since ApexMocks is recording all the actual method calls and the verify methods are comparing a single method call against all recorded calls of that method; it would seem useful to the developer to display an enumeration of the recorded method calls when this exception is thrown.

Here's an example where this would have helped me (example is a simplified version of the real code under test)

Code Under Test

public with sharing class MyCode {

	public void doStuff(List<Id> list0, List<Id> list1) {

		StuffService.add(list0);
		list0.clear();  // pay attention to this line
		StuffService.add(list1);
	}
} 

Service class being mocked

public with sharing class StuffServiceImpl implements IStuffService {
	static List<id> ids = new List<Id>();

	public void add(List<Id> idsToAdd) {
		ids.addAll(idsToAdd);
	}
}

Test method

@IsTest
private class MyCodeTest {
  @IsTest
  static void testInorder() {

    //	Given test data
    Id[] mockIds = new List<Id> {
			fflib_IDGenerator.generate(Account.SObjectType),
			fflib_IDGenerator.generate(Account.SObjectType),
			fflib_IDGenerator.generate(Account.SObjectType),
			fflib_IDGenerator.generate(Account.SObjectType)
    };
    //	Given mocks framework
    fflib_ApexMocks mocks = new fflib_ApexMocks();

    // Given mock Service
    StuffServiceImpl mockStuffService = (StuffServiceImpl) mocks.mock(StuffServiceImpl.class);
    // Given mock Service injected
    Application.Service.setMock(IStuffService.class,mockStuffService);

    //	Given inOrderMocks
    fflib_InOrder mocksInOrder = 
       new fflib_InOrder(mocks,new List<Object> {mockStuffService});

    //	Given code to test
    MyCode myCode = new MyCode();

    //	When called with distinct sets of Ids
    myCode.doStuff(new List<id> {mockIds[0],mockIds[1]},
				new List<Id> {mockIds[2],mockIds[3]});

    //	Then verify order of calls to mock service
   ((StuffServiceImpl)mocksInOrder.verify(mockStuffService,mocks.calls(1)
				.description('service sb called for ids[0] and [1]')))
				.add(new List<Id> {mockIds[0],mockIds[1]});
    ((StuffServiceImpl)mocksInOrder.verify(mockStuffService,mocks.calls(1)
				.description('service sb called for ids[2] and [3]')))
				.add(new List<Id> {mockIds[2],mockIds[3]});

  }
}

The first verify fails with

fflib_ApexMocks.ApexMocksException: In Order: Expected : 1, Actual: 0 -- Wanted but not invoked: StuffServiceImpl__sfdc_ApexStub.add(List<Id>). service sb called for ids[0] and [1].

...head scratching...

The reason why is that ApexMocks captures method non-primitive arguments by reference (rather than cloning them -- which is admittedly not always possible), so if the code under test changes the captured non-primitive argument before the ApexMocks verify is executed, the argument being compared (in my case):


((StuffServiceImpl)mocksInOrder.verify(mockStuffService,mocks.calls(1)
				.description('service sb called for ids[0] and [1]')))
				.add(new List<Id> {mockIds[0],mockIds[1]});

is no longer is equal to the argument that was captured because the code under test had cleared that argument! with list0.clear();

Five hours of my life went into discovering this after debugging the guts of ApexMocks and seeing that fflib_InOrder.verifyMethodCall was returning via getNextMethodCall() an argument of an empty list even though during method recording, the recorded arg was a list of two Ids. I should not have to dig into ApexMocks internals to see why the matching failed.

cropredyHelix avatar Aug 28 '19 20:08 cropredyHelix