Comprehensive Guide to Unit Testing in CAP

Estimated read time 15 min read

Unit testing is a vital component in software development, ensuring that individual parts of the application work correctly. In the context of CAP (Cloud Application Programming) and Node.js projects, unit testing helps maintain high code quality and reliability. This guide will walk you through the essentials of unit testing in CAP, including Test Driven Development (TDD), using JEST for unit tests, integrating unit tests into CI/CD pipelines, setting up your project, and practical examples.

1. Test Driven Development (TDD)

Test Driven Development (TDD) is a software development process where you write tests before writing the code that needs to be tested. This approach ensures that the code is continuously validated against a predefined set of tests, leading to higher code quality and fewer bugs.

 

Testing Pyramid:

Unit Tests: Automated tests that test individual functions or components in isolation.Integration Tests: Tests the interaction between multiple components or systems.System Tests: Tests the entire system as a whole.UI Tests: Tests the user interface, e.g., WDI5 (WebDriver Integration for UI5).

Best Practices for Writing Unit Tests:

Write Small, Focused Tests: Ensure each test focuses on a single functionality or component.Use Mocking and Stubbing: Avoid external dependencies by simulating complex objects or services.Arrange, Act, Assert Pattern: Structure tests by arranging conditions, acting by calling the method, and asserting the expected outcome.Run Tests Frequently: Integrate unit tests into the CI/CD pipeline to catch issues early.Test Edge Cases: Ensure robustness by covering potential failure points.Keep Tests Independent: Avoid dependencies between tests to prevent cascading failures and simplify debugging.

2. Unit Testing in CAP Using JEST

JEST is a comprehensive testing framework designed initially for React applications but now widely used across various JavaScript frameworks, including Node.js.

Tools for Unit Testing in CAP/Node.js Projects:

Option 1: Use Mocha, Chai, and Sinon.js:

Mocha: Unit test framework.Chai: Assertion library.Sinon.js: Mocking library (stubs).

Option 2: Use JEST:

JEST: An all-in-one unit testing framework developed by Facebook. It can be combined with Chai for enhanced capabilities.

For detailed documentation, visit the JEST website.

3. Unit Testing in CI/CD Pipelines (e.g. Azure)

Integrating unit tests into CI/CD pipelines ensures that code changes are continuously validated. Below is a sample config.yml file for setting up unit testing in Azure pipelines:

 

 

 

general:
buildTool: ‘mta’
productiveBranch: ‘main’
pipelineOptimization: true
repository: ‘CAPUnitTest’
owner: ‘UT-TEST’
nativeBuild: true
stages:
Build:
karmaExecuteTests: true
Acceptance:
cfApiEndpoint: <cfApiEndpoint>
cfOrg: <cforgname>
cfSpace: <cfspacename>
steps:
mtaBuild:
mtaBuildTool: cloudMbt
dockerImage: ‘devxci/mbtci-java21-node20:latest’
karmaExecuteTests:
runCommand: ‘npm run test’
dockerImage: ‘node:lts-bookworm’

 

 

 

4. Folder Structure and Set Up

Setting up the project for unit testing involves configuring dependencies, profiles, scripts, and the JEST configuration file.

 

Dev Dependencies:

Install the following dev dependencies

 

 

 

“devDependencies”: {
“chai”: “^4.4.1”,
“chai-as-promised”: “^7.1.2”,
“chai-shallow-deep-equal”: “^1.4.6”,
“chai-subset”: “^1.6.0”,
“jest”: “^29.7.0”,
“jest-junit”: “^16.0.0”,
“jest-sonar-reporter”: “^2.0.0”
}

 

 

 

Profiles:

You can create test profile for the unit testing as below:

 

 

 

“cds”: {
“[test]”: {
“folders”: {
“db”: “db”
},
“db”: {
“kind”: “sql”
}
}
}

 

 

 

 

Scripts:

npm run test will execute the unit tests:

 

“scripts”: {
“start”: “cds-serve”,
“jest”: “jest ./test –config ./test/jest.config.js –runInBand –detectOpenHandles –forceExit –ci –coverage”,
“test”: “cds bind –profile test –exec npm run jest”
}

 

 

 

JEST Configuration (jest.config.js):

Sample configuration file for the JEST configuration:

 

const config = {
testTimeout: 1000000,
testEnvironment: “node”,
// Automatically clear mock calls, instances, contexts and results before every test
clearMocks: true,

// Indicates whether the coverage information should be collected while executing the test
collectCoverage: true,

// An array of glob patterns indicating a set of files for which coverage information should be collected
// collectCoverageFrom: undefined,

// The directory where Jest should output its coverage files
coverageDirectory: “./coverage”,
reporters: [“default”, “jest-junit”],
testResultsProcessor: “jest-sonar-reporter”
};

module.exports = config;

 

5. Basic and Mocking Examples

Data Model (data-model.cds):

Data model having a vehicle entity with an association to carriers to retrieve the carrier(Business Partner) info

 

namespace test.vehicle;

using {
cuid,
managed,
Country
} from ‘@sap/cds/common’;
using {API_BUSINESS_PARTNER as bupa} from ‘../srv/external/API_BUSINESS_PARTNER’;

entity Carriers as projection on bupa.A_BusinessPartner {
key BusinessPartner as ID,
OrganizationBPName1 as fullName: String(80),
BusinessPartnerCategory as BPCategory,
BusinessPartnerIDByExtSystem as BPExternal
}

entity Vehicle : cuid, managed {
country: Country;
vehicleRegistrationNum: String;
carrier: Association to common.Carriers;
subCarrier: String;
loadingBanFlag: Boolean default false @readonly;
permanentBanFlag: Boolean default false;
warningFlag: Boolean default false;
compressedVehicleRegNum: String;
}

 

Test Class (Test.class.js):

Setting up the Test class:

 

“use strict”;

module.exports = class Test {
constructor(GET, POST, PATCH, DELETE, test, expect, axios, cds) {
this.cds = cds;
this.POST = POST;
this.PATCH = PATCH;
this.DELETE = DELETE;
this.test = test;
this.expect = expect;
this.axios = axios;
this.cds = cds;
this.GET = GET;
}
};

 

Test File (test.js):

All tests file having all the tests file to execute their tests. cds.test starts a new server and then the tests files are run.

 

“use strict”;

// require dependencies
const chai = require(‘chai’);
chai.use(require(‘chai-shallow-deep-equal’));

const VehicleService = require(‘../test/vehicle-service/vehicle-service-test’);

// launch cds server
const cds = require(‘@sap/cds/lib’);
const TestClass = require(‘./Test.class’);
if (cds.User.default) cds.User.default = cds.User.Privileged; // hardcode monkey patch
else cds.User = cds.User.Privileged;
const { GET, POST, PATCH, DELETE, test, expect, axios} = cds.test(‘serve’, __dirname + ‘/../srv’,’–in-memory’);

// run tests
const oTest = new TestClass(GET, POST, PATCH, DELETE, test, expect, axios, cds);
VehicleService.test(oTest);

 

Vehicle Service Test (vehicle-service-test.js):

Individual tests are written in this file. Here, three example test cases are written. First test checks the creation of vehicle and the second test gets the vehicle carrier by mocking the module getCarriers and returning with a mock return value. The third test case handles the error case where error is thrown using req.error.

 

“use strict”;
const { getCarriers } = require(“../../srv/code/getCarriers”);

jest.mock(“../../srv/code/getCarriers”);

module.exports = {
/**
*
* {object} oTestClass Test class described in /test/Test.class.js
*
*
*/
test: function (oTestClass) {
describe(“Vehicle Service”, () => {
const { GET, POST, test, expect } = oTestClass;
beforeAll(async () => {});
beforeEach(async () => {
await test.data.reset();
});
it(“Create Vehicle”, async () => {
// Arrange
const inputVehicle = {
country_code: “DE”,
vehicleRegistrationNum: “BAE 1276”,
subCarrier: “Sub-Carrier”,
loadingBanFlag: false,
permanentBanFlag: false,
warningFlag: false,
};

// Act
const { status } = await POST(“/odata/v4/v1/vehicle/Vehicles”, inputVehicle);
// Assert
expect(status).to.equal(201);
});

it(“Check Vehicle Carrier”, async () => {
// Arrange
jest.mocked(getCarriers).mockReturnValue([
{
ID: “80”,
fullName: “Carrier 80”,
BPCategory: “01”,
BPExternal: “80”,
},
]);
// Act
const { status, data } = await GET(“/odata/v4/v1/vehicle/Vehicles?$expand=carrier”);
// Assert
expect(status).to.equal(200);
expect(data?.value.length).to.equal(1);
console.log(data?.value[0]?.carrier);
expect(data?.value[0]?.carrier?.fullName).to.equal(“Carrier 80”);
});

it(“Create Vehicle with Error”, async () => {
const inputVehicle = {
country_code: “DE”,
vehicleRegistrationNum: “HR 1280”,
carrier_ID: “42”,
loadingBanFlag: false,
permanentBanFlag: false,
warningFlag: false,
};

await POST(“/odata/v4/v1/vehicle/Vehicles”, inputVehicle).catch(function (error) {
expect(error.response.status).to.equal(500);
expect(error.message).to.equal(“500 – Vehicle already exists”);
});
});
});
},
};

 

Get Carriers (getCarriers.js):

 

const cds = require(“@sap/cds”);
async function getCarriers(carrierIds) {
const bupa = await cds.connect.to(‘API_BUSINESS_PARTNER’);
return await bupa.run(SELECT.from(‘VehicleService.Carriers’).where({ ID: {in:carrierIds} }));
}

module.exports = {
getCarriers
}

 

Vehicle Field Validation Handler (VehicleFieldValidationHandler.js):

Custom handler file:

 

const { pattern, httpStatusCode } = require(‘./constant’);

async function validateVehicleRegistrationNum(req) {
// Regular expression pattern allowing numbers, alphabets, and spaces
// check if input matches the pattern
if (!pattern.test(req?.data?.vehicleRegistrationNum)) {
req.error({
code: ‘500’,
message: “Vehicle NumberVehicle Registration Number contains Alphabets, Numbers, Spaces and at least 3 characters”,
target: `in/vehicleRegistrationNum`
});
}
if (!req?.data?.vehicleRegistrationNum) {
req.error({
code: ‘500’,
message: “Vehicle Registration Number is required”,
target: `in/vehicleRegistrationNum`
});
}

}
async function validateMandatoryField(req) {

if (!req?.data?.country_code) {
req.error({
code: httpStatusCode.internalServerError,
message: “Country is Mandatory”,
target: `in/country_code`
});
}
}

async function duplicateVehicle(req) {
if (req?.data?.vehicleRegistrationNum) {
req.data.vehicleRegistrationNum = req?.data?.vehicleRegistrationNum?.toUpperCase();
let check = req?.data?.vehicleRegistrationNum?.replaceAll(” “, “”).trim().toUpperCase();

req.data.compressedVehicleRegNum = check;

let checkDuplicate = await SELECT.from(“test.vehicle.Vehicle”).where({ compressedVehicleRegNum: check });
let ID = req?.params[0]?.ID;
let idCheck = checkDuplicate?.length ? checkDuplicate[0].ID : “”;

if (checkDuplicate?.length && idCheck !== ID) {
req.error({
code: httpStatusCode.internalServerError,
message: “Vehicle already exists”,
target: `in/vehicleRegistrationNum`
})
}
};

}
module.exports = { validateVehicleRegistrationNum, validateMandatoryField, duplicateVehicle }

 

6. Coverage Report

Running npm run jest will generate a coverage report in the specified directory. This report provides insights into the percentage of code covered by tests, helping you identify untested parts of your application.

 

Conclusion

Unit testing is an essential practice for maintaining high-quality code in CAP and Node.js projects. By following the principles of TDD, using JEST for unit tests, integrating tests into CI/CD pipelines, and organizing your project structure efficiently, you can ensure your application is robust and reliable. Use the examples and configurations provided in this guide to set up and enhance your unit testing practices.

 

​ Unit testing is a vital component in software development, ensuring that individual parts of the application work correctly. In the context of CAP (Cloud Application Programming) and Node.js projects, unit testing helps maintain high code quality and reliability. This guide will walk you through the essentials of unit testing in CAP, including Test Driven Development (TDD), using JEST for unit tests, integrating unit tests into CI/CD pipelines, setting up your project, and practical examples.1. Test Driven Development (TDD)Test Driven Development (TDD) is a software development process where you write tests before writing the code that needs to be tested. This approach ensures that the code is continuously validated against a predefined set of tests, leading to higher code quality and fewer bugs. Testing Pyramid:Unit Tests: Automated tests that test individual functions or components in isolation.Integration Tests: Tests the interaction between multiple components or systems.System Tests: Tests the entire system as a whole.UI Tests: Tests the user interface, e.g., WDI5 (WebDriver Integration for UI5).Best Practices for Writing Unit Tests:Write Small, Focused Tests: Ensure each test focuses on a single functionality or component.Use Mocking and Stubbing: Avoid external dependencies by simulating complex objects or services.Arrange, Act, Assert Pattern: Structure tests by arranging conditions, acting by calling the method, and asserting the expected outcome.Run Tests Frequently: Integrate unit tests into the CI/CD pipeline to catch issues early.Test Edge Cases: Ensure robustness by covering potential failure points.Keep Tests Independent: Avoid dependencies between tests to prevent cascading failures and simplify debugging.2. Unit Testing in CAP Using JESTJEST is a comprehensive testing framework designed initially for React applications but now widely used across various JavaScript frameworks, including Node.js.Tools for Unit Testing in CAP/Node.js Projects:Option 1: Use Mocha, Chai, and Sinon.js:Mocha: Unit test framework.Chai: Assertion library.Sinon.js: Mocking library (stubs).Option 2: Use JEST:JEST: An all-in-one unit testing framework developed by Facebook. It can be combined with Chai for enhanced capabilities.For detailed documentation, visit the JEST website.3. Unit Testing in CI/CD Pipelines (e.g. Azure)Integrating unit tests into CI/CD pipelines ensures that code changes are continuously validated. Below is a sample config.yml file for setting up unit testing in Azure pipelines:   general:
buildTool: ‘mta’
productiveBranch: ‘main’
pipelineOptimization: true
repository: ‘CAPUnitTest’
owner: ‘UT-TEST’
nativeBuild: true
stages:
Build:
karmaExecuteTests: true
Acceptance:
cfApiEndpoint: <cfApiEndpoint>
cfOrg: <cforgname>
cfSpace: <cfspacename>
steps:
mtaBuild:
mtaBuildTool: cloudMbt
dockerImage: ‘devxci/mbtci-java21-node20:latest’
karmaExecuteTests:
runCommand: ‘npm run test’
dockerImage: ‘node:lts-bookworm’   4. Folder Structure and Set UpSetting up the project for unit testing involves configuring dependencies, profiles, scripts, and the JEST configuration file. Dev Dependencies:Install the following dev dependencies   “devDependencies”: {
“chai”: “^4.4.1”,
“chai-as-promised”: “^7.1.2”,
“chai-shallow-deep-equal”: “^1.4.6”,
“chai-subset”: “^1.6.0”,
“jest”: “^29.7.0”,
“jest-junit”: “^16.0.0”,
“jest-sonar-reporter”: “^2.0.0”
}   Profiles:You can create test profile for the unit testing as below:   “cds”: {
“[test]”: {
“folders”: {
“db”: “db”
},
“db”: {
“kind”: “sql”
}
}
}    Scripts:npm run test will execute the unit tests: “scripts”: {
“start”: “cds-serve”,
“jest”: “jest ./test –config ./test/jest.config.js –runInBand –detectOpenHandles –forceExit –ci –coverage”,
“test”: “cds bind –profile test –exec npm run jest”
}   JEST Configuration (jest.config.js):Sample configuration file for the JEST configuration: const config = {
testTimeout: 1000000,
testEnvironment: “node”,
// Automatically clear mock calls, instances, contexts and results before every test
clearMocks: true,

// Indicates whether the coverage information should be collected while executing the test
collectCoverage: true,

// An array of glob patterns indicating a set of files for which coverage information should be collected
// collectCoverageFrom: undefined,

// The directory where Jest should output its coverage files
coverageDirectory: “./coverage”,
reporters: [“default”, “jest-junit”],
testResultsProcessor: “jest-sonar-reporter”
};

module.exports = config; 5. Basic and Mocking ExamplesData Model (data-model.cds):Data model having a vehicle entity with an association to carriers to retrieve the carrier(Business Partner) info namespace test.vehicle;

using {
cuid,
managed,
Country
} from ‘@sap/cds/common’;
using {API_BUSINESS_PARTNER as bupa} from ‘../srv/external/API_BUSINESS_PARTNER’;

entity Carriers as projection on bupa.A_BusinessPartner {
key BusinessPartner as ID,
OrganizationBPName1 as fullName: String(80),
BusinessPartnerCategory as BPCategory,
BusinessPartnerIDByExtSystem as BPExternal
}

entity Vehicle : cuid, managed {
country: Country;
vehicleRegistrationNum: String;
carrier: Association to common.Carriers;
subCarrier: String;
loadingBanFlag: Boolean default false @readonly;
permanentBanFlag: Boolean default false;
warningFlag: Boolean default false;
compressedVehicleRegNum: String;
} Test Class (Test.class.js):Setting up the Test class: “use strict”;

module.exports = class Test {
constructor(GET, POST, PATCH, DELETE, test, expect, axios, cds) {
this.cds = cds;
this.POST = POST;
this.PATCH = PATCH;
this.DELETE = DELETE;
this.test = test;
this.expect = expect;
this.axios = axios;
this.cds = cds;
this.GET = GET;
}
}; Test File (test.js):All tests file having all the tests file to execute their tests. cds.test starts a new server and then the tests files are run. “use strict”;

// require dependencies
const chai = require(‘chai’);
chai.use(require(‘chai-shallow-deep-equal’));

const VehicleService = require(‘../test/vehicle-service/vehicle-service-test’);

// launch cds server
const cds = require(‘@sap/cds/lib’);
const TestClass = require(‘./Test.class’);
if (cds.User.default) cds.User.default = cds.User.Privileged; // hardcode monkey patch
else cds.User = cds.User.Privileged;
const { GET, POST, PATCH, DELETE, test, expect, axios} = cds.test(‘serve’, __dirname + ‘/../srv’,’–in-memory’);

// run tests
const oTest = new TestClass(GET, POST, PATCH, DELETE, test, expect, axios, cds);
VehicleService.test(oTest); Vehicle Service Test (vehicle-service-test.js):Individual tests are written in this file. Here, three example test cases are written. First test checks the creation of vehicle and the second test gets the vehicle carrier by mocking the module getCarriers and returning with a mock return value. The third test case handles the error case where error is thrown using req.error. “use strict”;
const { getCarriers } = require(“../../srv/code/getCarriers”);

jest.mock(“../../srv/code/getCarriers”);

module.exports = {
/**
*
* {object} oTestClass Test class described in /test/Test.class.js
*
*
*/
test: function (oTestClass) {
describe(“Vehicle Service”, () => {
const { GET, POST, test, expect } = oTestClass;
beforeAll(async () => {});
beforeEach(async () => {
await test.data.reset();
});
it(“Create Vehicle”, async () => {
// Arrange
const inputVehicle = {
country_code: “DE”,
vehicleRegistrationNum: “BAE 1276”,
subCarrier: “Sub-Carrier”,
loadingBanFlag: false,
permanentBanFlag: false,
warningFlag: false,
};

// Act
const { status } = await POST(“/odata/v4/v1/vehicle/Vehicles”, inputVehicle);
// Assert
expect(status).to.equal(201);
});

it(“Check Vehicle Carrier”, async () => {
// Arrange
jest.mocked(getCarriers).mockReturnValue([
{
ID: “80”,
fullName: “Carrier 80”,
BPCategory: “01”,
BPExternal: “80”,
},
]);
// Act
const { status, data } = await GET(“/odata/v4/v1/vehicle/Vehicles?$expand=carrier”);
// Assert
expect(status).to.equal(200);
expect(data?.value.length).to.equal(1);
console.log(data?.value[0]?.carrier);
expect(data?.value[0]?.carrier?.fullName).to.equal(“Carrier 80”);
});

it(“Create Vehicle with Error”, async () => {
const inputVehicle = {
country_code: “DE”,
vehicleRegistrationNum: “HR 1280”,
carrier_ID: “42”,
loadingBanFlag: false,
permanentBanFlag: false,
warningFlag: false,
};

await POST(“/odata/v4/v1/vehicle/Vehicles”, inputVehicle).catch(function (error) {
expect(error.response.status).to.equal(500);
expect(error.message).to.equal(“500 – Vehicle already exists”);
});
});
});
},
}; Get Carriers (getCarriers.js): const cds = require(“@sap/cds”);
async function getCarriers(carrierIds) {
const bupa = await cds.connect.to(‘API_BUSINESS_PARTNER’);
return await bupa.run(SELECT.from(‘VehicleService.Carriers’).where({ ID: {in:carrierIds} }));
}

module.exports = {
getCarriers
} Vehicle Field Validation Handler (VehicleFieldValidationHandler.js):Custom handler file: const { pattern, httpStatusCode } = require(‘./constant’);

async function validateVehicleRegistrationNum(req) {
// Regular expression pattern allowing numbers, alphabets, and spaces
// check if input matches the pattern
if (!pattern.test(req?.data?.vehicleRegistrationNum)) {
req.error({
code: ‘500’,
message: “Vehicle NumberVehicle Registration Number contains Alphabets, Numbers, Spaces and at least 3 characters”,
target: `in/vehicleRegistrationNum`
});
}
if (!req?.data?.vehicleRegistrationNum) {
req.error({
code: ‘500’,
message: “Vehicle Registration Number is required”,
target: `in/vehicleRegistrationNum`
});
}

}
async function validateMandatoryField(req) {

if (!req?.data?.country_code) {
req.error({
code: httpStatusCode.internalServerError,
message: “Country is Mandatory”,
target: `in/country_code`
});
}
}

async function duplicateVehicle(req) {
if (req?.data?.vehicleRegistrationNum) {
req.data.vehicleRegistrationNum = req?.data?.vehicleRegistrationNum?.toUpperCase();
let check = req?.data?.vehicleRegistrationNum?.replaceAll(” “, “”).trim().toUpperCase();

req.data.compressedVehicleRegNum = check;

let checkDuplicate = await SELECT.from(“test.vehicle.Vehicle”).where({ compressedVehicleRegNum: check });
let ID = req?.params[0]?.ID;
let idCheck = checkDuplicate?.length ? checkDuplicate[0].ID : “”;

if (checkDuplicate?.length && idCheck !== ID) {
req.error({
code: httpStatusCode.internalServerError,
message: “Vehicle already exists”,
target: `in/vehicleRegistrationNum`
})
}
};

}
module.exports = { validateVehicleRegistrationNum, validateMandatoryField, duplicateVehicle } 6. Coverage ReportRunning npm run jest will generate a coverage report in the specified directory. This report provides insights into the percentage of code covered by tests, helping you identify untested parts of your application. ConclusionUnit testing is an essential practice for maintaining high-quality code in CAP and Node.js projects. By following the principles of TDD, using JEST for unit tests, integrating tests into CI/CD pipelines, and organizing your project structure efficiently, you can ensure your application is robust and reliable. Use the examples and configurations provided in this guide to set up and enhance your unit testing practices.   Read More Technology Blogs by SAP articles 

#SAP

#SAPTechnologyblog

You May Also Like

More From Author

+ There are no comments

Add yours