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

[NP] add HTTP resources testing strategies #54908

Merged
merged 5 commits into from
Jan 28, 2020

Conversation

mshustov
Copy link
Contributor

Summary

Part of #53135
Documents integration- & unit-testing strategies for HTTP resources

[skip-ci]

@mshustov mshustov added chore Team:Core Core services & architecture: plugins, logging, config, saved objects, http, ES client, i18n, etc Feature:New Platform release_note:skip Skip the PR/issue when compiling release notes v7.7.0 labels Jan 15, 2020
@mshustov mshustov requested a review from a team as a code owner January 15, 2020 13:57
@elasticmachine
Copy link
Contributor

Pinging @elastic/kibana-platform (Team:Platform)

Copy link
Contributor

@pgayvallet pgayvallet left a comment

Choose a reason for hiding this comment

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

This is a very good presentation of existing testing tools, their pros/cons, and their preferred usage, with associated route testing exemples.

I'm just wondering if the type of tests presentation should really belongs in the HTTP Routes subsection or we should add a higher level section with them. Else I'm afraid we are going to introduce a lot of redundancy / repetition when describing how to test the other functional sections (so/es...), as the type of tests are quite similar.

Some rewordings/rephrasing comments: (please ignore some or all)

src/core/TESTING.md Outdated Show resolved Hide resolved
src/core/TESTING.md Outdated Show resolved Hide resolved
src/core/TESTING.md Outdated Show resolved Hide resolved
src/core/TESTING.md Outdated Show resolved Hide resolved
src/core/TESTING.md Outdated Show resolved Hide resolved
src/core/TESTING.md Outdated Show resolved Hide resolved
src/core/TESTING.md Show resolved Hide resolved
src/core/TESTING.md Outdated Show resolved Hide resolved
src/core/TESTING.md Show resolved Hide resolved
@mshustov
Copy link
Contributor Author

I'm just wondering if the type of tests presentation should really belongs in the HTTP Routes subsection or we should add a higher level section with them. Else I'm afraid we are going to introduce a lot of redundancy / repetition when describing how to test the other functional sections (so/es...), as the type of tests are quite similar.

It depends on the content of the other sections. 🤔
Do you want me to place them under Tools sections, for example?

@pgayvallet
Copy link
Contributor

pgayvallet commented Jan 21, 2020

It depends on the content of the other sections. 🤔
Do you want me to place them under Tools sections, for example?

I have the feeling the other sections will have a quite similar layout regarding the type of tests, so I think having a 'tool presentation' section before the guidelines on how to test the different part of the plugins would make sense yes.

Would like to have the others' opinion before having you restructure all this though. @rudolf @joshdover @eliperelman WDYT ?

@mshustov mshustov requested a review from a team January 22, 2020 10:26
@mshustov
Copy link
Contributor Author

@rudolf does it answer your question about route controllers testing #51150? I think the part with unit-tests should do the trick. Let me know if I can improve the docs.

Copy link
Contributor

@pgayvallet pgayvallet left a comment

Choose a reason for hiding this comment

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

LGTM. Would be nice to have a second reviewer though.

@rudolf
Copy link
Contributor

rudolf commented Jan 24, 2020

I think the "TestUtils" section is the closest to addressing #51150:

  1. Make it possible to use jest mocks for services so functional tests aren't required to test glue logic
  2. Test that the route path is correctly registered including any path parameters
  3. Test validation on request body and query parameters
  4. Make assertions against response bodies and HTTP status code

Your "TestUtils" example shows how to do (2) - (4) (or in the case of path parameters, it's obvious how to do it yourself), but it doesn't cover (1). In your example it's easy to test the controller logic that catches the MisformedTextError, but if we're trying to test controller logic that catches a SavedObjectNotFoundError that gets thrown by a service/model that works over the network, it's not obvious how to achieve that. Some pseudo code that I think could solve this...

import {dashboardModel} from './dashboardModel';
router.get(
  {
    path: '/myPlugin/dashboard/{id}',
    validate: {
      params: schema.object({
        id: schema.string({ maxLength: 100 }),
      }),
    },
  },
  async (context, request, response) => {
    try {
      const dashboard = await dashboardModel.find(request.params.id);
      return response.ok({ body: JSON.stringify(dashboard) });
    } catch(error) {
      if (error instanceof SavedObjectNotFoundError) {
        return response.badRequest({ body: error.message })
      }
    }
  }
);
// src/plugins/my_plugin/server/integration_tests/formatter.test.ts
jest.mock('../dashboardModel');

import { dashboardModel } from '../dashboardModel';
import * as kbnTestServer from 'src/test_utils/kbn_server';
dashboardModel.find.mockRejectedValueOnce(new SavedObjectNotFoundError());

describe('myPlugin', () => {
  describe('GET /myPlugin/dashboard', () => {
    let root: ReturnType<typeof kbnTestServer.createRoot>;
    beforeAll(async () => {
      root = kbnTestServer.createRoot();
      await root.setup();
      await root.start();
    }, 30000);

    afterAll(async () => await root.shutdown());
    it('returns 404 if dashboard not found', async () => {
      const response = await kbnTestServer.request
        .get(root, '/myPlugin/dashboard/10')
        .expect(404);

      expect(response.body).toHaveProperty('message');
    });
  });
});

Something else that could be useful would be to mock Core services or other plugins to test your controller logic, again I haven't tried this...

// src/plugins/my_plugin/server/integration_tests/formatter.test.ts
import * as kbnTestServer from 'src/test_utils/kbn_server';

describe('myPlugin', () => {
  describe('GET /myPlugin/dashboard', () => {
    let root: ReturnType<typeof kbnTestServer.createRoot>;
    let start;
    beforeAll(async () => {
      root = kbnTestServer.createRoot();
      await root.setup();
      start = await root.start();
      start.plugins.data = dataPluginMock;
    }, 30000);

    afterAll(async () => await root.shutdown());
    it('returns 404 if data plugin cannot locate index pattern', async () => {
      start.plugins.data.getIndexPattern.mockRejectedValueOnce(new NotFoundError());
      const response = await kbnTestServer.request
        .get(root, '/myPlugin/myroute/10')
        .expect(404);

      expect(response.body).toHaveProperty('message');
    });
  });
});

Copy link
Contributor

@rudolf rudolf left a comment

Choose a reason for hiding this comment

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

I think this document will be a really useful reference!

src/core/TESTING.md Outdated Show resolved Hide resolved
src/core/TESTING.md Show resolved Hide resolved

#### HTTP Routes
##### TestUtils
Copy link
Contributor

Choose a reason for hiding this comment

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

Although it's a form of integration testing, I wonder if we shouldn't move this out of the "Integration tests" section into it's own section called "Controller testing" or "controller integration testing". ITO the testing pyramid we would have from bottom to top: unit testing, controller testing, integration testing. Because it's a very lightweight type of integration test we should encourage more "controller testing" than fullstack integration tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

TestUtils is not limited by controller testing. For example, you can use it to test any data kept in memory by your plugin (registry pattern).

src/core/TESTING.md Outdated Show resolved Hide resolved
src/core/TESTING.md Show resolved Hide resolved
src/core/TESTING.md Outdated Show resolved Hide resolved
@mshustov
Copy link
Contributor Author

mshustov commented Jan 27, 2020

but if we're trying to test controller logic that catches a SavedObjectNotFoundError that gets thrown by a service/model that works over the network, it's not obvious how to achieve that.

Wouldn't it be enough to test it similarly to MisformedTextError?

// plugin
try{
  await model.do(savedObjectsClient)
} catch(error) {
  if (SavedObjectsErrorHelpers.isNotFoundError(error)) {
    return response.badRequest({ body: error.message })
  }
}
// test
it('allows SavedObjects "Not found" error to bubble up', async () => {
  jest.assertion(1);
  try{
    await model.do()
  } catch(error){
    expect(SavedObjectsErrorHelpers.isNotFoundError(error)).toBe(true);
  }
});

Regarding the two options that you proposed:

  • I think it's fine to mock your plugin internals for the integration test as you did in the first example. I will add a similar example to the guide.
  • There are lots of internal dependencies, and plugin mocking the platform services makes integration tests fragile. I'd avoid suggesting the second proposal.

WDYT?

@mshustov mshustov force-pushed the document-http-routes-testing branch from c1d2934 to dd3922c Compare January 28, 2020 10:28
@mshustov mshustov requested a review from rudolf January 28, 2020 10:41
expect(sanitizer.sanitize).toHaveBeenCalledWith('aaa');
});
afterAll(async () => await root.shutdown());
it('returns BadRequest if Sanitizer throws MisformedTextError', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

❤️

@rudolf
Copy link
Contributor

rudolf commented Jan 28, 2020

Wouldn't it be enough to test it similarly to MisformedTextError?

I like the example you added 👍

There are lots of internal dependencies, and plugin mocking the platform services makes integration tests fragile. I'd avoid suggesting the second proposal.
WDYT?

I agree, it's better to create an abstraction between your controller and dependencies rather than directly calling Core/Plugin API's from the controller.

@mshustov mshustov merged commit a831710 into elastic:master Jan 28, 2020
@mshustov mshustov deleted the document-http-routes-testing branch January 28, 2020 13:21
@mshustov mshustov added the backport:skip This commit does not require backporting label Jan 28, 2020
@mshustov mshustov removed the backport:skip This commit does not require backporting label Jan 28, 2020
mshustov added a commit to mshustov/kibana that referenced this pull request Jan 28, 2020
* add HTTP resources testing strategies

* address comments

* add error message test and update error test

* Apply suggestions from code review

Co-Authored-By: Rudolf Meijering <[email protected]>

* add controller testing example

Co-authored-by: Rudolf Meijering <[email protected]>
mshustov added a commit that referenced this pull request Jan 28, 2020
* add HTTP resources testing strategies

* address comments

* add error message test and update error test

* Apply suggestions from code review

Co-Authored-By: Rudolf Meijering <[email protected]>

* add controller testing example

Co-authored-by: Rudolf Meijering <[email protected]>

Co-authored-by: Rudolf Meijering <[email protected]>
gmmorris added a commit to gmmorris/kibana that referenced this pull request Jan 28, 2020
* master: (21 commits)
  [SIEM][Detection Engine] critical blocker updates to latest ECS version
  [Monitoring] Fix inaccuracies in logstash pipeline listing metrics (elastic#55868)
  Resetting errors and removing duplicates (elastic#56054)
  Add flag to opt out from sub url tracking (elastic#55672)
  [SIEM][Detection Engine] critical bug, fixes duplicate tags
  [ML] Anomaly Detection: Fix persist/restore of refreshInterval in globalState. (elastic#56113)
  [ML] Single Metric Viewer: Fix annnotations refresh. (elastic#56107)
  adapt ObjectToConfigAdapter.getFlattenedPaths to consider arrays as final values (elastic#56105)
  Add Appender.receiveAllLevels option to fix LegacyAppender (elastic#55752)
  [ML] Process delimited files like semi-structured text (elastic#56038)
  Charts plugin (combining ui/color_maps and EuiUtils) (elastic#55469)
  fix tutorial documentation (elastic#55996)
  [ML] Fix persist/restore of time/refreshInterval in data visualizer. (elastic#56122)
  [Index Management] Fix errors with validation (elastic#56072)
  [Index Management] Add try/catch when parsing index filter from URI (elastic#56051)
  [NP] add HTTP resources testing strategies (elastic#54908)
  [ML] Single Metric Viewer: Fix brush update on short recent timespans. (elastic#56125)
  [Uptime] Add timeout for slow process to skipped functional tests (elastic#56065)
  refactor (elastic#56121)
  Move tests in dashboard into appropriate folders (elastic#55304)
  ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
backported chore Feature:New Platform release_note:skip Skip the PR/issue when compiling release notes Team:Core Core services & architecture: plugins, logging, config, saved objects, http, ES client, i18n, etc v7.7.0 v8.0.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants