Define Unit Tests (TypeScript)
You want to write unit tests without having access to all of the infrastructure which can be achieved by mocking which is described here in this How-To.
Descriptionâ
Unit testing ensures code functionality and reliability, but testing parts of code that interact with external systems / infrastructure can be challenging.
Mocking creates artificial versions of external systems, allowing for testing without relying on the real systems.
In this How-To, you will see examples on how you can mock generated solution-framework functionalities that are used in implementation files (API / Command / Services / Agents), isolating code under test for comprehensive, reliable unit tests that cover even complex dependencies.
Please note that the links to the workbench tools in this tutorial only apply to the IBM Education Environment we provide. If you are using a different environment, e.g. your own installation, you will need to navigate directly to the required tools.
Dependenciesâ
By default, TypeScript service projects use Mocha and Chai for testing. Mocha is a widely used JavaScript test framework that is also popular for testing TypeScript projects due to its ease of use and flexibility. Chai, on the other hand, is an assertion library that is often used in conjunction with Mocha to provide a range of functions and styles for making assertions in JavaScript and TypeScript testing.
While Mocha is a powerful testing framework, it does not have built-in mocking capabilities. However, it can be used with third-party libraries that provide mocking functionality, such as ts-sinon or ts-mockito. These libraries can be used to create mock objects and stubs, allowing developers to isolate code under test and simulate dependencies for comprehensive and reliable testing. With these libraries, developers can easily create test cases for complex and error-prone code, helping them to identify and address issues early in the development process.
In this How-To guide, you'll use ts-sinon as mocking framework. ts-sinon is built on top of the popular Sinon.js library and provides a range of features for creating mock objects and stubs. It supports mocking of class constructors and instance methods, verification of method calls, and asynchronous testing. By leveraging ts-sinon, you can easily create and test complex services with dependencies.
To add ts-sinon as a dev dependency, you can use the following command:
npm install --save-dev ts-sinon
This command will install the latest version of ts-sinon as a dev dependency and update your package.json file accordingly.
Testing Filesâ
-
Currently generated
Commands,ServicesandAgentstest files are integration tests, they are not meant to be mockable as internally the runners used in testing load the implementation files and create instances of them, making mocking variables difficult, also command runners for example connect to repository to load the instance after command execution. -
To properly perform unit testing we will create new test files with prefix .unit, for example
CreateCustomerRelatedNotification.unit.test.ts.
This is only needed for testing Commands, Services and Agents, however for Api operations, it is already generated so that it gives direct access to operation instance hence giving access to variables that need to be mocked.
Testing Implementationâ
Unit test for implementation files can be divided inot several parts:
- Import Dependencies
- Declare Test Variables
- Extend Implementation class
- Setup Test Data and Mocks
- Clean Up
- Execution and Verification
Have a look at the example below for testing a command, later we will explain each part individually.
The below example is for a command, however same test file structure and recipe can be applied to service, agents and api operations.
// Importing needed classes and interfaces from solution framework
import { entities, Context, loadAndPrepareDebugConfig, TestEnvironment } from 'solution-framework';
import { EntityRepository } from 'solution-framework/dist/sdk/v1/solution/repository/EntityRepository';
// Importing needed chai and ts-sion
import { expect } from 'chai';
import sinon , {StubbedInstance, stubObject} from 'ts-sinon';
// Importing the command implementation class
import CreateCustomerRelatedNotificationBase from './CreateCustomerRelatedNotification';
describe('noti:CreateCustomerRelatedNotification', () => {
// As command classes are protected, we need to extend it and use it in our tests
class CreateCustomerRelatedNotification extends CreateCustomerRelatedNotificationBase {
constructor(instance: entities.noti_CustomerRelatedNotification, requestContext: Context, inputEntityData?: any) {
super(instance, requestContext, inputEntityData);
}
}
// Declare command instance
let createCustomerRelatedNotificationInstance: CreateCustomerRelatedNotification;
// Declare command input entity
let inputInstance: entities.noti_CreateCustomerRelatedNotification_Input;
// Declare entity Instance
let entityInstance: entities.noti_CustomerRelatedNotification;
// Declare repository stub for mocking Entity Repository
let repoStubHelper: StubbedInstance<EntityRepository>;
before(async () => {
// Create instance of test environment that provides access to factory to create new entity instances
const testEnvironment = new TestEnvironment();
// Load needed request context
const requestContext = loadAndPrepareDebugConfig().requestContext;
// Create input entity
inputInstance = testEnvironment.factory.entity.noti.CreateCustomerRelatedNotification_Input();
inputInstance.subject = 'New Notification about assignment';
inputInstance.userLogin = 'John';
inputInstance.customerName = 'John Doe';
inputInstance.icon = 'INFO';
// Create a new entity instance that command will work with
entityInstance = testEnvironment.factory.entity.noti.CustomerRelatedNotification();
// Create a command instance that we will test
// Notice that it needs the domain Json data and not the inout object) => inputInstance['_getDomainJSON']()
createCustomerRelatedNotificationInstance = new CreateCustomerRelatedNotification(entityInstance, requestContext, inputInstance['_getDomainJSON']());
// Stub the repository instance within the createCustomerRelatedNotificationInstance command instance
repoStubHelper = stubObject<EntityRepository>(createCustomerRelatedNotificationInstance.repo);
});
after(async () => {
// This block will run automatically after all tests.
// Alternatively, use afterEach() to define what should automatically happen after each test.
// This is an optional block.
// Recommended: remove all instances that were created
await testEnvironment.cleanup();
// Restore all created fake functions via sinon.fake
sinon.restore();
});
it('Repository was called and root Entity Instance Was Changed', async () => {
// Create a mocked function to mock repository find by Id call using fake function from sinon
// Here we will make it return the declared entityInstance
const mockedFindById = sinon.fake(()=> {
return entityInstance;
});
// Replace the stub function with the mocked function
repoStubHelper.noti.CustomerRelatedNotification.findById = mockedFindById;
// Call command execute method
await createCustomerRelatedNotificationInstance.execute();
// expect that repo find by id was called once
sinon.assert.calledOnce(mockedFindById);
// expect that repo find by id was called using the input entity id
sinon.assert.calledWith(mockedFindById, inputInstance._id);
// expect that the entity set by the command is the same as the mocked one
expect(createCustomerRelatedNotificationInstance.instance._id).to.equal(entityInstance._id);
// expect that command logic changed message
expect(createCustomerRelatedNotificationInstance.instance.message).to.equal('Message was changed by command');
});
});
Below sub-sections describe the various part from the above example.
Import Dependenciesâ
-
Import needed dependencies such as the
entities,TestEnvironment,Contextandcommandimplementation class. -
Also import needed sinon dependencies and expect function.
// Importing needed classes and interfaces from solution framework
import { entities, Context, loadAndPrepareDebugConfig, TestEnvironment } from 'solution-framework';
import { EntityRepository } from 'solution-framework/dist/sdk/v1/solution/repository/EntityRepository';
// Importing needed chai and ts-sion
import { expect } from 'chai';
import sinon , {StubbedInstance, stubObject} from 'ts-sinon';
// Importing the command implementation class
import CreateCustomerRelatedNotificationBase from './CreateCustomerRelatedNotification';
Extend Implementation Classâ
Since Command / Service / Agent classes are protected, we need to extend it and use it in our tests.
You need to extend the class you want to test (Command / Service / Agent)
- Import it from its src-impl folder as Base (example: CreateCustomerRelatedNotificationBase).
- Create the constructor with the same signature as the Base class of your (Command / Service / Agent).
class CreateCustomerRelatedNotification extends CreateCustomerRelatedNotificationBase {
constructor(instance: entities.noti_CustomerRelatedNotification, requestContext: Context, inputEntityData?: any) {
super(instance, requestContext, inputEntityData);
}
}
Declare Test Variablesâ
In unit test, there are variables that need to be declared and used such as:
- Command / Service / Agent we want to test.
- Input entity, root entity ..etc.
- Mocking Stubs (from ts-sinon to do mocking for repository, integration api dependencies, event producer, other service..etc.).
// Declare command instance
let createCustomerRelatedNotificationInstance: CreateCustomerRelatedNotification;
// Declare command input entity
let inputInstance: entities.noti_CreateCustomerRelatedNotification_Input;
// Declare entity Instance
let entityInstance: entities.noti_CustomerRelatedNotification;
// Declare repository stub for mocking Entity Repository
let repoStubHelper: StubbedInstance<EntityRepository>;
Setup Testâ
Before every individual unit test the before code block will be executed. In that block, we need to do our unit test setup such as:
- Instantiate variables.
- Set values needed.
- Do mocking for dependencies we want to mock such as (repository, integration api dependencies, event producer, other service..etc.).
before(async () => {
// Create instance of test environment that provides access to factory to create new entity instances
const testEnvironment = new TestEnvironment();
// Load needed request context
const requestContext = loadAndPrepareDebugConfig().requestContext;
// Create input entity
inputInstance = testEnvironment.factory.entity.noti.CreateCustomerRelatedNotification_Input();
inputInstance.subject = 'New Notification about assignment';
inputInstance.userLogin = 'John';
inputInstance.customerName = 'John Doe';
inputInstance.icon = 'INFO';
// Create a new entity instance that command will work with
entityInstance = testEnvironment.factory.entity.noti.CustomerRelatedNotification();
// Create a command instance that we will test
// Notice that it needs the domain Json data and not the inout object) => inputInstance['_getDomainJSON']()
createCustomerRelatedNotificationInstance = new CreateCustomerRelatedNotification(entityInstance, requestContext, inputInstance['_getDomainJSON']());
// Stub the repository instance within the createCustomerRelatedNotificationInstance command instance
repoStubHelper = stubObject<EntityRepository>(createCustomerRelatedNotificationInstance.repo);
});
Clean Upâ
After every individual unit test the after code block will be executed. In that block, we should restore any mocks / clean up any created entities.
after(async () => {
// This block will run automatically after all tests.
// Alternatively, use afterEach() to define what should automatically happen after each test.
// This is an optional block.
// Restore all created fake functions via sinon.fake
sinon.restore();
});
Execution and Verificationâ
Finally, the Unit Test block that usually contain
- Specific Setup for that unit test function
- Call to the function to be tested
- Verification using expects and asserts
it('Repository was called and root entity Instance Was Changed', async () => {
// Create a mocked function to mock repository find by Id call using fake function from sinon
// Here we will make it return the declared entityInstance
const mockedFindById = sinon.fake(()=> {
return entityInstance;
});
// Replace the stub function with the mocked function
repoStubHelper.noti.CustomerRelatedNotification.findById = mockedFindById;
// Call command execute method
await createCustomerRelatedNotificationInstance.execute();
// expect that repo find by id was called once
sinon.assert.calledOnce(mockedFindById);
// expect that repo find by id was called using the input entity instance id
sinon.assert.calledWith(mockedFindById, inputInstance._id);
// expect that the entity set by the command is the same as the mocked one
expect(createCustomerRelatedNotificationInstance.instance._id).to.equal(entityInstance._id);
// expect that command logic changed message
expect(createCustomerRelatedNotificationInstance.instance.message).to.equal('Message was changed by command');
});
Mocking Examplesâ
Mocking Repositoryâ
Repository can be called via repo variable which is available under this notation in implementation files, below is an example on how to mock that within a service.
// Importing needed classes and interfaces from solution framework
import { Context, loadAndPrepareDebugConfig, TestEnvironment} from 'solution-framework';
// Importing needed chai and ts-sion
import { expect } from 'chai';
import sinon , {StubbedInstance, stubObject} from 'ts-sinon';
// Importing the service implementation class
import SayHelloServiceBase from './SayHelloService';
// Importing EntityRepository class that we want to mock
import { EntityRepository } from 'solution-framework/dist/sdk/v1/solution/repository/EntityRepository';
describe('myHellos:SayHelloService', () => {
// As command classes are protected, we need to extend it and use it in our tests
class SayHelloService extends SayHelloServiceBase {
constructor(requestContext: Context, inputEntityData?: any) {
super(requestContext, inputEntityData);
}
}
// Declare command instance
let sayHelloServiceInstance: SayHelloService;
// Declare repoStubHelper for stubbing EntityRepository object
let repoStubHelper: StubbedInstance<EntityRepository>;
// Create instance of test environment that provides access to factory to create new entity instances
const testEnvironment = new TestEnvironment();
before(async () => {
// Load needed request context
const requestContext = loadAndPrepareDebugConfig().requestContext;
// Create input entity (input to the service)
const inputInstance = testEnvironment.factory.entity.myHellos.SayHelloService_Input();
// Create a service instance that we will test
// Notice that it needs the domain Json data and not the input object) => inputInstance['_getDomainJSON']()
sayHelloServiceInstance = new SayHelloService(requestContext, inputInstance['_getDomainJSON']());
// Stub the EntityRepository within the service instance using stubObject
repoStubHelper = stubObject<EntityRepository>(instance.repo);
});
after(async () => {
// Restore all created fake functions via sinon.fake
sinon.restore();
});
it('SendNotification service was called successfully', async () => {
// Create fake function for the findById repository function we want to mock
const mockedFindById = sinon.fake(()=> {
return entityInstance;
});
// Replace faker function with the stub function
repoStubHelper.noti.CustomerRelatedNotification.findById = mockedFindById;
// Call execute method
await sayHelloServiceInstance.execute();
// expect that repo find by id was called once
sinon.assert.calledOnce(mockedFindById);
});
Mocking Service Triggerâ
Services can be called via ServiceTrigger which is available under this notation in implementation files, below is an example on how to mock that.
// Importing needed classes and interfaces from solution framework
import { Context, loadAndPrepareDebugConfig, TestEnvironment} from 'solution-framework';
// Importing needed chai and ts-sion
import { expect } from 'chai';
import sinon , {StubbedInstance, stubObject} from 'ts-sinon';
// Importing the service implementation class
import SayHelloServiceBase from './SayHelloService';
// Importing service trigger class that we want to mock
import { ServiceTrigger } from 'solution-framework/dist/sdk/v1/solution/service/ServiceTrigger';
describe('myHellos:SayHelloService', () => {
// As command classes are protected, we need to extend it and use it in our tests
class SayHelloService extends SayHelloServiceBase {
constructor(requestContext: Context, inputEntityData?: any) {
super(requestContext, inputEntityData);
}
}
// Declare command instance
let sayHelloServiceInstance: SayHelloService;
// Declare serviceTriggerStubHelper stub for mocking ServiceTrigger
let serviceTriggerStubHelper: StubbedInstance<ServiceTrigger>;
// Create instance of test environment that provides access to factory to create new entity instances
const testEnvironment = new TestEnvironment();
before(async () => {
// Load needed request context
const requestContext = loadAndPrepareDebugConfig().requestContext;
// Create input entity (input to the service)
const inputInstance = testEnvironment.factory.entity.myHellos.SayHelloService_Input();
// Create a service instance that we will test
// Notice that it needs the domain Json data and not the input object) => inputInstance['_getDomainJSON']()
sayHelloServiceInstance = new SayHelloService(requestContext, inputInstance['_getDomainJSON']());
// Mock the service trigger
serviceTriggerStubHelper = stubObject<ServiceTrigger>(testSvcInstance.services);
});
after(async () => {
// Restore all created fake functions via sinon.fake
sinon.restore();
});
it('SendNotification service was called successfully', async () => {
// Create a mocked service function to replace the one in the serviceTriggerStubHelper
const mockedService = sinon.fake();
serviceTriggerStubHelper.myHellos.SendNotification = mockedService;
// Call execute method
await sayHelloServiceInstance.execute();
// expect that SendNotification service was called once
sinon.assert.calledOnce(mockedService);
});
Mocking Api Dependencyâ
Integration Api dependency can be called via Apis class which is available under this notation in implementation files, below is an example on how to mock that.
// Importing needed classes and interfaces from solution framework
import { Context, loadAndPrepareDebugConfig, TestEnvironment} from 'solution-framework';
// Importing needed chai and ts-sion
import { expect } from 'chai';
import sinon , {StubbedInstance, stubObject} from 'ts-sinon';
// Importing the service implementation class
import SayHelloServiceBase from './SayHelloService';
// Importing service trigger class that we want to mock
import { ServiceTrigger } from 'solution-framework/dist/sdk/v1/solution/service/ServiceTrigger';
import { Apis } from 'solution-framework/dist/sdk/v1/namespace/integration/apis';
describe('myHellos:SayHelloService', () => {
// As command classes are protected, we need to extend it and use it in our tests
class SayHelloService extends SayHelloServiceBase {
constructor(requestContext: Context, inputEntityData?: any) {
super(requestContext, inputEntityData);
}
}
// Declare command instance
let sayHelloServiceInstance: SayHelloService;
// Declare apiStubHelper stub for mocking integration api calls
let apiStubHelper: StubbedInstance<Apis>;
// Create instance of test environment that provides access to factory to create new entity instances
const testEnvironment = new TestEnvironment();
before(async () => {
// Load needed request context
const requestContext = loadAndPrepareDebugConfig().requestContext;
// Create input entity (input to the service)
const inputInstance = testEnvironment.factory.entity.myHellos.SayHelloService_Input();
// Create a service instance that we will test
// Notice that it needs the domain Json data and not the inout object) => inputInstance['_getDomainJSON']()
sayHelloServiceInstance = new SayHelloService(requestContext, inputInstance['_getDomainJSON']());
// Mock the service trigger
apiStubHelper = stubObject<Apis>(testSvcInstance.apis);
});
after(async () => {
// Restore all created fake functions via sinon.fake
sinon.restore();
});
it('SendNotification service was called successfully', async () => {
// Mock api send email function using fake function from sinon
const mockedApiCall = sinon.fake();
apiStubHelper.notification.postEmail = mockedApiCall;
// Call execute method
await sayHelloServiceInstance.execute();
// expect api call post email was called once
sinon.assert.calledOnce(mockedApiCall);
});
Mocking Event Factory and Publishâ
Currently, event factory is coupled with implementation base (Service / Agent / Command), for example:
If a service named TestSvc in domain namespace named myHellos publishes two events, it will have a factory that constructs these two events.
Service base class would have below inner structure
export declare abstract class myHellos_TestSvc extends DomainService {
//.... service base class variables
factory: {
entity: SolutionEntityFactory;
event: myHellos_TestSvcBusinessEventFactory;
error: myHellos_TestSvcBusinessErrorFactory;
reference: SolutionReferenceFactory;
external: SolutionExternalEntityFactory;
filter: FilterFactory;
};
protected constructor(requestContext: RequestContext, inputEntityData?: any);
// .... service base class methods
/**
* All assigned business events can be created here
*/
declare class myHellos_TestSvcBusinessEventFactory {
private _requestContext;
myHellos: {
HelloEvent: () => myHellos_HelloEvent$event;
AnotherEvent: () => myHellos_AnotherEvent$event;
};
constructor(requestContext: RequestContext);
}
In order to mock the event factory method call and also the event publish call, we can do something like below code snippet.
// Importing service implementation class
import TestSvcBase from './TestSvc';
describe('myHellos:TestSVC', () => {
// As command classes are protected, we need to extend it and use it in our tests
class TestSvc extends TestSvcBase {
constructor(requestContext: Context, inputEntityData?: any) {
super(requestContext, inputEntityData);
}
}
// Declare service instance
let testSvcInstance: TestSvc;
// Here we will stub the service event factory, we will use "any" type as the type is not exposed
let eventFactory: StubbedInstance<any>;
before(async () => {
// Load needed request context
const requestContext = loadAndPrepareDebugConfig().requestContext;
// Create a service instance that we will test
testSvcInstance = new TestSvc(requestContext);
// Stub the event factory
eventFactory = stubObject<any>(testSvcInstance.factory.event);
});
after(async () => {
// restore all created fakes
sinon.restore();
});
it('Hello Event was created and event publish was called', async () => {
const mockedPublishFn = sinon.fake();
const mockedEvent = sinon.fake(()=> {
return {
publish : mockedPublishFn // Here we return a mocked event object that has a mocked publish function
};
});
// Here we replace the event factroy creation with the mocked event
eventFactory.myHellos.HelloEvent = mockedEvent
// Call service execute method
await testSvcInstance.execute();
// expect that event factory was called to create an event instance
sinon.assert.calledOnce(mockedEvent);
// expect that the publish function was called from the event object
sinon.assert.calledOnce(mockedPublishFn);
});
});
You have successfully included a mocking library and mocked some of generated solution framework functionalities.