My first Salesforce CLI Plugin Part 3—Mocking an sfdx directory with Jest

This is the 3rd of part of this series, where I document my journey to creating my first Salesforce CLI plugin.

💡
Remember, the CLI plugin is meant to reorganize apex classes based on their prefix, so classes like fflib_UnitOfWork and fflib_Service would be grouped inside a fflib folder in an sfdx project directory.

Read part 1 to get more info on the use case and rationale.

You can read the previous parts here:

My first Salesforce CLI Plugin Part 1—The idea and progress so far
Join me as I document my journey to creating my first Salesforce CLI plugin. It’s going to change the world…
My first Salesforce CLI Plugin Part 2—Using Node.js to read files from an sfdx project directory
In part 2 of this series, we explore how to read files from an sfdx project to determine if they have a prefix.

Context

As I explained in part 2, I started developing this as a simple .js file that lived directly in the root of an sfdx project.

This is because by default, the script assumes that the apex classes are located at force-app/main/default/classes

function reoderFiles(classesPath='force-app/main/default/classes'){

    const files = fs.readdirSync(classesPath).map(file => `${classesPath}/${file}`)

    let filesByPrefix = new Map();
    filesByPrefix.set(OTHER_FILES,[]);

Notice that the parameter classesPath has a default value right in the method signature; this is known as default parameters in JavaScript. The idea is if you call the method without specifying the classesPath parameter (i.e like thisreorderFiles()), the default path will be used.

However, note that this is a relative path; I have no idea where this path exists in your computer; for example, the fully qualified path could be

/Users/pgonzalez/Documents/apps/dx-folders/force-app/main/default/classes

So, if you call the script without passing a classesPath parameter, it must be in the root directory of an sfdx project. Otherwise, you have to pass the parameter.

Anyway, the real point I wanted to make was that I started testing this script in a real sfdx project, and I could see the apex class files nicely getting organised in their respective subfolders.

The challenge

The problem with this approach is that every time I make a change to the script, I need to undo the new folder structure and put all the classes back in the default classes folder.

To do this, I would simply delete the entire classes folder and then issue the command sfdx force:source:retrieve -m ApexClass, which would re-create the folder, but that's hardly ideal, and it's too time-consuming.

Also, there's a dependency between my manual testing and this particular org, as the org has a bunch of apex classes with prefixes. What if I test this in an org with no prefixes? I wouldn't know if it's working.

So what I needed was a way to create the classes folder, with prefixed classes, on-demand, without having to rely on this particular sfdx project or Salesforce org.

This is the perfect use case for mocking.

💡
Mocking is a technique in software testing where a substitute object is created to simulate the behavior of a real object in the system. This allows developers to isolate and test specific components without using real objects, making it easier to identify and fix bugs.

Mock objects are used to test the interactions between different components or objects in the system without actually using the real objects, which may not be available or may have unwanted side effects during testing.


Thank you, ChatGPT :) 

Mocking the file system with Jest

Having the basics of my script working, I decided to move to a test-driven development approach while using Jest to mock the file system and create a mock structure of the classes folder of an sfdx project.

Jest doesn't provide an out-of-the-box mock implementation of the fs module in Node.js. Instead, you are encouraged to mock it yourself by hand!

No way I was going to spend time doing that. Surely someone had already fixed this problem.

Fortunately, I found mock-fs, which is exactly that: a mock implementation of the fs module. I found the documentation to be ok; this article from a random blog was actually more helpful in understanding how to use the mock.

Long story short: you have to create a javascript object that represents the file system folder you want to work on. Here's what I ended up creating as a mock sfdx project directory, and the classes folder in particular:

const project = {
    'force-app': {
        main: {
            default: {
                classes:{
                    'SRM_deployer.cls':'',
                    'SRM_deployer.cls-meta.xml':'',

                    'SRM_retrieve.cls':'',
                    'SRM_retrieve.cls-meta.xml':'',

                    'SRM_retrieve_Test.cls':`@IsTest
                                             private class MyTestClass{
                                            
                                            }`,
                    'SRM_retrieve_Test.cls-meta.xml':'',

                    'AccountService_Test.cls':`@istest
                                                private class MyTestClass{
                                            
                                            }`,
    ...//more code

You'll see that each nested property of the object represents as a folder, and then, each item in the classes object represents the actual file for the apex classes.

Note that I need 2 files per class: the actual code and the meta.xml file. I can also provide the contents of each file, which in my case doesn't matter, except for test classes, which I'll cover in a future chapter.

Then, in my test suite, I can have the following code

const mock = require('mock-fs');
const fs = require('fs');
const  reoderFiles = require('../../lib/reorderFiles');

describe('All tests', () => {

    const DEFAULT_PATH = 'force-app/main/default/classes/';

    beforeAll(async () => {
        mock(project);
        await reoderFiles();
    });

Here, mock refers to the function exported by mock-fs and project is the object that represents the sfdx project directory. So basically, before any test, I make sure to call the reorderFiles function, and it will act on the mocked file system instead of the real one.

Nice!

Tests examples

Now, let's see some examples of my Jest tests

test(`Both the .cls and .cls-meta.xml files for non-test classes should be moved
    to the "src" folder under the correct prefix"`, async () => {

        let files = [
            `${DEFAULT_PATH}SRM/src/SRM_deployer.cls`,
            `${DEFAULT_PATH}SRM/src/SRM_deployer.cls-meta.xml`,
            `${DEFAULT_PATH}SRM/src/SRM_retrieve.cls`,
            `${DEFAULT_PATH}SRM/src/SRM_retrieve.cls-meta.xml`

        ]

        files.forEach(file => {

            expect(
                fs.existsSync(file),
                `${file} does not exist`
            ).toEqual(true);

        })

    });

In this test, I put some of the SRM prefixed classes in a files array. Note that I'm specifying the new path, where each class is inside the SRM/src directory, instead of them being in the classes folder.

Then, I iterate over each file and use the fs.existsSync function to test that the file exists at the desired location.

Now look how easy it is to confirm that all the different use cases are working as expected

You can see the rest of the tests here

dx-folders/reoderFiles.test.js at 5812c98839785836dda64d64e40972cc67de27ec · pgonzaleznetwork/dx-folders
Contribute to pgonzaleznetwork/dx-folders development by creating an account on GitHub.
More tests using the mock sfdx project

Conclusion

I highly recommend this technique for anyone building CLI plugins that need to interact with an sfdx project.

If I have time, one day, I may create a much bigger JavaScript object that represents an entire project, including other files like .forceignore, etc., that would allow anyone building CLI plugins to get started with test-driven development quickly.

If you liked this entry and don't want to miss the next one, let me know

Subscribe for exclusive Salesforce Engineering tips, expert DevOps content, and previews from my book 'Clean Apex Code' – by the creator of HappySoup.io!
fullstackdev@pro.com
Subscribe