DEV Community

Cover image for Write Cypress Commands Once, Use Everywhere: Building a Reusable NPM Package
Alicia Marianne
Alicia Marianne

Posted on

Write Cypress Commands Once, Use Everywhere: Building a Reusable NPM Package

We already know that cypress allow us to create commands that can be used in different tests, but this is on the context of the project, imagine that you have different teams but they have similar workflows, like, authentication. Each team will need to create a command or a test itself related to authentication, but what if I tell you that you can just create it once and use in different projects? In this article we will see how create our own npm package that can be used in any of your cypress projects.

What we are going to learn

For this tutorial we will use tests that I used on this article API Testing with Cypress - Part I . During this tutorial we will:

  • How to create the common project library
  • Implement the first common features to this project
  • Publish our npm common project on NPM
  • Using this common in our project

Setup of the project

Let's start our project creating a new project:

mkdir cypress-common cd cypress-common npm init -y 
Enter fullscreen mode Exit fullscreen mode

Installing the dependencies:

npm install cypress typescript --save-dev 
Enter fullscreen mode Exit fullscreen mode

Our project will have the following structure:

cypress-common/ ├── src/ │ ├── utils/ │ │ └── requestService.ts │ │ └── responseValidation.ts │ │ └── constractValidation.ts │ ├── contracts/ │ │ └── bookContracts.json │ ├── mocks/ │ │ └── example.ts │ ├── pages/ │ │ └── login.ts │ └── index.ts ├── tsconfig.json ├── package.json 
Enter fullscreen mode Exit fullscreen mode

Adding the configuration of TypeScript:

{ "compilerOptions": { "target": "es6", "lib": ["es6", "dom"], "moduleResolution": "node", "strict": true, "esModuleInterop": true, "resolveJsonModule": true, "declaration": true, "outDir": "./dist", "types": ["cypress"] }, "include": ["src/**/*.ts"] } 
Enter fullscreen mode Exit fullscreen mode

To use some methods from cypress we will need to install it in our project:

 npm install cypress --save-dev 
Enter fullscreen mode Exit fullscreen mode

Creating the features of our common library

requestService.ts

First feature that we will add in our common, is the request feature. For this feature, we need to receive the method, url and the body if needed, for it, we will create a type called BaseRequestOptions that is a custom type alias that defines the shape of an object:

type BaseRequestOptions = { method: Cypress.HttpMethod; url: string; body?: any; }; 
Enter fullscreen mode Exit fullscreen mode

Now we can create the class and our method:

type BaseRequestOptions = { method: Cypress.HttpMethod; url: string; body?: any; }; export class RequestService { request({ method, url, body = null }: BaseRequestOptions): Cypress.Chainable<Cypress.Response<any>>{ return cy.request({ method, url, body, failOnStatusCode: false, }); } } 
Enter fullscreen mode Exit fullscreen mode

Basically this class provides a wrapper around Cypress’s cy.request() command. When called, it:

  • Sends an HTTP request using cy.request() with the given methodurl, and optional body.
  • Sets failOnStatusCode: false so Cypress does not fail the test automatically if the response status is an error(like 4xx or 5xx).
  • Returns a Cypress.Chainable<Cypress.Response<any>>, which means you get back a Cypress chainable object wrapping the HTTP response, so you can continue chaining .then().should(), etc., in your test.

contractValidation.ts

The ContractValidator class validates that a given object has all the required properties defined in a contract schema and that each property matches its expected type (string, number, boolean, object, or array).This is a good way to make sure that your APIs are respecting the contract defined.

type SupportedTypes = 'string' | 'number' | 'boolean' | 'object' | 'array'; type ContractSchema = Record<string, SupportedTypes>; export class ContractValidator { private schema: ContractSchema; constructor(schema: ContractSchema) { this.schema = schema; } validate(response: Record<string, any>): void { for (const key in this.schema) { const expectedType = this.schema[key]; // Check property exists expect(response, `Response should have property '${key}'`).to.have.property(key); const value = response[key]; const actualType = Array.isArray(value) ? 'array' : typeof value; // Check type matches expected type expect(actualType, `Property '${key}' should be of type '${expectedType}', but got '${actualType}'`).to.equal(expectedType); } } } 
Enter fullscreen mode Exit fullscreen mode

responseValidation.ts

The ResponseValidation class verifies that all properties of a ResponseBody object from a request match exactly with those in a corresponding response, using equality checks for each field

type ResponseBody = { id: number, title: string, description: string, pageCount: number, excerpt: string, publishDate: string } export class ResponseValidation{ responseValidation(requestBody: ResponseBody, responseBody: ResponseBody): void{ for (const key in requestBody) { // Check that the property exists in the response expect(responseBody, `Response should have property '${key}'`).to.have.property(key); // Check that values match expect(responseBody[key as keyof ResponseBody], `Property '${key}' should match`).to.equal(requestBody[key as keyof ResponseBody]); } } } 
Enter fullscreen mode Exit fullscreen mode

Using and publishing your common project

Now that we have our common ready, we need to build it and start to use in our cypress projects.

Build and run locally

First, go to you package.json and make add to it or update:

{ "main": "dist/index.js", "scripts": { "build": "tsc" }, } 
Enter fullscreen mode Exit fullscreen mode

After that, you can run the command npm run build It runs the TypeScript compiler (tsc) to transpile TypeScript code (.ts / .tsx) into JavaScript (.js).

When it is done, you need to run npm link this way you will be able to add to your project.

Publishing on NPM

First you need to created your account on npmjs. After that, on you terminal run:

npm login 
Enter fullscreen mode Exit fullscreen mode

After do the login, make sure that your package.json contains:

{ "name": "@m4rri4nne/cypress-common", "version": "1.0.0", "main": "dist/index.js", "publishConfig": { "access": "public" } } 
Enter fullscreen mode Exit fullscreen mode

⚠️ --access public is mandatory on the 1st publication if the package has a scope (like @m4rri4nne/). If you've removed the scope, --access public is optional, but you can leave it in.

After that run:

 npm publish --access public 
Enter fullscreen mode Exit fullscreen mode

After that you can check you package created on your profile on npmjs.

Using in our project

Now that we have the common package done, you can add to your cypress project running: .

 npm install @m4rri4nne/cypress-common 
Enter fullscreen mode Exit fullscreen mode

Or if you are running it locally, after make the link, run in your project:

 npm link @m4rri4nne/cypress-common 
Enter fullscreen mode Exit fullscreen mode
Changing the POST method

Refactoring our existing tests using the common:

 import {RequestService, ContractValidator, bookContract, ResponseValidation } from '@m4rri4nne/cypress-common'; // Declaring the depencencies  const requestService = new RequestService(); const validator = new ContractValidator(bookContract); const responseValidation = new ResponseValidation(); const baseUrl = "https://fakerestapi.azurewebsites.net/api/v1/Books" describe('Testing POST Method', ()=>{ it('Create a book with success', ()=>{ const body = { id: 100, title: "string1", description: "string1", pageCount: 100, excerpt: "string", publishDate: "2023-10-14T18:44:34.674Z" } requestService.request({ method: 'POST', url: baseUrl, body: body }).then((response) =>{ expect(response.status).to.equal(200) validator.validate(response.body) responseValidation.responseValidation(body, response.body) }) }) }) 
Enter fullscreen mode Exit fullscreen mode

You can check the other refactoring here.

Why create a library instead using cypress commands?

I think the only possible answer to that question is a big Depends.
If we take the execution of tests that have been refactored, we can see a significant reduction in test execution time

test results

But is it worth developing a project from scratch simply for a few milliseconds? That will depend on your project, your time available to maintain it. But there are a few advantages I want to highlight that might make you consider it:

  • Reusing code between projects: You create customised commands, request services, validators, etc. only once in the common project. Different teams and projects can install via NPM and use it, ensuring consistency.
  • Strong typing with TypeScript: Avoids passing the wrong parameters to functions and commands due validation at build time. Facilitates maintenance, because TS itself shows where your code breaks if you change something.
  • Single, consistent standard: All teams follow the same standard for requests, validations, commands, etc. Easy to apply good practices (e.g. failOnStatusCode, response schemas, logs).
  • Reduction of duplication: No need to recreate the same login commands, data creation, mocks, etc. in each Cypress project. Updates and bug fixes are done in one place.
  • Better maintenance and evolution: If a flow changes (e.g. the company login), you adjust it in the common project and all the projects that use it already benefit from updating the version.

These were one of the main reasons that encouraged me to try to create this and I hope it motivates you too. I still intend to add more things to this common and if you have any questions or ideas, just get in touch with me.

You can see the common project here

Top comments (0)