4

I am unable to use the System.StubProvider to mock/stub a complicated Backend class of which I have no implementation but only an interface

public interface ShopBackend {
    void initialize();
    Boolean hasInStock(String item);
}

Here is my test using github.com/rsoesemann/apex-mocks

@IsTest
private class MockClass_Test {

    @IsTest
    static void expectCallsToMockedClass() {

        // Setup
        MockClass mock = new MockClass(ShopBackend.class)
                                .expects(new Call('initialize'))
                                .expects(new Call('hasInStock')
                                                .withParam('item', 'FizzBuzz')
                                                .returning(true));

        ShopBackend mockedBackend = (ShopBackend) mock.instance();


        // Exercise
        new Shop(mockedBackend).buy('FizzBuzz');


        // Verify
        System.assert(mock.verify());
    }
}

which fails in this line with this error:

System.AssertException: Assertion Failed: Parameter not expected: unnamed__0
Class.Call.validate: line 33, column 1
Class.MockClass.handleMethodCall: line 37, column 1
Class.ShopBackend__sfdc_ApexStub.hasInStock: line 7, column 1
Class.Shop.buy: line 14, column 1
Class.MockClass_Test.expectCallsToMockedClass: line 18, column 1

because Parameter names are not tracked correctly (stay unnamed__0) when mocked from an interface (not a class).

The test succeeds when I replace the interface with this empty class:

public class ShopBackend {
    public void initialize() {

    }

    public Boolean hasInStock(String item) {
        return null;
    }
}

This must be a bug or an undocumented Limitation that should be added here.

  • 1
    given that the apexmocks works using an interface (and apexmocks calls the stubAPI), it might be worth tracing the apexmocks code to see how it builds the StubAPI MockClass differently than your example – cropredy Jul 1 at 22:17
  • Fully agree. But the reason for writing my own mock lib was that the apex-mock was to hard to understand the internals. So it brings me back to my initial challenge 😬 – Robert Sösemann Jul 2 at 7:24
  • I can't disagree that I struggled with the ApexMocks syntax (especially the Matchers) at first - but with practice and careful reading of the various resources, I got it and then developed Illuminated Cloud live templates to drop in the necessary syntax wherever I needed it. Since I also ruthlessly exploit fflib pattern, the apexmocks pattern was rinse and repeat – cropredy Jul 2 at 14:02
6

This is subtle...

If I write an object to implement the ShopBackend interface, there's no requirement that this object chooses the same parameter names as the interface. It only has to match the signature in terms of types and order.

So, I can quite happily write

public class ShopFoo implements ShopBackend {
    public void initialize() {
    }

    public Boolean hasInStock(String foo) {
        return true;
    }
}

I've implemented ShopBackend correctly, but I've changed the parameter name of hasInStock from item to foo.

If you were to test that against mock which expects the parameter names to be the same, it would fail. But, in reality, that is a valid way of implementing the interface.

Why does it work with fflib-apex-mocks as per @stohn's answer? Because the fflib version is constructing the expectation of which methods are going to be called by recording what it hears via the Stub API. So, when you write

mocks.startStubbing();
mocks.when( mockedBackend.hasInStock(itemToBeTested) ).thenReturn(true);
mocks.stopStubbing();

fflib-apex-mocks is using the Stub API to make the list of what methods to expect. This means that if the Stub API turn the names into unnamed__0, it does it both on the expectation and the actual test execution. So, everything matches.

So, it could be better explained in the docs, but I think what you're attempting to do here is actually being too specific in your verification. You should only expect that the method names, and parameter types + order is correct. Expecting the parameter names is unnecessary and can lead to false failures.

| improve this answer | |
  • this is a great answer – cropredy Jul 2 at 14:00
2

With the installation of fflib-apex-mocks [1], a slightly different implementation of the Shop code, and using the included test class implementing fflib-apex-mocks, the interface could be successfully mocked.

Something could be amiss with the mocking implementation of your usage.

public inherited sharing class Shop
{
    private IShopBackend backend = null;
    private String shoppingResult = null;

    public Shop(IShopBackend backend)
    {
        this.backend = backend;
    }

    public void buy(String item_name)
    {
        backend.initialize();

        if (backend.hasInStock(item_name))
            shoppingResult = 'Item in stock';
        else {
            shoppingResult = 'Item IS NOT in stock';
        }
    }

    //=================================
    //  Testing functionality
    //=================================
    
    @TestVisible
    private String getResultStatus()
    {
        return shoppingResult;
    }
}
@IsTest
public class ShopTest
{
    @IsTest
    private static void test()
    {
        String itemToBeTested = 'FizzBuzz';

        // FFLIB mock implementation
        fflib_ApexMocks mocks = new fflib_ApexMocks();
        IShopBackend mockedBackend = (IShopBackend) mocks.mock(IShopBackend.class);

        mocks.startStubbing();
        mocks.when( mockedBackend.hasInStock(itemToBeTested) ).thenReturn(true);
        mocks.stopStubbing();

        // Exercise the target
        Shop s = new Shop(mockedBackend);
        s.buy(itemToBeTested);

        // Verify the results
        ((IShopBackend) mocks.verify(mockedBackend)).initialize();
        ((IShopBackend) mocks.verify(mockedBackend)).hasInStock(itemToBeTested);
        System.assertEquals('Item in stock', s.getResultStatus());
    }
}

[1] https://github.com/apex-enterprise-patterns/fflib-apex-mocks

| improve this answer | |
  • 1
    +1 for apexmocks solution although this doesn't really answer the OP – cropredy Jul 1 at 22:16
  • 3
    @cropredy Given the OP uses a mocking framework, I decided to test with fflib-apex-mocks to narrow the problem to either the platform or the poster's mocking framework. Like you said above, the culprit lies within the other mocking framework, which the framework authors can pursue. :-) – stohn Jul 1 at 22:58

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.