This answer is not intended to teach you everything about writing unit tests, nor to specifically answer every question, but to provide a quick summary and links to the resources that will help you move forward and develop more specific questions that SFSE can assist with.
Overview
Unit (and integration) testing is a big topic, but it starts with a small set of principles. If you've never written a unit test before, we strongly encourage you to complete the Unit Testing on the Lightning Platform Trailhead module and read at least the Month of Testing series. These materials and others are linked under Resources, below.
Fundamentally, testing comprises three steps, all of which take place within the context of a unit test:
- Creating test data as input for your code, which is designed to ensure that a specific logical path is executed. This can take the form of values in memory or of creating and inserting sObjects.
- Executing that code, meaning that that specific code path runs within a method annotated with the
@isTest
annotation.
- Writing assertions to demonstrate that the results of the code are correct and as expected for the given inputs.
Then, all code that is executed under step (2) is counted as covered under Salesforce's code coverage metrics. Code coverage is a side effect of high quality unit tests. Salesforce uses code coverage as a proxy to measure the presence of unit tests in your deployments.
Unit testing principles are quite general, and most Apex code is not special in the sense of requiring unique approaches to create a successful test. Techniques for implementing tests that perform all three steps are taught in the resources we include below.
Test Isolation
On Salesforce, all unit tests are executed in an isolated context. In this context, your code cannot see data in your organization, including ordinary records as well as Custom Settings. All data must be created via the unit test or @testSetup
method.
Metadata records, including Users and Custom Metadata, are visible in test context.
An older annotation, seeAllData=true
, allows tests to see all data in the Salesforce org. Use of this annotation is strongly discouraged for new unit tests, and is considered a very bad practice. It's important instead to follow the first step above, by designing test data as input for your code. This practice makes unit tests self-contained and repeatable, and insulates them against fragility stemming from data changes.
Smoke Tests (Tests without Assertions)
Unit tests that don't contain assertions are often called smoke tests. These tests have very limited value, because they show nothing other than that your code does not crash under a specific set of circumstances. They don't prove the code works or does what it's intended to do.
Resources
Trailhead
Apex Developer Guide
- Testing Apex
- The
Test
class reference, which includes a variety of testing-related utility methods, including Test.stopTest()
and Test.startTest()
, as well as methods for setting static SOSL results, controlling audit fields, working with mocks and stubs, and other tools.
Blogs and Articles
- Month of Testing series from the Salesforce Developers blog.
Dreamforce Video Content
Third-Party Testing Frameworks (Advanced Topics)
- ApexMocks, an open-source mocking framework for Apex.
- Force-DI, a framework for pervasive dependency injection.
Best Answer
Asynchronous Apex includes all of the methods for executing code on the Salesforce platform outside a synchronous transaction, including:
@future
methods.Because these constructs are by nature asynchronous, do not come with an SLA, and can be executed by Salesforce based upon overall system load and other considerations, we cannot typically guarantee exactly when they will be executed. This requires some changes to how we build and structure unit tests for code that is built within or makes use of any of the four asynchronous code types mentioned above.
Use of
Test.startTest()
andTest.stopTest()
Because of the way Asynchronous Apex works, any asynchronous code - a future method is a useful example - will not be executed during the confines of an Apex unit test unless we take specific action. A unit test forms a single transaction, and asynchronous code enqueued within that transaction cannot be executed until the transaction commits successfully.
For this reason, Salesforce has provided a framework to force asynchronous code to execute synchronously for testing: We enclose our test code between
Test.startTest()
andTest.stopTest()
. The system collects all asynchronous calls made afterstartTest()
. WhenstopTest()
is executed, these collected asynchronous processes are then run synchronously and complete before control returns to our code.Following
Test.stopTest()
, our code can evaluate the results of the executed asynchronous code and make assertions to validate its behavior.Nested Asynchronous Code
The collection and synchronous execution of Asynchronous Apex applies only between
Test.startTest()
andTest.stopTest()
. Any further asynchronous code that's enqueued by the asynchronous operations that are executed atTest.stopTest()
is not executed synchronously in the context of the unit test. For example, if we're working with the following code:A unit test structured like this will not work:
The second assertion will fail, because the batch class
ContactsUpdateBatch
, fired from within the asynchronousMySchedulable
, will not execute during test context - even though the first layer,MySchedulable.execute()
, is called atTest.stopTest()
.The same pattern applies to other multi-layer asynchronous code, including
@future
methods and Queueables.There's no work-around to allow multi-level asynchronous code to execute in test context. Instead, the tests must be constructed to validate functionality without requiring this, by decomposing the tests to validate smaller units and/or using techniques like dependency injection to validate the connections between different asynchronous code units.
The example above can be effectively tested by decomposition: we can write separate unit tests against the Schedulable and the Batchable to validate their operation. The Schedulable test would validate the update to the Account and that a batch has been enqueued; the Batch test would validate the associated Contact updates.
Batch Class Execution
Unit test context permits only one batch execution (call to
execute()
) to occur in a single unit test. While in most cases your unit tests would not insert more than one batch's worth of test data, it is possible to do so. This will result in an exception being thrown. Your unit test needs to guarantee that only one batch executes, either by controlling the batch size of the test data set or both.Batches that execute across metadata objects, such as
User
, are especially vulnerable to this challenge. While unit tests for these Batch classes may succeed in developer orgs or scratch orgs that have tiny record sets, they will fail when deployed to a larger production org. In many cases, these batches need at least a light dependency injection strategy to allow the unit test to control the queries executed bystart()
.For example, the query might be exposed in a
@TestVisible
instance variable to allow a unit test to inject a more restricted query, or adding anId
set to restrict the query results.Resources
Trailhead Modules