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.
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:
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.
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
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