Test classes are pieces of code used to evaluate other pieces of code to ensure everything is functioning correctly and reliably. These can act as a sort of early warning system, alerting us that bugs or issues have arisen, even if we aren’t modifying any code.
If you’re not a developer, you may not be familiar with the concept of testing code. But trust me, it’s important! This is especially important when code is used to power critical business processes and any issues that arise in production can be hugely expensive in terms of cost to the business, time spent debugging and resolving the issue, and most importantly, in the elevated stress levels!
The Different Types of Programmatic Tests
Test classes can be broken down into a variety of different types, which changes how we would write a test and its expected results. For Apex test classes, these usually fall into one of the following three categories:
- These tests are the most basic of our tests, we take an individual method and test that when given an output and if it returns a specific output. If it does not, we fail the test.
- This type of test validates that our smaller units of codes work well together, without any unexpected behavior.
- A much more “real-life” test, here we test the business logic of our code, ensuring that in a real life scenario, the code works as expected. These might commonly be referred to as “end to end” testing.
Salesforce Code Coverage Requirements
As you’re probably already aware, Salesforce mandates a minimum of 75% code coverage for a deployment to production: that is if a method has 4 lines of code and it is executed in a test, it will generate 4 lines of coverage or 100% coverage for that method.
Salesforce considers an aggregate of all none-test code, and lines of code covered by executed tests when deploying Apex code to a production environment and will block the deployment if it is below 75%.
The Myth of Code Coverage
Code coverage is a useful way to gauge the completeness of the tests, but shouldn’t be considered the only or even the primary measure of the quality of the tests. While achieving the minimum 75% code coverage requirement is essential, it is far from a guarantee that the code is free from bugs and issues and will actually work as expected.
The main problem with relying solely on code coverage is in its very nature, it only measures the lines of code that are executed during a test, not the quality of those tests. For example, if you have a method with 10 lines of code and you write a test that only executes 5 of those lines, you will have achieved 50% code coverage. This does not factor in the different scenarios in which a piece of code may be executed.
Another issue is that code coverage doesn’t consider the complexity of the code being tested. A method with just a few lines of simple code may be easy to test and achieve 100% code coverage, but a more complex method with many lines of code may be much harder to adequately test, even if it achieves the same percentage of code coverage – this can lead to important pieces of logic being poorly tested.
What Makes Good Tests?
To make good tests, first we need to fully understand the requirements and functionality of the code so that we can plan out and design tests which are comprehensive, focused, and reliable. What exactly does this mean?
- Comprehensive: Cover all, or as many as possible, scenarios which may be experienced by the code. This includes cover edge cases, unexpected and rare situations which may occur: for example, if we have a method which took in a number between 1 and 10, an edge case test would be giving it a number outside this range.
- Focused: Covers a specific function or aspect of the code. This is usually achieved by breaking down complex pieces of code into smaller methods which do very specific functions and testing those individual functions work as expected.
- Reliable: This one can be broken down into several items:
- Repeatable: Regardless of when, who, where or how the tests are run, it should always produce the same result.
- Isolated: Changes in external factors should not influence the test. This includes dependencies on other pieces of code or other customizations in the org.
- Maintainable: As the system evolves, it should be easy to update and change the tests. For example, let’s say we add a new required field on an object – it should be easy to update our tests to account for that.
Mocking and Stubs
We can further augment our tests by designing our code to utilize dependency injection. This is where we make it so our code’s external dependencies – that is code in other classes – replaceable during a test execution with a mocked instance of it, which we call a stub. When the methods are called on the dependency, the stub is invoked instead, allowing us to skip executing the dependencies code, and returning a known result for that test. Let’s walk through the entire process.
- First off, we need to create the code which will be consumed in other classes, and this could be something as simple as a Database Access Object, controlling DML and queries, all the way through to something much more complex like a service class acting as an entry point for an integration with an external system.
In this example, we are going to write all of our functional methods for our dependency as instance methods, instead of static. This will enable us to mock these methods during our test run. Also, we need to create a way for other classes to access the instance, regardless of whether it has been mocked or not.
For this we can use something similar to a Singleton, having a getInstance method which creates a single instance of our class, stores it, and gives it out when requested. Unfortunately, due to limitations with the Stub API, we must leave a public constructor; ideally we would rather have it be private to guarantee the only mechanism to get an instance of this class would be to go via the getInstance method.
- Next, we need to modify our consuming class to utilize the getInstance method. This is a very easy change and we can either chain methods if we don’t need to use multiple functions, or we could assign it to a variable if we wanted to access it several times (to improve readability).
- Now, we can start building our stub – this is where the magic happens. In its simplest form, we simply need to implement the StubProvider interface and its handleMethodCall method – this is the method that will actually be called when a method is called on our stubbed instance. Depending on the complexity of our methods being called, or simply whether there are multiple of them which could be called, we can utilize the method arguments to decide upon exactly what we wish to do. At its simplest, we can simply return a hardcoded value.
In the example below, we have also added a static setStub method to the stub class, this acts as a super easy way for us to initialize and assign our stub to the instance variable of the original class. This gives us a nice and easy way for tests to assign that variable with a stubbed instance, meaning that when they call the getInstance method, they get this stub instead.
- Finally, the last thing we need to do is to actually write our tests. In the example below, we can see that all we need to do is call the setStub method on our DependencyStub class, and bingo, our test is now utilizing our stub, completely bypassing our dependency during the test run! Both of the test methods below would pass and are purely to highlight the fact we’d get different values from our stub. In practice, your unit tests would be unlikely to test with and without a stub.
This guide is a very basic example of how we can use a stubbed class and easily switch it out. What you decide to stub really comes down to context, however, certain scenarios can benefit more from stubs and mocking than others. For example, integrations are a prime example of where planning for mocking can be a great benefit, allowing you to easily decouple your business logic from your integration layer.
With a little creative thinking, you can easily create a generic stub class for use across a wide variety of situations. However, in some, more complex, scenarios, creating a bespoke stub can have its advantages over using a generic one. The pattern above is also just a simple way of allowing a class to be mocked, a more advanced – and usually better – method is to utilize dependency injection, where instead we pass the dependent class instances into our consumer classes constructors.
Writing effective and reliable tests for your Apex code is an important part of ensuring the quality and functionality of your code. By properly testing your code, you can identify and fix errors or defects before they cause issues in production, saving the day before it had chance to ‘go bad’. Rather than being a chore, with the right approach, writing Apex tests can be a breeze and enjoyable, which can then act as a faithful companion, ready to alert you before issues arise.