@loopback/docs
Version:
Documentation files rendered at [https://loopback.io](https://loopback.io)
306 lines (239 loc) • 10.4 kB
Markdown
---
lang: en
title: 'Testing your extension'
keywords: LoopBack 4.0, LoopBack 4, Node.js, TypeScript, OpenAPI
sidebar: lb4_sidebar
permalink: /doc/en/lb4/Testing-your-extension.html
---
## Overview
LoopBack 4 extensions are often used by other teams. A thorough test suite for
your 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 functional
documentation for your users
## Project Setup
We recommend that you use `/cli` to create the extension, as it
installs several tools you can use for testing, such as `mocha`, assertion
libraries, linters, etc.
The `/cli` includes the `mocha` automated test runner and a `test`
folder containing recommended folders for various types of tests. `Mocha` is
enabled by default if `/cli` is used to create the extension project.
The `/cli` installs and configures `mocha`, creates the `test` folder,
and also enters a `test` command in your `package.json`.
Assertion libraries such as [ShouldJS](http://shouldjs.github.io/) (as
`expect`), [SinonJS](http://sinonjs.org/), and a test sandbox are made available
through the convenient `/testlab` package. The `testlab` is also
installed by `/cli`.
### Manual Setup - Using Mocha
- Install `mocha` by running `npm i --save-dev mocha`. This will save the
`mocha` 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 that
you write unit, integration, and acceptance tests to test your application from
a variety of perspectives. Comprehensive testing ensures correctness,
integration, and future compatibility.
You may use any development methodology you want to write your extension; the
important thing is to test it with an automated test suite. In Traditional
development methodology, you write the code first and then write the tests. In
Test-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 a
function. Unit tests ensure variable and state changes by outside actors don't
affect the results. [Test doubles](https://en.wikipedia.org/wiki/Test_double)
should be used to substitute function dependencies. You can learn more about
test doubles and Unit testing here:
[Testing your Application: Unit testing](Testing-your-application.md#unit-testing).
#### Controllers
At its core, a controller is a simple class that is responsible for related
actions on an object. Performing unit tests on a controller in an extension is
the same as performing unit tests on a controller in an application.
To test a controller, you instantiate a new instance of your controller class
and test a function, providing a test double for constructor arguments as
needed. Following are examples that illustrate how to perform a unit test on a
controller class:
{% include code-caption.html content="src/controllers/ping.controller.ts" %}
```ts
export class PingController {
('/ping')
ping(msg?: string) {
return `You pinged with ${msg}`;
}
}
```
{% include code-caption.html content="src/__tests__/unit/controllers/ping.controller.unit.ts" %}
```ts
import {PingController} from '../../..';
import {expect} from '/testlab';
describe('PingController() unit', () => {
it('pings with no input', () => {
const controller = new PingController();
const result = controller.ping();
expect(result).to.equal('You pinged with undefined');
});
it("pings with msg 'hello'", () => {
const controller = new PingController();
const result = controller.ping('hello');
expect(result).to.equal('You pinged with hello');
});
});
```
You can find an advanced example on testing controllers in
[Unit test your Controllers](Testing-your-application.md#unit-test-your-controllers).
#### Decorators
The recommended usage of a decorator is to store metadata about a class or a
class method. The decorator implementation usually provides a function to
retrieve the related metadata based on the class name and method name. For a
unit test for a decorator, it is important to test that that it stores and
retrieves the correct metadata. _The retrieval gets tested as a result of
validating whether the metadata was stored or not._
Following is an example for testing a decorator:
{% include code-caption.html content="src/decorators/test.decorator.ts" %}
```ts
export function test(file: string) {
return function (target: Object, methodName: string): void {
Reflector.defineMetadata(
'example.msg.decorator.metadata.key',
{file},
target,
methodName,
);
};
}
export function getTestMetadata(
controllerClass: Constructor<{}>,
methodName: string,
): {file: string} {
return Reflector.getMetadata(
'example.msg.decorator.metadata.key',
controllerClass.prototype,
methodName,
);
}
```
{% include code-caption.html content="src/__tests__/unit/decorators/test.decorator.unit.ts" %}
```ts
import {test, getTestMetadata} from '../../..';
import {expect} from '/testlab';
describe('test.decorator (unit)', () => {
it('can store test name via a decorator', () => {
class TestClass {
('me.test.ts')
me() {}
}
const metadata = getTestMetadata(TestClass, 'me');
expect(metadata).to.be.a.Object();
expect(metadata.file).to.be.eql('me.test.ts');
});
});
```
#### Mixins
A Mixin is a TypeScript function that extends the `Application` Class, adding
new constructor properties, methods, etc. It is difficult to write a unit test
for a Mixin without the `Application` Class dependency. The recommended practice
is to write an integration test is described in
[Mixin Integration Tests](#mixin-integration-tests).
#### Providers
A Provider is a Class that implements the `Provider` interface. This interface
requires the Class to have a `value()` function. A unit test for a provider
should test the `value()` function by instantiating a new `Provider` class,
using a test double for any constructor arguments.
{% include code-caption.html content="src/providers/random-number.provider.ts" %}
```ts
import {Provider} from '/core';
export class RandomNumberProvider implements Provider<number> {
value() {
return (max: number): number => {
return Math.floor(Math.random() * max) + 1;
};
}
}
```
{% include code-caption.html content="src/__tests__/unit/providers/random-number.provider.unit.ts" %}
```ts
import {RandomNumberProvider} from '../../..';
import {expect} from '/testlab';
describe('RandomNumberProvider (unit)', () => {
it('generates a random number within range', () => {
const provider = new RandomNumberProvider().value();
const random: number = provider(3);
expect(random).to.be.a.Number();
expect(random).to.equalOneOf([1, 2, 3]);
});
});
```
#### 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 your
extension artifacts work together as well as ``. It is recommended to
test two items together and substitute other integrations as test doubles so it
becomes apparent where the integration errors may occur.
#### Mixin Integration Tests
A Mixin extends a base Class by returning an anonymous class. Thus, a Mixin is
tested by actually using the Mixin with its base Class. Since this requires two
Classes to work together, an integration test is needed. A Mixin test checks
that new or overridden methods exist and work as expected in the new Mixed
class. Following is an example for an integration test for a Mixin:
{% include code-caption.html content="src/mixins/time.mixin.ts" %}
```ts
import {Constructor} from '/core';
export function TimeMixin<T extends MixinTarget<object>>(superClass: T) {
return class extends superClass {
constructor(...args: any[]) {
super(...args);
if (!this.options) this.options = {};
if (typeof this.options.timeAsString !== 'boolean') {
this.options.timeAsString = false;
}
}
time() {
if (this.options.timeAsString) {
return new Date().toString();
}
return new Date();
}
};
}
```
{% include code-caption.html content="src/__tests__/integration/mixins/time.mixin.integration.ts" %}
```ts
import {expect} from '/testlab';
import {Application} from '/core';
import {TimeMixin} from '../../..';
describe('TimeMixin (integration)', () => {
it('mixed class has .time()', () => {
const myApp = new AppWithTime();
expect(typeof myApp.time).to.be.eql('function');
});
it('returns time as string', () => {
const myApp = new AppWithLogLevel({
timeAsString: true,
});
const time = myApp.time();
expect(time).to.be.a.String();
});
it('returns time as Date', () => {
const myApp = new AppWithLogLevel();
const time = myApp.time();
expect(time).to.be.a.Date();
});
class AppWithTime extends TimeMixin(Application) {}
});
```
### 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 the
extension artifacts such as decorators, mixins, providers, repositories, etc. No
test doubles are needed for an Acceptance test. This is a black box test where
you don't know or care about the internals of the extensions. You will be using
the extension as if you were the consumer.
Due to the complexity of an Acceptance test, there is no example given here.
Have a look at
[loopback4-example-log-extension](https://github.com/loopbackio/loopback-next/tree/master/examples/log-extension)
to understand the extension artifacts and their usage. An Acceptance test can be
seen here:
[src/**tests**/acceptance/log.extension.acceptance.ts](https://github.com/loopbackio/loopback-next/blob/master/examples/log-extension/src/__tests__/acceptance/log.extension.acceptance.ts).