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

test: E2E Mock API structure #2624

Merged
merged 15 commits into from Nov 15, 2021

Conversation

victorgcramos
Copy link
Member

@victorgcramos victorgcramos commented Oct 1, 2021

This commit adds a plugin extensible test support structure for e2e tests. It
allows the developer to have a more scalable structure for mock api test data,
create custom commands, and generate custom data for tests.

  • adds a plugin extensible test structure
  • improve the proposals list test to work offline using the new structure
  • improve docs for e2e testing including a tutorial of how to extend the core
    plugin to create new custom support plugins

In order to make our codebase more scalable, we organized the support commands
and files according to the politeia plugin
structure. Each plugin will have its own support files, which can extend the
core (records) support files and create custom commands. You can use the custom
commands to test the application.

Core Package

Let's take a deeper look at the teste2e/cypress/support/core:

teste2e/cypress/support/core
├── api.js # mock api repliers
├── commands.js # custom plugin commands
├── generate.js # mock data generators
└── utils.js # support utils and helpers

api.js:

Contains all repliers for the plugin api calls. Each replier will accept one
object with two parameters: testParams and requestParams. Both describe
respectively test configuration parameters, which can be used to configure
the response to match test pre-conditions, and request data sent to the API.

Let's build some replier for the /records/v1/inventory call:

import { Inventory } from "./generate";
import { stateToString } from "./utils";

export function inventoryReply({
  testParams: { amountByStatus = {}, pageLimit = 20 },
  requestParams: { state = 2, page }
}) {
  const inventory = new Inventory(amountByStatus, {
    page,
    pageLimit
  });
  return { [stateToString(state)]: inventory };
}
//...

Tip: all repliers names should be followed by the Reply suffix, such as
inventoryReply, recordsReply, policyReply...

As you can see, we are receiving { amountByStatus, pageLimit } from the test
case, which describe the status and state pre-conditions to run the tests.
{ state, page } is the request body object for the /inventory call (check
the records api docs).

commands.js

Contains all custom cypress commands that can be used for testing the plugin
contents. See the Cypress Docs
for more information about how to create custom commands.

The core package contains the custom commands that can be used by other
support plugins, and core support methods, which can be used to create custom
plugin commands, such as createMiddleware:

Let's build the recordsMiddleware command for the records package using
the createMiddleware method:

import "@testing-library/cypress/add-commands";
import { inventoryReply, recordsReply } from "./api";
// ...
Cypress.Commands.add(
  "recordsMiddleware",
  createMiddleware({
    packageName: "records",
    repliers: {
      // the replier we just created for the records/v1/inventory call
      inventory: inventoryReply,
      // other replier for the records/v1/records call
      records: recordsReply
    },
    baseUrl: "/api/records/v1"
  })
);

Tip: all middlewares names should be followed by the Middleware suffix, such
as recordsMiddleware, ticketvoteMiddleware, commentsMiddleware...

As mentioned before, "middlewares are nothing but an interceptor + alias", so
you can access the alias using the "{packageName}.{endpoint}" selector.

Now, we can use the recordsMiddleware to intercept the /inventory call:

// mytest.js
it("should fetch the inventory on records page", () => {
  // will automatically fetch the inventory
  cy.visit("/records");
  // will intercept the /records/v1/inventory call and return the mock data
  cy.recordsMiddleware("inventory", {
    amountByStatus: { authorized: 2, started: 3, unauthorized: 3 }
  });
  // wait for the call to be complete using the middleware alias.
  cy.wait("@records.inventory");
  // ...
});

generate.js

Contains all mock data generators for the plugin. We can generate custom data
using the faker NPM package, which is a
poweful tool to generate random data.

Let's create a mock Record generator to generate custom records for our tests:

import faker from "faker";

/**
 * Inventory instantiates a new inventory map with random tokens according to
 * the amountByStatus ({ [status]: amount }) object.
 * @param { Object } amountByStatus
 * @param { Object } { pageLimit, page }
 */
export function Inventory(amountByStatus = {}, { pageLimit = 20, page = 1 }) {
  return compose(
    reduce(
      (acc, [status, amount]) => ({
        ...acc,
        [status]: compose(
          get(page - 1),
          chunk(pageLimit),
          map(() => faker.git.shortSha().slice(0, 7)),
          range(amount)
        )(0)
      }),
      {}
    ),
    entries
  )(amountByStatus);
}

Usage:

const inv = new Inventory({ public: 2, archived: 1 });
// { public: ["87fah87", "5h97h53"], archived: ["a3575ca"] }

Tip: forcing generators to use the constructor pattern for our mock data is a
good option, because we can extend the prototype and use the instanceof
operator to enforce the "type ckecking". For example, we can check if some
proposal is a record using proposal instanceof Record condition.

How to create a support package

Previously, we talked about the core support package, but one of the greatest
features of our plugin extensible architecture is that you can use the core
package commands to create new custom support files. Let's build an example for
the ticketvote plugin and create a middleware for the
ticketvote/v1/inventory call.

  1. Create a new ticketvote folder under the teste2e/cypress/support
    dir including the new api.js, commands.js, generate.js and utils.js
    files. It should look like this:

    teste2e/cypress/support
    ├── core
    │   ├── api.js
    │   ├── commands.js
    │   ├── generate.js
    │   └── utils.js
    └── ticketvote
        ├── api.js
        ├── commands.js
        ├── generate.js
        └── utils.js
    
  2. Create the API replier for the /inventory request:

    import { inventoryReply as recordsInventoryReply } from "../core/api";
    import { statusToString } from "./utils";
    
    export function inventoryReply(props, { status, ...requestParams } = {}) {
      const inventory = recordsInventoryReply(props, requestParams);
      if (status) {
        const readableStatus = statusToString(status);
        return Object.entries(inventory).reduce(
          (acc, [state, statuses]) => ({
            ...acc,
            [state]: {
              [readableStatus]: statuses[readableStatus]
            }
          }),
          {}
        );
      }
      return inventory;
    }

    Notice that we can use the same recordsInventoryReply as a replier to the
    /inventory call, but we can also extend the replier and modify it to
    whatever we want. In this case, we want to filter the inventory to match the
    request status, so we can only reply tokens for the given status, if it
    exists.

  3. Create the ticketvote middleware using the core createMiddleware method:

    import "@testing-library/cypress/add-commands";
    import { createMiddleware } from "../core/commands";
    import { inventoryReply } from "./api";
    
    Cypress.Commands.add(
      "ticketvoteMiddleware",
      createMiddleware({
        packageName: "ticketvote",
        repliers: {
          inventory: inventoryReply
        },
        baseUrl: "/api/ticketvote/v1"
      })
    );
  4. Write the test case under teste2e/cypress/e2e dir:

    // mytest.js
    it("should fetch the ticketvote inventory on home page", () => {
      // ticketvote page will automatically fetch the inventory
      cy.visit("/ticketvote");
      // will intercept the /ticketvote/v1/inventory call and return
      // the mock data
      cy.ticketvoteMiddleware("inventory", {
        amountByStatus: { authorized: 1, started: 1, unauthorized: 1 }
      });
      // wait for the call to be complete using the middleware alias.
      cy.wait("@ticketvote.inventory").then((response) => {
        // {
        //   vetted: {
        //     authorized: ["b5aa3b2"],
        //     started: ["ab76379"],
        //     unauthorized: ["d9a78d5"]
        //   }
        // }
      });
      // Will automatically fetch the authorized tokens
      cy.visit("/ticketvote/authorized");
      cy.wait("@ticketvote.inventory").then((response) => {
        // {
        //   vetted: {
        //     authorized: ["b5aa3b2"]
        //   }
        // }
      });
      // ...
    });

    Tip: test cases are linked to the application, while the support packages
    are linked to the backend structure.

  5. You can combine the recordsMiddleware and ticketvoteMiddleware to
    generate custom records for the ticketvote inventory tokens:

    it("should fetch records from ticketvote inventory", () => {
      cy.ticketvoteMiddleware("inventory", {
        amountByStatus: { authorized: 1, started: 1, unauthorized: 1 }
      });
      cy.recordsMiddleware("records", { state: 2, status: 2 });
    
      cy.visit("ticketvote/records?tab=authorized");
      cy.findAllByTestId("record").should("have.length", 1); // OK
      cy.visit("ticketvote/records?tab=started");
      cy.findAllByTestId("record").should("have.length", 1); // OK
      cy.visit("ticketvote/records?tab=unauthorized");
      cy.findAllByTestId("record").should("have.length", 1); // OK
    });

This commit adds a plugin extensible test structure for e2e tests. It
allows the developer to have a more scalable support structure for
mock api test data, create custom commands, and generate custom data for
tests. This diff also includes a working offline test example for
proposals list.
@victorgcramos victorgcramos changed the title [wip] test: E2E Mock API structure test: E2E Mock API structure Oct 13, 2021
@victorgcramos victorgcramos marked this pull request as ready for review October 13, 2021 00:36
Copy link
Member

@amass01 amass01 left a comment

Choose a reason for hiding this comment

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

@victorgcramos Nice work mate, I like the direction of this.

I got one suggestion regarding the params here:

export function inventoryReply(
  { amountByStatus = {}, pageLimit = 20 } = {},
  { state = 2, page }
)

I would change the function to accept one options object with all configuration instead of two destructed objects. This makes it easier to read and maintain, what do you think ?

@victorgcramos
Copy link
Member Author

hey @amass01, I liked your suggestion, but I opted to split those parameters so we can have a better idea of what's the request body data, and what are the test configuration parameters. Making it an argument is a good suggestion, but what about using a single object approach, which receives 2 destructured props: testParams and requestParams?

export function inventoryReply(
  { testParams: { amountByStatus, pageLimit }, requestParams: { state, page } }
)

@amass01
Copy link
Member

amass01 commented Oct 13, 2021

@victorgcramos yep this what i had in mind

@victorgcramos victorgcramos marked this pull request as draft October 26, 2021 21:39
@victorgcramos victorgcramos marked this pull request as ready for review October 27, 2021 23:24
Copy link
Member

@amass01 amass01 left a comment

Choose a reason for hiding this comment

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

Nice work mate, good job with the core package, I like it.

Found one TODO that can be done in this PR.

@@ -9,6 +9,7 @@ import get from "lodash/fp/get";
import find from "lodash/fp/find";
import path from "path";

// TODO: move record related utils to core package
Copy link
Member

Choose a reason for hiding this comment

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

a TODO

Comment on lines +30 to +32
beforeEach(function useAppMiddlewares() {
// here you can define global middlewares
});
Copy link
Member

Choose a reason for hiding this comment

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

oh, that's great and will be useful when migration to 100% mocked APIs.

@amass01
Copy link
Member

amass01 commented Nov 3, 2021

Hitting some failing tests here, not sure if they are related to my setup:

image

Copy link
Member

@amass01 amass01 left a comment

Choose a reason for hiding this comment

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

tACK.

====================================================================================================
  (Run Finished)
       Spec                                              Tests  Passing  Failing  Pending  Skipped
  ┌────────────────────────────────────────────────────────────────────────────────────────────────┐
  │ ✔  admin/account.js                         00:10        5        5        -        -        - │
  ├────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ ✔  admin/comments.js                        00:07        1        1        -        -        - │
  ├────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ ✔  admin/proposals.js                       00:43        8        8        -        -        - │
  ├────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ ✔  comments/authorUpdates.js                00:22        3        3        -        -        - │
  ├────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ ✔  comments/comments.js                     00:29        5        5        -        -        - │
  ├────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ ✔  comments/commentVotes.js                 00:27        5        5        -        -        - │
  ├────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ ✔  proposal/create.js                       00:04        1        1        -        -        - │
  ├────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ ✔  proposal/detail.js                       00:53       22       22        -        -        - │
  ├────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ ✔  proposal/edit.js                         00:24        4        4        -        -        - │
  ├────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ ✔  proposal/formErrorCodes.js               00:06        5        5        -        -        - │
  ├────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ ✔  proposal/list.js                         00:41       16       16        -        -        - │
  ├────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ ✔  user/2fa.js                              00:07        2        2        -        -        - │
  ├────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ ✔  user/login.js                            00:34        7        7        -        -        - │
  ├────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ ✔  user/register.js                         00:14        2        2        -        -        - │
  └────────────────────────────────────────────────────────────────────────────────────────────────┘
    ✔  All specs passed!                        05:27       86       86        -        -        -

@tiagoalvesdulce
Copy link
Member

@victorgcramos conflicts

Copy link
Member

@tiagoalvesdulce tiagoalvesdulce left a comment

Choose a reason for hiding this comment

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

LGTM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants