Unit Testing: best practices and strategies to successful implementations

A good and adequate implementation of unit testing ensures that the system satisfies the requirements for which it was built, in addition to verifying that changes in the code do not affect them. Additionally, it should be noted that for a deployment to productive environments, at least 75% code coverage is required, which is achieved only with unit tests. In this blog we will be covering different aspects of unit testing to help developers in their creation and maintenance in order to gain the objectives.

Your team and ours working as one

Access Top IT Talent in your timezone at half the cost

Introduction to Unit Testing

As we mentioned above, testing is the key to successful long-term development and is a critical component of the development process. It ensures high quality for your Apex code while validating that the requirements have been met. Additionally, it lets you meet requirements for deploying Apex (Apex code can only be written in a sandbox environment or a Developer org, not in production), as unit tests result in code coverage of the classes and triggers of the org, and it is necessary a minimum of 75% of code coverage for successful deployments.

In this blog we will introduce and discuss some tools/techniques required to successfully develop unit tests.

Example

The org has this class, with a static method that calculates a temperature value in Celsius, given a temperature in Fahrenheit:

code

For the class, we created this test class and methods, to validate some example values but also to meet the code coverage.

code

In this case the code coverage of the TemperatureConverter class is satisfied with just one test method, so it should be enough with, for example, the first method in the test (testWarmTemp() → test a warm temperature as the common human would feel). We add some other test scenarios to include cases for freezing, boiling and negative, so we can validate the results are expected with different scenarios.

testFreezingPoint() → freezing temperature of water

testBoilingPoint() → boiling temperature of water

testNegativeTemp() → a negative temperature value as an entry (and in this case, but not as a rule, a negative value also as the output).

We can note some interesting tags in the test classes, such as @isTest annotation at class and method levels, the static keyword in the method definition (known as modifier), and System.assert sentences. We will cover them in the next sections.

@isTest and @TestVisible annotation for test and helper test methods

There are some rules a test method must accomplish to be considered as candidates to be run as part of a test suite, the first and more important is that a class containing test methods must be annotated with @isTest. That class can contain several methods, but only those annotated with the @isTest can be executed as a test, other methods can be private and will not be part of a test suite. Additionally, there can be private methods in non-test classes that may need to be tested in a test, those methods must be annotated as @TestVisible.

Private methods inside a test class: Include a private modifier instead of public, they can be invoked only inside the test class.

Public Helper methods in non-test class: Meaning they can be accessed outside the class that contains it, they can be used by test methods and classes.

Private Helper methods in non-test class: Meaning they can not be accessed outside the class that contains it, but if include a @TestVisible annotation, they can be used by test methods and classes.

public/private/static/void modifiers and signature in test methods

A method in a test class (annotated with @isTest) can be declared public or private (or without any – the default access modifier in Apex is private) to be considered as a test method. They must include the @isTest annotation, or similarly add a testMethod modifier before the void modifier in any place in the test declaration. Also, test methods don’t allow parameters, and must not return any value (void).

  • Annotation: @isTest or testMethod
  • Access modifiers: public or static
  • Signature: no parameters and no return any value

Initial data availability in unit testing

In a common scenario a test needs to interact with sObjects, and they need to be available during  the test execution. There are several mechanisms to have data available, to be consumed in a test, and they include:

  • TestSetup annotation in a method → @TestSetup
  • SeeAllData parameter in isTest annotation at class level → @isTest(seeAllData=true)
  • Creation of Test Utility Class, annotated with @isTest
  • LoadData artifact available as a static Test method -> Test.loadData()

@TestSetup

A method annotated with @TestSetup is invoked for every test that runs in a test suite execution. They can be responsible for inserting records/set environment variables that are going to be used in the subsequent tests in the same test class. All this data is available before a test starts running, and is rolled-back when the tests ends. Hence, each test has the same data available, but we don’t need to add them manually. We can create/update/delete more records in the test obviously, to accommodate for a particular test run.

@isTest(seeAllData=true)

Normally and following the concepts of testing processes, all data needed and updated during a test execution is not physically inserted in the database, Instead, the test framework maintains in memory the data schema and records, and it remains during the execution of all tests, after that it is destroyed in memory. But in some cases, it is very complex or hard to set up all the data for a particular test run, and in other cases it is necessary to read some environment variables or records as they are in the org. For those particular cases it is possible to use the data that we have in the system at test execution time.

By annotating your class with @isTest(SeeAllData=true), you allow test methods to access all org records. The best practice, however, is to run Apex tests with no org data access, using @isTest(SeeAllData=false). Depending on the API version you’re using, the default annotation can vary.

It is important to note that, when using SeeAllData=true, any insert/update/delete operation will not impact the database, it acts as a normal test execution instead.

You can annotate the test method as @isTest(SeeAllData=true) instead of applying @isTest(SeeAllData=true) at the class level.

@isTest(seeAllData=true)

Test Utility Class

A good practice is having a special type of class, a public class that is annotated with @isTest and can be accessed only from a running test. Test utility classes contain methods that can be called by test methods to perform useful tasks, such as setting up test data. Test utility classes are excluded from the org’s code size limit. We can include several methods in that class, and we can include one or more parameters, useful within the method for the task it is built for.

Example:

This method creates a specified number of accounts, with a related list of opportunities per each account.

@isTest

We can invoke this method ONLY from a test class, like this:

@isTest

Test.loadData()

There is another mechanism to manage the data available in the Org during testing, without too much code, and it is using the loadData() method provided by the Test class. ALl you need is a CSV file added in the org as a static resource, and you can reference it from the test method to return a list of sObjects.

The csv (multiple lines with comma delimited values) file must contain the list of API names in the first row, and the corresponding values in each of the following rows. For an empty value just don’t enter anything between the commas, all rows (including the first) MUST have the same number of commas. If a required field is empty, the system will throw an exception and will stop the execution, all validations for the sObject fields are being fired with the same behavior.

 

The Test.loadaData() method receives 2 parameters:

  • sObjectType
  • Static resource name

Example:

List of sObjects to be loaded: Account

  1. Create csv file with content:

Name,Website,Phone,BillingStreet,BillingCity,BillingState,BillingPostalCode,BillingCountry

sForceTest1,http://www.sforcetest1.com,(415) 901-7000,The Landmark @ One Market,San Francisco,CA,94105,US

sForceTest2,http://www.sforcetest2.com,(415) 901-7000,The Landmark @ One Market Suite 300,San Francisco,CA,94105,US

sForceTest3,http://www.sforcetest3.com,(415) 901-7000,1 Market St,San Francisco,CA,94105,US

 

2. Create static resource for the cvs file

Name: testAccounts

MIME Type: text/csv

 

3. Call Test.loadData in a test method to populate the test accounts.

Test.startTest() & Test.stopTest() sentences

During a test method definition, we can identify 3 sections according to what is being done in each of them.

Prepare valid Data for test:

First, we prepare the data (in addition to those data when we use a TestSetup method) that we need to have according to what the test method is going to validate.

Execute the methods to be tested:

Second, we fire the process on what we want to do the validation. And finally we add sentences to verify the validation (what we expect versus what is being returned). For step 2, i.e. when firing the code that we need to validate, we need to enclose that invocation within 2 specific sentences: Test.startTest() and Test.stopTest(). That causes that the system identifies and considers that as the part to be validated during the test.

Verify Results using assertions and make adjustments if it’s needed:

Then, during the 3rd section we validate the results of the functionality we are testing, using special methods (we will describe them next).

Use of Assert sentences (Assert / System.assert)

In the 3rd section, we mentioned this is the place where we add validations after the execution of the code we are testing. For those validations we use assert sentences, where we compare the expected result with the current result.

We have asserts methods in 2 different classes:

 

Assert class:

Assert.areEqual(expected, actual)

Assert.areNotEqual(notExpected, actual)

Assert.isNull(value)

Assert.isNotNull(value)

Assert.isTrue(condition)

Assert.isFalse(condition)

 

System class:

System.assert(condition)

System.assertEquals(expected, actual)

System.assertNotEquals(expected, actual)

 

When the expected result does not match the actual, or if the condition is not met, an exception is thrown.

Best Practices when writing unit testing

Make multiple test cases, try to cover all scenarios (positive, negative, empty, nulls).

Do not use seeAllData, use TestSetup and helper/utilities methods.

Verify results using asserts.

Test one aspect of code at a time, try to not cover several scenarios in the same method, use different methods for each scenario you want to test and validate.

Group tests in Test Suites.

Test-drive development is a technique where we create the test before implementing the code. Try to use it when applicable.

Conclusions

Building tests is a critical task in the development process, it ensures the correct implementation of all the components in the org, and that the results of each piece is the expected according with the requirements. We should strongly consider it as an important part of the build and must be part of the deliverable. You should run them periodically to identify some strange behavior or if something has been broken during development. Also, running them in each org where we deploy the code ensures nothing is forgotten. And last but not least, it is the unique mechanism to accomplish the code coverage of all we have written.

Why you should augment your team with Folder IT

Outsourcing or Augmenting your AI Team with Folder IT professionals is a cost effective solution that does not sacrifice on quality nor communication effectiveness. Our teams are qualified for working with all the latest technologies and for joining you right away.

Request a quote now for outsourcing your project or staff augmentation services to Argentina.

Get in Touch