Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Saved Objects testing #56965

Merged
merged 4 commits into from
Feb 7, 2020
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
242 changes: 231 additions & 11 deletions src/core/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,34 @@

This document outlines best practices and patterns for testing Kibana Plugins.

- [Strategy](#strategy)
- [Core Integrations](#core-integrations)
- [Core Mocks](#core-mocks)
- [Testing Kibana Plugins](#testing-kibana-plugins)
- [Strategy](#strategy)
- [New concerns in the Kibana Platform](#new-concerns-in-the-kibana-platform)
- [Core Integrations](#core-integrations)
- [Core Mocks](#core-mocks)
- [Example](#example)
- [Strategies for specific Core APIs](#strategies-for-specific-core-apis)
- [HTTP Routes](#http-routes)
- [SavedObjects](#savedobjects)
- [Elasticsearch](#elasticsearch)
- [Plugin Integrations](#plugin-integrations)
- [Plugin Contracts](#plugin-contracts)
- [HTTP Routes](#http-routes)
- [Preconditions](#preconditions)
- [Unit testing](#unit-testing)
- [Example](#example-1)
- [Integration tests](#integration-tests)
- [Functional Test Runner](#functional-test-runner)
- [Example](#example-2)
- [TestUtils](#testutils)
- [Example](#example-3)
- [Applications](#applications)
- [Example](#example-4)
- [SavedObjects](#savedobjects)
- [Unit Tests](#unit-tests)
- [Integration Tests](#integration-tests-1)
- [Elasticsearch](#elasticsearch)
- [Plugin integrations](#plugin-integrations)
- [Preconditions](#preconditions-1)
- [Testing dependencies usages](#testing-dependencies-usages)
- [Testing components consuming the dependencies](#testing-components-consuming-the-dependencies)
- [Testing optional plugin dependencies](#testing-optional-plugin-dependencies)
- [Plugin Contracts](#plugin-contracts)

## Strategy

Expand Down Expand Up @@ -540,11 +559,212 @@ describe('renderApp', () => {
});
```

#### SavedObjects
### SavedObjects

_How to test SO operations_
#### Unit Tests

#### Elasticsearch
To unit test code that uses the Saved Objects client mock the client methods
and make assertions against the behaviour you would expect to see.

Since the Saved Objects client makes network requests to an external
Elasticsearch cluster, it's important to include failure scenarios in your
test cases.

When writing a view with which a user might interact, it's important to ensure
your code can recover from exceptions and provide a way for the user to
proceed. This behaviour should be tested as well.

Below is an example of a Jest Unit test suite that mocks the server-side Saved
Objects client:

```typescript
// src/plugins/myplugin/server/lib/short_url_lookup.ts
import crypto from 'crypto';
import { SavedObjectsClientContract } from 'kibana/server';

export const shortUrlLookup = {
generateUrlId(url: string, savedObjectsClient: SavedObjectsClientContract) {
const id = crypto
.createHash('md5')
.update(url)
.digest('hex');

return savedObjectsClient
.create(
'url',
{
url,
accessCount: 0,
createDate: new Date().valueOf(),
accessDate: new Date().valueOf(),
},
{ id }
)
.then(doc => doc.id)
.catch(err => {
if (savedObjectsClient.errors.isConflictError(err)) {
return id;
} else {
throw err;
}
});
},
};

```

```typescript
// src/plugins/myplugin/server/lib/short_url_lookup.test.ts
import { shortUrlLookup } from './short_url_lookup';
import { savedObjectsClientMock } from '../../../../../core/server/mocks';

describe('shortUrlLookup', () => {
const ID = 'bf00ad16941fc51420f91a93428b27a0';
const TYPE = 'url';
const URL = 'http://elastic.co';

const mockSavedObjectsClient = savedObjectsClientMock.create();

beforeEach(() => {
jest.resetAllMocks();
});

describe('generateUrlId', () => {
it('provides correct arguments to savedObjectsClient', async () => {
const ATTRIBUTES = {
url: URL,
accessCount: 0,
createDate: new Date().valueOf(),
accessDate: new Date().valueOf(),
};
mockSavedObjectsClient.create.mockResolvedValueOnce({
id: ID,
type: TYPE,
references: [],
attributes: ATTRIBUTES,
});
await shortUrlLookup.generateUrlId(URL, mockSavedObjectsClient);

expect(mockSavedObjectsClient.create).toHaveBeenCalledTimes(1);
const [type, attributes, options] = mockSavedObjectsClient.create.mock.calls[0];
expect(type).toBe(TYPE);
expect(attributes).toStrictEqual(ATTRIBUTES);
expect(options).toStrictEqual({ id: ID });
});

it('ignores version conflict and returns id', async () => {
mockSavedObjectsClient.create.mockRejectedValueOnce(
mockSavedObjectsClient.errors.decorateConflictError(new Error())
);
const id = await shortUrlLookup.generateUrlId(URL, mockSavedObjectsClient);
expect(id).toEqual(ID);
});

it('rejects with passed through savedObjectsClient errors', () => {
const error = new Error('oops');
mockSavedObjectsClient.create.mockRejectedValueOnce(error);
return expect(shortUrlLookup.generateUrlId(URL, mockSavedObjectsClient)).rejects.toBe(error);
});
});
});
```

The following is an example of a public saved object unit test. The biggest
difference with the server-side test is the slightly different Saved Objects
client API which returns `SimpleSavedObject` instances which needs to be
reflected in the mock.

```typescript
// src/plugins/myplugin/public/saved_query_service.ts
import { SavedObjectsClientContract, SavedObjectAttributes } from 'src/core/public';

export const createSavedQueryService = (savedObjectsClient: SavedObjectsClientContract) => {
const saveQuery = async (attributes: SavedObjectAttributes) => {
try {
return await savedObjectsClient.create('query', attributes, {
id: attributes.title as string,
});
} catch (err) {
throw new Error('Unable to create saved query, please try again.');
}
};

return {
saveQuery,
};
};
```

```typescript
// src/plugins/myplugin/public/saved_query_service.test.ts
import { createSavedQueryService } from './saved_query_service';
import { savedObjectsServiceMock } from '../../../core/public/mocks';
import { SavedObjectsClientContract, SimpleSavedObject } from '../../../core/public';

describe('saved query service', () => {
const savedQueryAttributes = {
title: 'foo',
description: 'bar',
query: {
language: 'kuery',
query: 'response:200',
},
};

const mockSavedObjectsClient = savedObjectsServiceMock.createStartContract()
.client as jest.Mocked<SavedObjectsClientContract>;

const savedQueryService = createSavedQueryService(mockSavedObjectsClient);

afterEach(() => {
jest.resetAllMocks();
});

describe('saveQuery', function() {
it('should create a saved object for the given attributes', async () => {
// The public Saved Objects client returns instances of
// SimpleSavedObject, so we create an instance to return from our mock.
rudolf marked this conversation as resolved.
Show resolved Hide resolved
const mockReturnValue = new SimpleSavedObject(mockSavedObjectsClient, {
type: 'query',
id: 'foo',
attributes: savedQueryAttributes,
references: [],
});
mockSavedObjectsClient.create.mockResolvedValue(mockReturnValue);

const response = await savedQueryService.saveQuery(savedQueryAttributes);
expect(mockSavedObjectsClient.create).toHaveBeenCalledWith('query', savedQueryAttributes, {
id: 'foo',
});
expect(response).toBe(mockReturnValue);
});

it('should reject with an error when saved objects client errors', () => {
mockSavedObjectsClient.create.mockRejectedValue(new Error('timeout'));

return expect(
rudolf marked this conversation as resolved.
Show resolved Hide resolved
savedQueryService.saveQuery(savedQueryAttributes)
).rejects.toMatchInlineSnapshot(`[Error: Unable to create saved query, please try again.]`);
});
});
});
```

#### Integration Tests
To get the highest confidence in how your code behaves when using the Saved
Objects client, you should write at least a few integration tests which loads
data into and queries a real Elasticsearch database.

To do that we'll write a Jest integration test using `TestUtils` to start
Kibana and esArchiver to load fixture data into Elasticsearch.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

another option could be to load data via HTTP endpoint since most likely plugin receives user data in this way.


1. Create the fixtures data you need in Elasticsearch
2. Create a fixtures archive with `node scripts/es_archiver save <name> [index patterns...]`
3. Load the fixtures in your test using esArchiver `esArchiver.load('name')`;

_todo: fully worked out example_

### Elasticsearch

_How to test ES clients_

Expand Down
4 changes: 2 additions & 2 deletions src/core/public/legacy/legacy_service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ import { uiSettingsServiceMock } from '../ui_settings/ui_settings_service.mock';
import { LegacyPlatformService } from './legacy_service';
import { applicationServiceMock } from '../application/application_service.mock';
import { docLinksServiceMock } from '../doc_links/doc_links_service.mock';
import { savedObjectsMock } from '../saved_objects/saved_objects_service.mock';
import { savedObjectsServiceMock } from '../saved_objects/saved_objects_service.mock';
import { contextServiceMock } from '../context/context_service.mock';

const applicationSetup = applicationServiceMock.createInternalSetupContract();
Expand Down Expand Up @@ -97,7 +97,7 @@ const injectedMetadataStart = injectedMetadataServiceMock.createStartContract();
const notificationsStart = notificationServiceMock.createStartContract();
const overlayStart = overlayServiceMock.createStartContract();
const uiSettingsStart = uiSettingsServiceMock.createStartContract();
const savedObjectsStart = savedObjectsMock.createStartContract();
const savedObjectsStart = savedObjectsServiceMock.createStartContract();
const fatalErrorsStart = fatalErrorsServiceMock.createStartContract();
const mockStorage = { getItem: jest.fn() } as any;

Expand Down
5 changes: 3 additions & 2 deletions src/core/public/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { i18nServiceMock } from './i18n/i18n_service.mock';
import { notificationServiceMock } from './notifications/notifications_service.mock';
import { overlayServiceMock } from './overlays/overlay_service.mock';
import { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock';
import { savedObjectsMock } from './saved_objects/saved_objects_service.mock';
import { savedObjectsServiceMock } from './saved_objects/saved_objects_service.mock';
import { contextServiceMock } from './context/context_service.mock';
import { injectedMetadataServiceMock } from './injected_metadata/injected_metadata_service.mock';

Expand All @@ -40,6 +40,7 @@ export { legacyPlatformServiceMock } from './legacy/legacy_service.mock';
export { notificationServiceMock } from './notifications/notifications_service.mock';
export { overlayServiceMock } from './overlays/overlay_service.mock';
export { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock';
export { savedObjectsServiceMock } from './saved_objects/saved_objects_service.mock';

function createCoreSetupMock({ basePath = '' } = {}) {
const mock = {
Expand Down Expand Up @@ -70,7 +71,7 @@ function createCoreStartMock({ basePath = '' } = {}) {
notifications: notificationServiceMock.createStartContract(),
overlays: overlayServiceMock.createStartContract(),
uiSettings: uiSettingsServiceMock.createStartContract(),
savedObjects: savedObjectsMock.createStartContract(),
savedObjects: savedObjectsServiceMock.createStartContract(),
injectedMetadata: {
getInjectedVar: injectedMetadataServiceMock.createStartContract().getInjectedVar,
},
Expand Down
4 changes: 2 additions & 2 deletions src/core/public/plugins/plugins_service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ import { injectedMetadataServiceMock } from '../injected_metadata/injected_metad
import { httpServiceMock } from '../http/http_service.mock';
import { CoreSetup, CoreStart, PluginInitializerContext } from '..';
import { docLinksServiceMock } from '../doc_links/doc_links_service.mock';
import { savedObjectsMock } from '../saved_objects/saved_objects_service.mock';
import { savedObjectsServiceMock } from '../saved_objects/saved_objects_service.mock';
import { contextServiceMock } from '../context/context_service.mock';

export let mockPluginInitializers: Map<PluginName, MockedPluginInitializer>;
Expand Down Expand Up @@ -110,7 +110,7 @@ describe('PluginsService', () => {
notifications: notificationServiceMock.createStartContract(),
overlays: overlayServiceMock.createStartContract(),
uiSettings: uiSettingsServiceMock.createStartContract(),
savedObjects: savedObjectsMock.createStartContract(),
savedObjects: savedObjectsServiceMock.createStartContract(),
fatalErrors: fatalErrorsServiceMock.createStartContract(),
};
mockStartContext = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ const createMock = () => {
return mocked;
};

export const savedObjectsMock = {
export const savedObjectsServiceMock = {
create: createMock,
createStartContract: createStartContractMock,
};
4 changes: 2 additions & 2 deletions src/core/public/saved_objects/simple_saved_object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

import { get, has, set } from 'lodash';
import { SavedObject as SavedObjectType, SavedObjectAttributes } from '../../server';
import { SavedObjectsClient } from './saved_objects_client';
import { SavedObjectsClientContract } from './saved_objects_client';

/**
* This class is a very simple wrapper for SavedObjects loaded from the server
Expand All @@ -41,7 +41,7 @@ export class SimpleSavedObject<T extends SavedObjectAttributes> {
public references: SavedObjectType<T>['references'];

constructor(
private client: SavedObjectsClient,
private client: SavedObjectsClientContract,
{ id, type, version, attributes, error, references, migrationVersion }: SavedObjectType<T>
) {
this.id = id;
Expand Down