Originating from the principles of Extreme Programming in the late 1990s, Test-Driven Development (TDD) has revolutionized the way we build software, championing a forward-thinking approach that prioritizes testing at the heart of development. With Test-Driven Development (TDD), developers write tests before functional code in short, iterative cycles of testing and development. TDD may appear counterintuitive at first, but with practice you’ll reduce code complexity, prevent over-engineering, and promote bugs-resilient software. As a result, you’ll have more time to write new features and less time fixing bugs.
Here are some TDD tips and best practices:
To follow the Test-Driven Development (TDD) process, adopt the “Red-Green-Refactor” mantra as a guiding principle:
- Red: Begin by writing a test that fails and defining the expected behavior of the code you’re about to write.
- Green: Write the code necessary to make the failing test pass successfully, ensuring that your code meets the specified requirements.
- Refactor: Once the test passes, improve the code’s design, structure, and efficiency without altering its functionality.
We’ll use the “Red-Green-Refactor” mantra to write a Suitelet that loads a Sales Order record and modifies its ‘memo’ field. This walkthrough is a stepping stone showcasing how TDD can be used in your NetSuite development workflow.
Red Stage
Our first step is to create a failing test for our Suitelet script that we have not yet created. Start with creating an empty Suitelet.test.js file in the __tests__ directory. This file will contain the test cases for your Suitelet. Next, create an empty file Suitelet.js to the SuiteScripts directory. This file will contain the functional Suitelet code that you will be testing.
Next, we will establish a mock context, stipulate the behavior of mock functions, and test our Suitelet script using the mock context. Copy and paste this code sample to Suitelet.test.js:
import Suitelet from “SuiteScripts/Suitelet”;
import record from “N/record”;
import Record from “N/record/instance”;
jest.mock(“N/record”);
jest.mock(“N/record/instance”);
beforeEach(() => {
jest.clearAllMocks();
})
describe(“Suitelet Test”, () => {
it(“Sales Order memo field has been updated”, () => {
// given
const context = {
request: {
method: ‘GET’,
parameters: {
salesOrderId: 1352
}
}
};
record.load.mockReturnValue(Record);
Record.save.mockReturnValue(1352);
// when
Suitelet.onRequest(context);
// then
expect(record.load).toHaveBeenCalledWith({id: 1352});
expect(Record.setValue).toHaveBeenCalledWith({fieldId: ‘memo’, value: ‘foobar’});
expect(Record.save).toHaveBeenCalledWith({enableSourcing: false});
});
});
This test case verifies that the Suitelet script loads a sales order record, updates the memo field with a specific value, and saves the modified record back to NetSuite, all while adhering to the expected behavior and function calls.
Mocking Behavior:
- record.load.mockReturnValue(Record): Mocks the behavior of the record.load function to return a Record instance.
- record.save.mockReturnValue(1352): Mocks the behavior of the Record.save function to return a sales order ID.
Executing this test will result in failure, as we haven’t yet incorporated the functionality being tested in Suitelet.js.
Green Stage
Now, let’s write minimal code to pass our test. In our Suitelet, we need to implement the onRequest function which handles the HTTP request. In this code block, we’ll make sure the request is a ‘GET’ request, then use the N/record module to load and update the sales order record.
Now, Suitelet.js should appear as follows:
/**
* @NApiVersion 2.1
* @NScriptType Suitelet
* @NModuleScope SameAccount
*/
define([“N/record”], function(record) {
return {
onRequest: function(context) {
if (context.request.method === ‘GET’) {
const salesOrderId = context.request.parameters.salesOrderId;
let salesOrderRecord = record.load({id: salesOrderId});
salesOrderRecord.setValue({fieldId: ‘memo’, value: “foobar”});
salesOrderRecord.save({enableSourcing: false});
}
}
};
});
Running the test again should now result in success, as you’ve implemented the required functionality.
Refactor Stage
Finally, it’s time to refine your code. You’ll extract the code responsible for updating a record field into a separate function, named updateRecordField. This will make your code neater, more comprehensible, and allow for the functionality to be reused later if needed.
Here’s the refactored Suitelet.js:
/**
* @NApiVersion 2.1
* @NScriptType Suitelet
* @NModuleScope SameAccount
*/
define([“N/record”], function(record) {
return {
onRequest: function(context) {
if (context.request.method === ‘GET’) {
const salesOrderId = context.request.parameters.salesOrderId;
record.submitFields({
type: record.Type.SALES_ORDER,
id: salesOrderId,
values: {
memo: “foobar”
},
options: {
enableSourcing: false,
ignoreMandatoryFields: true // Optional: Set to true if you want to bypass mandatory field checks
}
});
}
}
};
});
In this refactor, we’ve replaced the record.load() and record.save() calls with a single record.submitFields() call in the updateRecordField() function. It updates the memo field of the sales order record with the value “foobar”. Using submitFields() can be a more efficient choice in terms of governance units when you only need to update one or few fields, and don’t need to load and manipulate the entire record. However, when you have to deal with sublists or need to execute business logic that relies on other field data from the record, you may need to use load() and save().
Next, we’ll modify Suitelet.test.js file to work with the record.submitFields() method:
import Suitelet from “SuiteScripts/Suitelet”;
import record from “N/record”;
jest.mock(“N/record”);
beforeEach(() => {
jest.clearAllMocks();
});
describe(“Suitelet Test”, () => {
it(“Sales Order memo field has been updated”, () => {
// given
const context = {
request: {
method: ‘GET’,
parameters: {
salesOrderId: 1352
}
}
};
record.submitFields = jest.fn();
// when
Suitelet.onRequest(context);
// then
expect(record.submitFields).toHaveBeenCalledWith({
type: record.Type.SALES_ORDER,
id: 1352,
values: {
‘memo’: ‘foobar’
},
options: {
enableSourcing: false,
ignoreMandatoryFields : true
}
});
});
});
This test checks that record.submitFields() is called with the correct parameters. It expects the memo field of the sales order record to be updated with the value ‘foobar’.
Now, use the command npm run deploy to validate your Suitelet test and deploy changes to your NetSuite account.
And there you have it—a complete TDD cycle writing a Suitelet with SuiteScript 2.1 and Jest. The true strength of TDD comes from its repetitive nature. By persistently writing tests and improving your code, your codebase will become easier to manage over time. While it’s optional, NetSuite strongly recommends using TDD to deliver dependable high-quality code. NetSuite’s SuiteCloud Development Framework (SDF) and SuiteScript are designed with TDD principles and provide a robust platform for testing custom functionality and automated testing implementation. Keep practicing and exploring TDD principles—you’ll quickly appreciate its benefits in your SuiteScript development journey.