Skip to content

Commit

Permalink
update code examples
Browse files Browse the repository at this point in the history
  • Loading branch information
shimks committed Mar 2, 2018
1 parent f93733e commit 37a8169
Showing 1 changed file with 80 additions and 43 deletions.
123 changes: 80 additions & 43 deletions pages/en/lb4/Testing-your-application.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ Your `package.json` should then look something like this:
// ...
"devDependencies": {
"@loopback/testlab": "^<current-version>",
"@types/mocha": "^<current-version>",
"mocha": "^<current-version>"
},
"scripts": {
Expand All @@ -88,8 +89,8 @@ To clean the database before each test, set up a `beforeEach` hook to call a hel
{% include code-caption.html content="test/helpers/database.helpers.ts" %}
```ts
export async function givenEmptyDatabase() {
await new ProductRepository().deleteAll();
await new CategoryRepository().deleteAll();
await new ProductRepository(database).deleteAll();
await new CategoryRepository(database).deleteAll();
}

// in your test file
Expand All @@ -111,8 +112,9 @@ See [@loopback/openapi-spec-builder](https://www.npmjs.com/package/@loopback/ope

In practice, a rich method-based API is overkill and a simple function that adds missing required properties is sufficient.

{% include code-caption.html content="test/helpers/database.helpers.ts" %}
```ts
export function givenProductData(data: Partial<Product>) {
export function givenProductData(data?: Partial<Product>) {
return Object.assign({
name: 'a-product-name',
slug: 'a-product-slug',
Expand All @@ -122,7 +124,7 @@ export function givenProductData(data: Partial<Product>) {
}, data);
}

export async function givenProduct(data: Partial<Product>) {
export async function givenProduct(data?: Partial<Product>) {
return await new ProductRepository().create(
givenProductData(data));
}
Expand All @@ -140,7 +142,8 @@ It's tempting to define a small set of data that's shared by all tests. For exam

Use the test data builders described in the previous section to populate your database with the data specific to your test only.

Using the e-commerce example described above, this is how integration tests for the `CategoryRepository` might look:
<!-- NOTE(shmks): the code below deals with relations which has not been implemented in LoopBack4 yet. It needs to be revisited once it's been implemented. -->
<!-- Using the e-commerce example described above, this is how integration tests for the `CategoryRepository` might look:
```ts
describe('Category (integration)', () => {
Expand All @@ -159,7 +162,7 @@ describe('Category (integration)', () => {
const category = await givenCategory({
products: [await givenProduct()],
subcategories: [
givenCategory({
await givenCategory({
products: [await givenProduct()]
})
],
Expand All @@ -170,7 +173,7 @@ describe('Category (integration)', () => {
});
});
});
```
``` -->

Write higher-level helpers to share the code for re-creating common scenarios. For example, if your application has two kinds of users (admins and customers), then you may write the following helpers to simplify writing acceptance tests checking access control:

Expand Down Expand Up @@ -242,7 +245,7 @@ Verify how was the stubbed method executed at the end of your unit test (in the
```ts
// expect that repository.find() was called with the first
// argument deeply-equal to the provided object
expect(findStub).to.be.calledWithMatch({where: {id: 1}});
sinon.assert.calledWithMatch({where: {id: 1}});
```

See [Unit test your controllers](#unit-test-your-controllers) for a full example.
Expand All @@ -259,8 +262,9 @@ Unit tests should apply to the smallest piece of code possible to ensure other v

{% include code-caption.html content="test/unit/controllers/product.controller.test.ts" %}
```ts
import {ProductController, ProductRepository} from '../../..';
import {expect, sinon} from '@loopback/testlab';
import {ProductRepository} from '../../../src/repositories';
import {ProductController} from '../../../src/controllers';

describe('ProductController (unit)', () => {
let repository: ProductRepository;
Expand All @@ -270,12 +274,12 @@ describe('ProductController (unit)', () => {
it('retrieves details of a product', async () => {
const controller = new ProductController(repository);
const findStub = repository.find as sinon.SinonStub;
findStub.resolves([{id: 1, name: 'Pen'}]);
findStub.resolves([{name: 'Pen', slug: 'pen'}]);

const details = await controller.getDetails(1);
const details = await controller.getDetails('pen');

expect(details).to.containDeep({name: 'Pen'});
expect(findStub).to.be.calledWithMatch({where: {id: 1}});
expect(details).to.containEql({name: 'Pen', slug: 'pen'});
sinon.assert.calledWithMatch(findStub, {where: {slug: 'pen'}});
});
});

Expand All @@ -296,8 +300,8 @@ Remember to use [Test data builders](#use-test-data-builders) whenever you need
{% include code-caption.html content="test/unit/models/person.model.test.ts" %}

```ts
import {Person} from '../../../src/models/person.model'
import {givenPersonData} from '../../helpers/database.helpers'
import {Person} from '../../../src/models/person.model';
import {givenPersonData} from '../../helpers/database.helpers';
import {expect} from '@loopback/testlab';

describe('Person (unit)', () => {
Expand All @@ -307,8 +311,8 @@ describe('Person (unit)', () => {
const person = givenPerson({
firstname: 'Jane',
middlename: 'Smith',
surname: 'Brown'
}));
surname: 'Brown',
});

const fullName = person.getFullName();
expect(fullName).to.equal('Jane Smith Brown');
Expand All @@ -317,8 +321,8 @@ describe('Person (unit)', () => {
it('omits middlename when not present', () => {
const person = givenPerson({
firstname: 'Mark',
surname: 'Twain'
}));
surname: 'Twain',
});

const fullName = person.getFullName();
expect(fullName).to.equal('Mark Twain');
Expand Down Expand Up @@ -370,15 +374,21 @@ Here is an example showing how to write an integration test for a custom reposit

{% include code-caption.html content= "test/integration/repositories/category.repository.test.ts" %}
```ts
import {givenEmptyDatabase} from '../../helpers/database.helpers.ts';
import {
givenEmptyDatabase,
givenCategory,
} from '../../helpers/database.helpers';
import {CategoryRepository} from '../../../src/repositories';
import {expect} from '@loopback/testlab';
import {database} from '../../../src/datasources/db.datasource';

describe('CategoryRepository (integration)', () => {
beforeEach(givenEmptyDatabase);

describe('findByName(name)', () => {
it('return the correct category', async () => {
const stationery = await givenCategory({name: 'Stationery'});
const repository = new CategoryRepository();
const repository = new CategoryRepository(database);

const found = await repository.findByName('Stationery');

Expand All @@ -393,21 +403,23 @@ describe('CategoryRepository (integration)', () => {
Integration tests running controllers with real repositories are important to verify that the controllers use the repository API correctly, and the commands and queries produce expected results when executed on a real database. These tests are similar to repository tests: we are just adding controllers as another ingredient.

```ts
import {ProductController, ProductRepository, Product} from '../../fixtures';
import {expect} from '@loopback/testlab';
import {givenEmptyDatabase, givenProduct} from '../../helpers/database.helpers';
import {ProductController} from '../../../src/controllers';
import {ProductRepository} from '../../../src/repositories';
import {database} from '../../../src/datasources/db.datasource';

describe('ProductController (integration)', () => {
beforeEach(givenEmptyDatabase);

describe('getDetails()', () => {
it('retrieves details of the given product', async () => {
const pencil = await givenProduct({name: 'Pencil', slug: 'pencil'});
const controller = new ProductController(new ProductRepository());
const controller = new ProductController(new ProductRepository(database));

const details = await controller.getDetails('pencil');

expect(details).to.eql(pencil);
expect(details).to.containEql(pencil);
});
});
});
Expand Down Expand Up @@ -437,24 +449,32 @@ It enables API consumers to leverage a whole ecosystem of related tooling. To ma

Example usage:

{% include code-caption.html content= "test/acceptance/api-spec.test.ts" %}
```ts
// test/acceptance/api-spec.test.ts
import {validateApiSpec} from '@loopback/testlab';
import {HelloWorldApp} from '../..';
import {HelloWorldApplication} from '../..';
import {RestServer} from '@loopback/rest';
import {validateApiSpec} from '@loopback/testlab';

describe('API specification', () => {
it('api spec is valid', async () => {
const app = new HelloWorldApp();
const app = new HelloWorldApplication();
const server = await app.getServer(RestServer);
const spec = server.getApiSpec();
await validateApiSpec(apiSpec);
await validateApiSpec(spec);
});
});
```

### Perform an auto-generated smoke test of your REST API

{% include important.html content="
The top-down approach for building LoopBack
applications is not yet fully supported. Therefore, the code outlined in this
section is outdated and may not work out of the box. They will be revisited
after our MVP release.
" %}

The formal validity of your application's spec does not guarantee that your implementation is actually matching the specified behavior. To keep your spec in sync with your implementation, you should use an automated tool like [Dredd](https://www.npmjs.com/package/dredd) to run a set of smoke tests to verify conformance of your app with the spec.

Automated testing tools usually require little hints in your specification to tell them how to create valid requests or what response data to expect. Dredd in particular relies on response [examples](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#exampleObject) and request parameter [x-example](http://dredd.org/en/latest/how-to-guides.html#example-values-for-request-parameters) fields. Extending your API spec with examples is good thing on its own, since developers consuming your API will find them useful too.
Expand All @@ -463,9 +483,20 @@ Here is an example showing how to run Dredd to test your API against the spec:

{% include code-caption.html content= "test/acceptance/api-spec.test.ts" %}
```ts
import {expect} from '@loopback/testlab';
import {HelloWorldApplication} from '../..';
import {RestServer, RestBindings} from '@loopback/rest';
import {spec} from '../../apidefs/swagger';
const Dredd = require('dredd');

describe('API (acceptance)', () => {
let app: HelloWorldApplication;
// tslint:disable no-any
let dredd: any;
before(initEnvironment);
after(async () => {
await app.stop();
});

it('conforms to the specification', done => {
dredd.run((err: Error, stats: object) => {
Expand All @@ -480,13 +511,14 @@ describe('API (acceptance)', () => {
});

async function initEnvironment() {
const app = new HelloWorldApp();
app = new HelloWorldApplication();
const server = await app.getServer(RestServer);
// For testing, we'll let the OS pick an available port by setting
// RestBindings.PORT to 0.
server.bind(RestBindings.PORT).to(0);
// app.start() starts up the HTTP server and binds the acquired port
// number to RestBindings.PORT.
await app.boot();
await app.start();
// Get the real port number.
const port = await server.get(RestBindings.PORT);
Expand All @@ -497,11 +529,11 @@ describe('API (acceptance)', () => {
level: 'fail', // report 'fail' case only
silent: false, // false for helpful debugging info
path: [`${baseUrl}/swagger.json`], // to download apiSpec from the service
}
},
};
dredd = new Dredd(config);
});
})
}
});
```

The user experience is not as great as we would like it, we are looking into better solutions; see [GitHub issue #644](https://github.com/strongloop/loopback-next/issues/644). Let us know if you can recommend one!
Expand All @@ -512,19 +544,23 @@ You should have at least one acceptance (end-to-end) test for each of your REST

Here is an example of an acceptance test:

{% include code-caption.html content= "test/acceptance/product.test.ts" %}
```ts
// test/acceptance/product.acceptance.ts
import {HelloWorldApp} from '../..';
import {RestBindings, RestServer} from '@loopback/rest';
import {expect, supertest} from '@loopback/testlab';
// test/acceptance/product.test.ts
import {HelloWorldApplication} from '../..';
import {supertest, expect, createClientForHandler} from '@loopback/testlab';
import {givenEmptyDatabase, givenProduct} from '../helpers/database.helpers';
import {RestServer, RestBindings} from '@loopback/rest';

describe('Product (acceptance)', () => {
let app: HelloWorldApp;
let request: supertest.SuperTest<supertest.Test>;
let app: HelloWorldApplication;
let client: supertest.SuperTest<supertest.Test>;

before(givenEmptyDatabase);
before(givenRunningApp);
after(async () => {
await app.stop();
});

it('retrieves product details', async () => {
// arrange
Expand All @@ -541,20 +577,21 @@ describe('Product (acceptance)', () => {
const expected = Object.assign({id: product.id}, product);

// act
const response = await request.get('/product/ink-pen')
const response = await client.get('/product/ink-pen');

// assert
expect(response.body).to.deepEqual(expected);
expect(response.body).to.containEql(expected);
});

async function givenRunningApp() {
app = new HelloWorldApp();
app = new HelloWorldApplication();
const server = await app.getServer(RestServer);
server.bind(RestBindings.PORT).to(0);
await app.boot();
await app.start();

const port: number = await server.get(RestBindings.PORT);
request = supertest(`http://127.0.0.1:${port}`);
const port = await server.get(RestBindings.PORT);
client = createClientForHandler(server.handleHttp);
}
});
```
Expand Down

0 comments on commit 37a8169

Please sign in to comment.