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
Installing the dependencies:
npm install cypress typescript --save-dev
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
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"] }
To use some methods from cypress we will need to install it in our project:
npm install cypress --save-dev
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; };
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, }); } }
Basically this class provides a wrapper around Cypress’s cy.request()
command. When called, it:
- Sends an HTTP request using
cy.request()
with the givenmethod
,url
, and optionalbody
. - 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); } } }
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]); } } }
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" }, }
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
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" } }
⚠️
--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
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
Or if you are running it locally, after make the link, run in your project:
npm link @m4rri4nne/cypress-common
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) }) }) })
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
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)