Writing solid tests is crucial in order to build reliable and efficient business applications. In this post, we’ll do a refresher on unit tests and we’ll present apex-mockery, a lightweight Apex unit testing library that helps you write truly decoupled Apex unit tests using mocks and assertions. We’ll share code samples to help you understand how you can use the library to build easy-to-understand and fast-running unit tests.
A refresher on unit tests
Before we take a look at the apex-mockery library, let’s take a step back and go through some of the basics of unit testing from a technology-agnostic point of view. Then, we’ll look at Apex and discuss why most of us should write unit tests instead of integration tests.
Unit tests are at the base of the test pyramid
Software engineering encompasses multiple types of tests: unit, integration, service, functional UI, end-to-end, user acceptance, and more. As Martin Fowler puts it, we can represent a good balance between these types of tests within the scope of a project by representing them as a pyramid.
Labels (test types) can change, but the key principle here is that the tests that run fast and often should lie at the bottom of the pyramid. These are the easiest to implement and maintain (thus costing less). Then, as we climb up to the top, we increase complexity and cost: tests run slower and become harder to implement and maintain.
In the context of this post and for the sake of brevity, we’ll focus solely on unit tests. These are the first that you should implement in any project, and they should be a priority in your testing strategy.
By definition, unit tests are meant to test the smallest amount of code (a unit) of a project. Unit tests must only rely on pure logic and be completely decoupled from their dependencies (other classes) and boundaries (other services, such as data storage or web services). Unit tests must run fast; they do not require a particular test setup, such as data to be inserted in the database, and they require you to mock the dependencies of the class under test.
Write Apex unit tests instead of integration tests
Apex benefits from tight integration with the Salesforce Platform, and while this feature is great for things like quickly and easily accessing the database, it blurs the lines of the separation of concerns between logic and services. As a consequence, it’s very easy to write Apex integration tests instead of unit tests. For example, Apex code is often tested in conjunction with the database using @TestSetup
and DML statements. While these integration tests help to achieve coverage, they rely on the database and thus take more time to run than “pure” unit tests.
As Mitch Spano shared in his pure Apex unit tests presentation, most of the time, it is not necessary to rely on integration tests for testing high-level software layers, such as LWC controllers, service, and application layers. Thanks to the Apex Stub API released back in Spring ’17, developers can break away from those dependencies in the context of tests by building their own unit test library/framework or using an existing one like apex-mockery.
Introducing apex-mockery
As part of Salesforce’s engineering work, we were developing an internally managed package and we needed a library to write unit tests. We wanted to write simple “arrange” steps (as in the Arrange-Act-Assert pattern), write understandable assertions, and mock our dependencies. We looked across the ecosystem for an easy-to-read and well-tested library that we could use to build our product, but we didn’t find a perfect match, so Antoine Rosenbach, Lionel Armanet and Ludovic Meurillon decided to write their own. We were so happy with the final library implementation that we decided to release it as open source under the name apex-mockery.
The apex-mockery library provides a simple, lightweight, and easy-to-read mocking library for Apex built using the Stub API. The library is built so that it is simple to use and provides the best developer experience possible when generating mocks and stubs, configuring spies, and writing assertions.
We’ll walk you through a sample scenario so that you can understand the power of the library with some practical examples. Then, we’ll show you how you could write tests for this sample project in three steps:
- Create mocks and method spies
- Stub spied methods
- Write assertions
Sample scenario: bakery orders and delivery
Consider the following sample scenario: a bakery takes in pastry orders and plans deliveries using a dedicated service. The only pieces of data that we’re considering in the context of this scenario are the pastry names and their delivery date.
Below is the basic implementation of our bakery scenario (full code is available in the project repository).
Pastry.cls
DeliveryService.cls
DeliveryServiceImpl.cls
OrderConfirmation.cls
Bakery.cls
Now that we’ve had a glimpse at our sample project, let’s take a look at how we could write tests for the Bakery.order
method.
Step 1: Create mocks and method spies
In order to work, the Bakery
class needs a DeliveryService
instance to be passed in its constructor. In a production context, the service is provided with a concrete DeliveryServiceImpl
instance like so:
However, in the context of unit tests, you should not use a real service instance to ensure decoupling. In other words, DelivertServiceImpl
will be tested unitarily on its own, so you do not need to test the two classes integrated together. You can replace the service dependency with a mock that implements the DeliverService
interface.
Here’s how you can easily create and inject such a mock, thanks to apex-mockery:
Then, your test needs a spy, so that you can drive the behavior of the planDelivery
method and run assertions on its calls.
Now that you have a mock service and a spy on its planDelivery
method, let’s look at how can you configure your spy and run assertions on it.
Step 2: Stub spied methods
Once you have a mock instance, you can drive how its methods behave by controlling their return values and throwing exceptions.
Use the returns
and throwsException
methods to specify a default behavior that is applied to all calls to the stubbed methods. Then, if needed, you use a combination of whenCalledWith(<args>).thenReturn
and whenCalledWith(<args>).thenThrow
to apply specific behaviors to method calls matching the specified arguments.
During test execution, apex-mockery starts by looking for a match in the configuration supplied by whenCalledWith
. If none is found, it falls back to the default configuration (returns
or throwException
).
Let’s look at some common stub configuration situations (see more recipes).
- Return something every time
planDelivery
is called
- Throw an exception every time
planDelivery
is called
- Return something when called with a specific argument
- Throw an exception when called with a specific argument
Now that you know how to drive the behavior of your mock, you can add assertions to test your code.
Step 3: Write assertions
apex-mockery provides a fluent assertions API. As soon as you start your expectation with Expect.that(mySpy)
, you’ll have access to several assertions methods. The library comes with a number of easy-to-use behavior assertions such as:
If the base argument matchers are not enough for your needs, you can also create your own custom argument matchers.
Piecing the full example together
Now that we saw the individual steps, let’s wrap it up and take a look at our test for the Bakery.order
method. Notice how you can use apex-mockery assertions, along with with the standard Apex assertions from the system.Assert
class, in your tests.
Closing words
This concludes our tour of unit tests and the apex-mockery library. You learned how decoupled unit tests are easier to write and run significantly faster. Having fast tests shortens the development lifecycle feedback loop, reduces CI workflow run duration, and speeds up deployments. These factors empower developers to deploy and run tests often, thus improving quality.
apex-mockery helps you steer your project in this direction. Check out the project’s repository to get started. You’ll find the library’s documentation with the installation instructions options (source install or unlocked package), some sample recipes, and a migration guide. Happy unit testing!
About the authors
Ludovic Meurillon is a Software Engineer with the Service Cloud team in Grenoble, France. He pushed code to production for years, enjoys removing more lines of code than he adds, and prefers pair-programming over code reviews and working products over perfect design. Follow him on Twitter @LudoMeurillon or check his GitHub projects @ludomeurillon.
Sébastien Colladon is a CTA and Software Engineer with the Service Cloud team in Paris, France. He loves to contribute to making the Salesforce ecosystem a better place and enjoys learning and working with others. Check his GitHub projects @scolladon.
Philippe Ozil is a Principal Developer Advocate at Salesforce where he focuses on the Salesforce Platform. He writes technical content and speaks frequently at conferences. He is a full-stack developer and enjoys working on DevOps, robotics, and VR projects. Follow him on Twitter @PhilippeOzil or check his GitHub projects @pozil.