Unit Test – How to Mock a Void Service That Returns Values in One of Its Arguments

apexmockdependency-injectionunit-test

Let's say I have a typical fflib service class with a void method (Application.cls omitted for clarity)

public class MyService {
  public static void doStuffInSitu(String someContext, MyWrapper wrapper){
    service().doStuffInSitu(someContext,myWrapper);
  }
  private static IMyService service() {  // service factory
    return (IMyService) Application.Service.newInstance(IMyService.class);
  }
}

public class MyServiceImpl implements IMyService { // concrete impl of service

   public void doStuffInSitu(String someContext, MyWrapper wrapper) {
       // a lot of complicated stuff goes here
       wrapper.bar = 'someBarValue';  // set value in calling arg
       return;
   }
}

with inner type

public class MyWrapper {
  public String bar;
  public string foo;
}

all of this enclosed within some larger code path:

public class MyOrchestration{

   public void doOrchestration() {
     // do a bunch of stuff - set someMyWrapper.foo
     MyService.doStuff('abc',someMyWrapper);
     // do stuff that relies on myWrapper returning a value in .bar <== IMPORTANT
   }

with testmethod

  new MyOrchestration().doOrchestration();
 

Since I'm testing doOrchestration, i want the service it calls (MyService.doStuffInSitu(..)) to be mocked and return a mocked bar value back in its calling arg as that is the way the service was written.

How do I do this?

Best Answer

Since the service method being mocked is a void method, you can't use the ApexMocks thenReturn. You can use the doAnswer method of ApexMocks. I'm assuming some familiarity with ApexMocks already. If using Amoss, consult its documentation.

The normal setup for apex mocking except where noted on the line marked doAnswer

Test method

    fflib_ApexMocks mocks = new fflib_ApexMocks(); // establish mock environment
    
    //  mock and stub MyServiceImpl
    mockMyService = (MyServiceImpl) mocks.mock(MyServiceImpl.class);

    mocks.startStubbing();
    ((MyServiceImpl)mocks.doAnswer(new MockMyServiceAnswer(),mockMyService) // <== doAnswer
        .doStuffInSitu( // service returns values in its argument[1]
            (String) fflib_Match.anyObject(),
            (MyWrapper) fflib_Match.anyObject()); 
    mocks.stopStubbing();

    // Given mocks injected
    Application.Service.setMock(IMyService.class,mockMyService);

    // When code-under-test executed
    new MyOrchestration().doOrchestration();
    // then verify  -- e.g. verify mock UnitOfWork (not shown)

The above is saying -- that when method doStuffInSitu of the injected mock Service (mockMyService) is called with any String argument and any MyWrapper argument, return in the calling arg a value determined by the class MockMyServiceAnswer.

doAnswer takes two arguments - an object that implements fflib_Answer and the object where the answer should be returned in one of its calling arguments (i.e., the "answer")

So, let's look at the class that does the answering

 public class MockMyServiceAnswer implements fflib_Answer { 

   public Object answer(fflib_InvocationOnMock invocation) {

     MyWrapper myWrapper = invocation.getArgument(1); // [1] is the myWrapper arg in call to doStuffInSitu
     myWrapper.bar = 'mockedBarAnswer'; // enhance the arg w/ answer
     // return is only required if mocked method is not void   

   }
 }

That's it - you tell Apex Mocks via .doAnswer where your answering object is and let it return a value back to the mocked MyServiceImpl object's relevant argument

Notes

  1. You can make your Answer class parameterized using typical dependency injection through its constructor so several test methods can mock different values answered in the myWrapper.foo variable thus changing the paths through doOrchestration's business logic

  2. If you had multiple methods within MyServiceImpl that returned values within its arguments, then you'd have multiple classes, each implementing fflib_Answer. Your stubbing would associate each method with a different doAnswer Answer object.

  3. You can stub multiple answers for the same method call by using matchers if the arguments passed to doStuffInSitu vary based on your business logic and unit test case.

  4. If your method being mocked is not a void method but also modifies values back into its arguments, you can use the .thenAnswer(..) method. This is well covered in Answering with ApexMocks by Enzo Deti.

  5. Of course, if void method doStuffInSitu returned a value rather than returning values through its argument, you wouldn't use ApexMocks .doAnswer(..) but instead use .thenReturn(..).

Related Topic