
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.
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.
JEST is a comprehensive testing framework designed initially for React applications but now widely used across various JavaScript frameworks, including Node.js.
For detailed documentation, visit the JEST website.
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'
Setting up the project for unit testing involves configuring dependencies, profiles, scripts, and the JEST configuration file.
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"
}
You can create test profile for the unit testing as below:
"cds": {
"[test]": {
"folders": {
"db": "db"
},
"db": {
"kind": "sql"
}
}
}
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"
}
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;
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;
}
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;
}
};
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);
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");
});
});
});
},
};
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
}
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 }
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.
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.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
User | Count |
---|---|
14 | |
11 | |
7 | |
6 | |
6 | |
6 | |
5 | |
5 | |
5 | |
5 |