Overview

LoopBack 4 extensions are often used by other teams. A thorough test suite foryour extension brings powerful benefits to all your users, including:

  • Validating the behavior of the extension
  • Preventing unwanted changes to the API or functionality of the extension
  • Providing working samples and code snippets that serve as functionaldocumentation for your users

Project Setup

We recommend that you use @loopback/cli to create the extension, as itinstalls several tools you can use for testing, such as mocha, assertionlibraries, linters, etc.

The @loopback/cli includes the mocha automated test runner and a testfolder containing recommended folders for various types of tests. Mocha isenabled by default if @loopback/cli is used to create the extension project.The @loopback/cli installs and configures mocha, creates the test folder,and also enters a test command in your package.json.

Assertion libraries such as ShouldJS (asexpect), SinonJS, and a test sandbox are made availablethrough the convenient @loopback/testlab package. The testlab is alsoinstalled by @loopback/cli.

Manual Setup - Using Mocha

  • Install mocha by running npm i —save-dev mocha. This will save themocha package in package.json as well.
  • Under scripts in package.json add the following:test: npm run build && mocha —recursive ./dist/test

Types of tests

A comprehensive test suite tests many aspects of your code. We recommend thatyou write unit, integration, and acceptance tests to test your application froma variety of perspectives. Comprehensive testing ensures correctness,integration, and future compatibility.

You may use any development methodology you want to write your extension; theimportant thing is to test it with an automated test suite. In Traditionaldevelopment methodology, you write the code first and then write the tests. InTest-driven development methodology, you write the tests first, see them fail,then write the code to pass the tests.

Unit Tests

A unit test tests the smallest unit of code possible, which in this case is afunction. Unit tests ensure variable and state changes by outside actors don’taffect the results. Test doublesshould be used to substitute function dependencies. You can learn more abouttest doubles and Unit testing here:Testing your Application: Unit testing.

Controllers

At its core, a controller is a simple class that is responsible for relatedactions on an object. Performing unit tests on a controller in an extension isthe same as performing unit tests on a controller in an application.

To test a controller, you instantiate a new instance of your controller classand test a function, providing a test double for constructor arguments asneeded. Following are examples that illustrate how to perform a unit test on acontroller class:

src/controllers/ping.controller.ts

  1. export class PingController {
  2. @get('/ping')
  3. ping(msg?: string) {
  4. return `You pinged with ${msg}`;
  5. }
  6. }

src/tests/unit/controllers/ping.controller.unit.ts

  1. import {PingController} from '../../..';
  2. import {expect} from '@loopback/testlab';
  3. describe('PingController() unit', () => {
  4. it('pings with no input', () => {
  5. const controller = new PingController();
  6. const result = controller.ping();
  7. expect(result).to.equal('You pinged with undefined');
  8. });
  9. it("pings with msg 'hello'", () => {
  10. const controller = new PingController();
  11. const result = controller.ping('hello');
  12. expect(result).to.equal('You pinged with hello');
  13. });
  14. });

You can find an advanced example on testing controllers inUnit test your Controllers.

Decorators

The recommended usage of a decorator is to store metadata about a class or aclass method. The decorator implementation usually provides a function toretrieve the related metadata based on the class name and method name. For aunit test for a decorator, it is important to test that that it stores andretrieves the correct metadata. The retrieval gets tested as a result ofvalidating whether the metadata was stored or not.

Following is an example for testing a decorator:

src/decorators/test.decorator.ts

  1. export function test(file: string) {
  2. return function(target: Object, methodName: string): void {
  3. Reflector.defineMetadata(
  4. 'example.msg.decorator.metadata.key',
  5. {file},
  6. target,
  7. methodName,
  8. );
  9. };
  10. }
  11. export function getTestMetadata(
  12. controllerClass: Constructor<{}>,
  13. methodName: string,
  14. ): {file: string} {
  15. return Reflector.getMetadata(
  16. 'example.msg.decorator.metadata.key',
  17. controllerClass.prototype,
  18. methodName,
  19. );
  20. }

src/tests/unit/decorators/test.decorator.unit.ts

  1. import {test, getTestMetadata} from '../../..';
  2. import {expect} from '@loopback/testlab';
  3. describe('test.decorator (unit)', () => {
  4. it('can store test name via a decorator', () => {
  5. class TestClass {
  6. @test('me.test.ts')
  7. me() {}
  8. }
  9. const metadata = getTestMetadata(TestClass, 'me');
  10. expect(metadata).to.be.a.Object();
  11. expect(metadata.file).to.be.eql('me.test.ts');
  12. });
  13. });

Mixins

A Mixin is a TypeScript function that extends the Application Class, addingnew constructor properties, methods, etc. It is difficult to write a unit testfor a Mixin without the Application Class dependency. The recommended practiceis to write an integration test is described inMixin Integration Tests.

Providers

A Provider is a Class that implements the Provider interface. This interfacerequires the Class to have a value() function. A unit test for a providershould test the value() function by instantiating a new Provider class,using a test double for any constructor arguments.

src/providers/random-number.provider.ts

  1. import {Provider} from '@loopback/context';
  2. export class RandomNumberProvider implements Provider<number> {
  3. value() {
  4. return (max: number): number => {
  5. return Math.floor(Math.random() * max) + 1;
  6. };
  7. }
  8. }

src/tests/unit/providers/random-number.provider.unit.ts

  1. import {RandomNumberProvider} from '../../..';
  2. import {expect} from '@loopback/testlab';
  3. describe('RandomNumberProvider (unit)', () => {
  4. it('generates a random number within range', () => {
  5. const provider = new RandomNumberProvider().value();
  6. const random: number = provider(3);
  7. expect(random).to.be.a.Number();
  8. expect(random).to.equalOneOf([1, 2, 3]);
  9. });
  10. });

Repositories

This section will be provided in a future version.

Integration Tests

An integration test plays an important part in your test suite by ensuring yourextension artifacts work together as well as @loopback. It is recommended totest two items together and substitute other integrations as test doubles so itbecomes apparent where the integration errors may occur.

Mixin Integration Tests

A Mixin extends a base Class by returning an anonymous class. Thus, a Mixin istested by actually using the Mixin with its base Class. Since this requires twoClasses to work together, an integration test is needed. A Mixin test checksthat new or overridden methods exist and work as expected in the new Mixedclass. Following is an example for an integration test for a Mixin:

src/mixins/time.mixin.ts

  1. import {Constructor} from '@loopback/context';
  2. export function TimeMixin<T extends Constructor<any>>(superClass: T) {
  3. return class extends superClass {
  4. constructor(...args: any[]) {
  5. super(...args);
  6. if (!this.options) this.options = {};
  7. if (typeof this.options.timeAsString !== 'boolean') {
  8. this.options.timeAsString = false;
  9. }
  10. }
  11. time() {
  12. if (this.options.timeAsString) {
  13. return new Date().toString();
  14. }
  15. return new Date();
  16. }
  17. };
  18. }

src/tests/integration/mixins/time.mixin.integration.ts

  1. import {expect} from '@loopback/testlab';
  2. import {Application} from '@loopback/core';
  3. import {TimeMixin} from '../../..';
  4. describe('TimeMixin (integration)', () => {
  5. it('mixed class has .time()', () => {
  6. const myApp = new AppWithTime();
  7. expect(typeof myApp.time).to.be.eql('function');
  8. });
  9. it('returns time as string', () => {
  10. const myApp = new AppWithLogLevel({
  11. timeAsString: true,
  12. });
  13. const time = myApp.time();
  14. expect(time).to.be.a.String();
  15. });
  16. it('returns time as Date', () => {
  17. const myApp = new AppWithLogLevel();
  18. const time = myApp.time();
  19. expect(time).to.be.a.Date();
  20. });
  21. class AppWithTime extends TimeMixin(Application) {}
  22. });

Acceptance Test

An Acceptance test for an extension is a comprehensive test written end-to-end.Acceptance tests cover the user scenarios. An acceptance test uses all of theextension artifacts such as decorators, mixins, providers, repositories, etc. Notest doubles are needed for an Acceptance test. This is a black box test whereyou don’t know or care about the internals of the extensions. You will be usingthe extension as if you were the consumer.

Due to the complexity of an Acceptance test, there is no example given here.Have a look atloopback4-example-log-extensionto understand the extension artifacts and their usage. An Acceptance test can beseen here:src/tests/acceptance/log.extension.acceptance.ts.