BinaryVision

Mocking in Apex

March 26, 2024

Introduction

Mocking in Apex serves as an indispensable tool for effective testing, allowing developers to simulate behavior and precisely control the outcomes of function calls. This not only enhances productivity by eliminating the need for extensive integration tests but also facilitates seamless testing in a controlled environment.

Benefits of Mocking

  • Testing in isolation
    • Allows developers to isolate the code that is under test, providing consistent and accurate results.
    • No need to set up cumbersome amounts of data.
    • Avoids intermittent failures due to things like SObject locking.
    • We can easily simulate calling external services or resource intensive operations.
    • Makes it easier to make changes, when you have large, extensive integration tests. Any change in behaviour, even when intentional might mean changing a number of integration tests forcing developers to spend time updating the cumbersome integration tests.
    • When bugs are found, one can easily write a much simpler test to cover the newly discovered edgecase.
  • Performance
    • Because your code is being tested in isolation and not speaking to other classes directly, we can enable IsTest(IsParallel=true) worry free.
    • As we need to cover a minimum of 75% code coverage, for larger projects the extensive tests needed without mocking can take a considerable amount of time because of the amount of classes, triggers and synchronously run jobs(Queueables/Batchables) tests would need to run.
      • This is because any asynchronous jobs that are dispatched within Test.startTest and Test.stopTest will run synchronously.
  • Provides developer documentation. Because the function under test is isolated from other classes, a developer can determine what the intentions are.
  • Allows refactoring to improve code readability. Any issues that araise would be caught by the thorough tests.

Mocking Framework

Well, we want to create a mocking framework, thankfully, salesforce provides us a stub API. If you wanted to, it could end there. However it is quite clunky when it comes to providing mocks for multiple classes. But we can improve upon it by writing a wrapper around it.

I've created one for fun , the principle is quite simple. To Make assertions on the number of calls or that our mocked object was called with the expected arguments, we need to store the invocations(Method calls) , which contain the method name and its arguments at a minimum. By storing each invocation, we can assert the expected number of calls to the method.

For mocking the return values or perhaps throwing an exception when we expect to, we have need a matcher of some kind, I kept it extremely simple in my example by relying strictly on the method name. This is not ideal because you could have overloading methods potentially being called in the same function, unlikely and odd as that would be, I've not dealt with it for the sake of me making one for fun. It also does not currently respond to specific inputs but I've dealt with that via the Stub API. Which, assuming we've mocked the method, will return whatever the stub wants. The two most important of which would be the ReturnStub and the ExceptionStub. Which just return a value specified in the mock or throw an exception when called.

So in essence we've written a builder which creates the MockProvider which uses Salesforces StubProvider, when ever an invocation occurs on the mocked object, it will go through the logic we've implemented in our tests and return the mocked values or throw an exception

Choosing the Right Mocking Framework

While building your own framework can be insightful, leveraging existing open-source solutions offer tested and robust alternatives. These frameworks provide comprehensive functionality, clear documentation, and alignment with industry standards, ultimately streamlining the testing process and ensuring code reliability. In my case, for the sake of experimenting, I didn't implement complex comparisons between objects or anything of the sort(If I were, SObject.getPopulatedFieldsAsMap would be a savior but doesn't work for DTOs). These frameworks below however, have.

  • FFLib ApexMocks Framework
  • Apex Mockery
    • Provides all the same functionality as above.
    • Good documentation
    • Provided by salesforce itself
    • Not in widespread use.
    • It's consistent with the jest testing framework, which is great as that is what you would use when working with Lightning Web Components. It makes it easier to remember what the APIs are without having to refer to documentation.
    • I hadn't heard of this when I initially made my own for fun, the APIs are eerly similar to my own probably because we both drew on Jest for inspiration

Mocking in practice

I provided a simple example of the tests I wrote for my own framework. Practical implementation of mocking involves considerations such as creating mockable wrappers around native Salesforce classes and controlling interactions with the org itself. By adhering to best practices and leveraging mock frameworks effectively, developers can streamline the testing process and ensure the reliability and stability of their Salesforce applications. For unit tests, it's best we don't call methods like Database.query or Database.insert directly. For insertions, we want to just verify it was called with the expected arguments. When querying, we want to control what gets returned without having to insert records, not only does this improve performance, it makes the tests more reliable and easier to follow.

    
    Fauxpex.MockBuilder databaseMock = new Fauxpex.MockBuilder(DMLWrapper.type);

    databaseMock.method('query').willReturn(
        new Contact[]{
            new Contact(FirstName='Binary', LastName='Vision'),
            new Contact(FirstName='You', LastName='There')
        }
    );
        
        

Probably the most important API of all to wrap, is the DML operations. This is probably by far the API we all use the most. It handles all the CRUD operations, which we don't want to perform during unit testing and we just want to assert that the correct objects were saved. Below is a simple wrapper around one of the operations that I would recommend you make use of.

        
    class with sharing DMLWrapper {
        public void insert(SObject[] sobjectsToInsert) {
            // You could always expands the use of the wrapper,
            // check the FLS of the objects inserted with SObject.getPopulatedFieldsAsMap().keySet()
            // or check CRUD permissions. Centralizing it does make life easier.
            insert sobjectsToInsert;
        }
    }
        
    

This would allow us to check it would have created our expected SObject records.

    
    databaseMock.method('insert')
        .toHaveBeenCalledWith(
            new Contact[]{
                new Contact(FirstName='James', LastName='Bond')
            }
        );
    
    

Another classic you want have a wrapper for are queries. This is where the Repository/Selector pattern becomes useful, not only for centralizing queries for re-use, but also allows us to mock the query results. The pattern is exactly as it sounds, although the Repository can technically do other DML operations, it mostly provides a consistent interface for querying data.


    class with sharing ContactRepository {
        // The simplest method of all, of course this would allow you to
        // have a consistent location to also query other SObject relations
        // or handle dynamic queries.
        public Contact[] selectByIds(Id[] ids) {
            return [
                SELECT Id, FirstName, LastName
                FROM Contact
                WHERE Id IN :ids
            ];
        }
    }
    
    

Now that we've covered the essential elements of writing mockable code in Apex, we can remember writing testable code that isn't deeply coupled to the Salesforce APIs doesn't only improve the test performance. It allows for small, detailed tests that are easier to maintain and write, which allows for greater reliability and quality in your product.