Tips for Writing better Apex tests | SOLID Principles Apex

Telegram logo Join our Telegram Channel

Hello Trailblazers, In this post, we will discuss the advanced best practices and tips for writing the apex test classes for beginner and experienced developers. In my opinion and based on my experience of working with different teams, this is the most overlooked topic in Salesforce Development Ecosystem.

So let's break up the testing into three different parts. All parts are really important aspects to make your application more reliable and flexible and easy to maintain.

  • Write code that is easy to test and maintain.
  • Unit test for individual functions, and classes.
  • Integrate and Test.
Tips for Writing better Apex tests | SOLID Principles Apex

Unit tests vs Integration Tests

Unit tests are created to test the small functionalities of your code, unit tests are generally performed in isolation and are independent of other code. For example, you have a class that is used to add automatic discounts to Opportunity products based on some conditions. Let us understand this with the help of a class diagram and pseudocode. 


In the class DiscountUtils we have three static methods, respectively having responsibilities like

  • fetching the discount records from the database.
  • applying the discounts to products by matching the conditions.
  • calculating the discounted prices of the products.

Unit Tests

So unit testing will be testing each of the individual methods by providing test cases for them. You can at least have one positive and one negative test case for each method.

Unit Test Example: see how we can test fetchDiscounts() function and the same can be followed for others.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@testSetup static void makeData(){
    // create test data...
}

@IsTest
static void testFetchDiscounts(){
    List<Type> discounts = DiscountUtils.fetchDiscounts(arg1, arg2...);
    
    // assert the expected results based on your test.
}


Write code that is easy to test and maintain using SOLID Principles

This section is the key takeaway from this lesson. There are numerous ways how we can make our code easy to read and maintain, one of them being breaking the code into smaller pieces of reusable and testable functions. The key to writing tests is the ability to write multiple small tests with one or more scenarios instead of one big test with multiple scenarios.

Writing code that is easy to maintain is very subjective and can be different for different teams. But there are some widely accepted ways to do that. And, that is the SOLID Principles. These principles are mainly used for writing the actual code but not for writing tests.

So why am I talking about SOLID principles in the article that talks about writing tests? The answer is simple, if your code is well structured and easy to read for humans then it is comparatively easy to test.

Let's assume that you are assigned to the midst of a project, and you are tasked with changing some existing code that was written by someone else. Imagine how difficult it is to understand code that is not well documented, and does not follow the naming conventions.

In such cases, if any tests are failing or code coverage is not enough to do the deployments, then it will be difficult for you to write the tests. Also usually it takes more than the required time.

SOLID is an acronym for Object-oriented Design principles invented by Robert C. Martin and Barbara Liskov

It's very important to understand the basics of OOPS before you read about the SOLID principle. In most Salesforce projects first two principles can be easily applied. I seldom find applications for the rest three.

So don't worry if you don't understand those. You can always come back and refer to this post, whenever you want.

  • S - Single-responsibility Principle

    A class should have only one job to do, so we only should update that class when there is a change in that particular feature. There should be a single reason to modify the class.

    For example, we talked about the DiscountUtils class previously. we should use this class to put discount-related logic only.

    Another example is that we always have a separate trigger handler for each object in Salesforce.

    From the testing perspective, this principle means that one test method should test a single functionality or business process. So we need to update the particular tests where the code is modified. This principle is applicable to both Unit tests and Integration tests.

    In simple words, the idea is to avoid modification of the tests when not needed and keep tests simple and loosely coupled.

  • O - Open-closed Principle
    This principle means that the class/function/component should be open for extension but closed for modification. For example, if you have a class that calculates the discounts, then you should only change that class when there is a change in the discount logic.

    So my idea is to avoid writing everything in a single test method. Based on the business requirements and scenarios we must break down the tests.

    Example: Let's say we have a class that handles order payments. in the Sales Order flow, users can choose one of the two payment methods: 

    a: Credit Card
    b: Paypal

    We must have two test methods for each scenario. So that we don't need to update the Credit Card related tests when there is a change in Paypal flow.

  • L - Liskov Substitution Principle
    This principle tells us that if class A is the parent of B then A can be replaced by B without making any code changes to A, also program should not run into any errors if A is replaced by B.

    In simple words, the parent class in the inheritance should only have functions or properties that are applicable to all its children.

    For example, you can drive all vehicles but not all vehicles can self-drive, so it does not make sense to add the enableSelfDriveMode() function to the parent class but it is important to add drive() method to all vehicles. See below code example.

    public abstract class Vehicle {
        public abstract void drive();
    }
    
    public class Car extends Vehicle {
        public void drive() {
            System.out.println("Driving a car!");
        }
    }
    
    public class Tesla extends Vehicle {
        public void drive() {
            System.out.println("Driving a Tesla!");
        }
    	
        public void enableSelfDriveMode() {
            System.out.println("Self-Drive mode enabled!");
        }
    }
    
    public class TestLiskov {
        public static void main(String[] args) {
            List<Vehicle> vehicles = new List<Vehicle>{
                new Car(),
                new Tesla()
            };
            
            for (Vehicle v : vehicles) {
                v.drive();
            }
        }
    }
  • I - Interface Segregation Principle
    This principle is very simple it tells us to keep two interfaces separate and break them into smaller interfaces so that the classes need to implement only the methods that they need.

    A very good example is the standard Database.Batchable interface. How many times it has happened you don't need the finish() method? Many times right? Most of the time we don't use it but, we must implement it because it is not in the separate interface. So the idea is to have its own interface and we implement that only when it is needed.

    You might be tempted to say that that is not a big problem to have an empty method. But this is an oversimplified example. Most of our projects are way too complicated than this.


  • D - Dependency Injection Principle
    This principle says that our classes should not be too much dependent on each other or should not be tightly coupled in other ways. This principle may seem daunting but it is not as difficult to understand as it seems.

    So let us understand it using a simple example below. We have three different payment methods for orders and they have their own implementation for that. But, the common order process class is using all three classes and it can not be deployed without those three classes.

    public class StripePayment {
        public void makePayment(Order order){
            // assume payment was successful
            order.Payment_Status__c = 'Done';
        }
    }
    
    public class PayPalPayment {
    	public void makePayment(Order order){
            // assume payment was successful
            order.Payment_Status__c = 'Done';
        }
    }
    
    public class CreditCardPayment {
        public void makePayment(Order order){
            // assume payment was successful
            order.Payment_Status__c = 'Done';
        }
    }
    
    public class OrderProcess {
        public void processOrderPayment(Order order){
            if(order.Payment_Mode__c == 'Stripe'){
                StripePayment stripe = new StripePayment();
                stripe.makePayment(order);
            } else if(order.Payment_Mode__c == 'Stripe'){
                PayPalPayment paypal = new PayPalPayment();
                paypal.makePayment(order);
            } else if(order.Payment_Mode__c == 'Stripe'){
                CreditCardPayment crediCard = new CreditCardPayment();
                crediCard.makePayment(order);
            }
            
            update order;
        }
    }
    

    There are several problems with the above approach
    • Every time there is a new payment method added we need to update the OrderProcess class. And it gets complicated over a period of time.
    • It will fail the CI-CD process if any of the dependent classes are having any problems.
    • We can not create mockups to test the classes.
    • We can not change order service at run time without changing the code.
    • So how do we fix the problem?

      First, we will create an Interface and replace the payment service with that so we are not dependent on any class directly. Now as we see in the below code the OrderProcess class is not directly dependent on any of the payment services directly.

      Also, the class that is called the OrderProcess can still decide what payment service to use. 

      Now if any new payment service is added, we just need to implement the interface IPaymentService and don't need any change in the OrderProcess Class.

      public class OrderProcess {
          public void processOrderPayment(Order order, IPaymentService paymentService){
              paymentService.makePayment(order);
              update order;
          }
          
          public interface IPaymentService{
              void makePayment(Order order);
          }
          
          public class StripePayment implements IPaymentService{
              public void makePayment(Order order){
                  // assume payment was successful
                  order.Payment_Status__c = 'Done';
              }
          }
          
          public class PayPalPayment {
              public void makePayment(Order order){
                  // assume payment was successful
                  order.Payment_Status__c = 'Done';
              }
          }
          
          public class CreditCardPayment {
              public void makePayment(Order order){
                  // assume payment was successful
                  order.Payment_Status__c = 'Done';
              }
          }
      }
      

    • We can also take this one step further by using custom metadata in the truly dynamic payment service sectors. That is very nicely explained in this blog: Breaking Runtime Dependencies with Dependency Injection
    • Also, you can this on YouTube for better understanding: Write Flexible Apex using Dependency Injection | Developer Quick Takes

    Closing Thoughts

    These practices are very helpful and really help us to write better code. These principles are just guidelines and could not be applied to all use cases. So it's always better to plan not to overuse them. Also, we need to consider the trade-offs that come with using these principles.

    Hope this was helpful!!




    No comments :
    Post a Comment

    Hi there, comments on this site are moderated, you might need to wait until your comment is published. Spam and promotions will be deleted. Sorry for the inconvenience but we have moderated the comments for the safety of this website users. If you have any concern, or if you are not able to comment for some reason, email us at rahul@forcetrails.com