Building REST API with Express, TypeScript - Part 4: Jest and unit testing
In the previous post, we set up and connected a postgres database to the Express and TypeScript REST API server and wrote some REST APIs. In this post, we will set up a testing framework and add unit tests to test those APIs.
Why to test ?
Testing is an important part of the software development life cycle. It is a process of verifying the behavior of the application. It validates that the application is working as it is intended to be and meets all the requirements.
The codebase grows as we introduce more features to the application. It is difficult and not efficient to verify all features manually before every release. Thus writing tests to verify the behavior of the application is easier and more efficient. We can just run the tests after making any changes to verify that the application is working as expected.
Testing helps to discover bugs early in the development lifecycle and increases developer confidence in releasing any new changes.
There are multiple types of software testing. In this blog, we will focus mainly on unit testing.
Unit Testing
In unit testing, we test the unit of the application in isolation. A unit can be anything like class, function, method, etc. Unit tests require minimum effort to set up and write tests. Unit tests also act as a self-documentation for the function/method. It also helps in debugging as the developer can focus on one part of the application instead of the whole app.
Setup Jest
There are multiple frameworks for unit testing JavaScript code. We will be using Jest which is currently the most popular testing framework.
Let's setup and configure Jest in our express-ts server.
Install Jest and its type definitions as development dependencies.
npm i -D jest @types/jest
Add test
script in package.json
. In test script call the jest
command.
package.json
"scripts": {
...
"test": "jest"
},
Let's add a dummy test to check if the Jest setup so far is working or not.
src/controllers/ping.controller.test.ts
test("it should pass", async () => {
expect(true).toBe(false);
});
npm test
After running the test
command the test should fail. Change the false
to true
and re-run the test again. It should pass. The Jest setup so far is working fine.
Let's add a test for the ping controller. The getMessage
method has to return pong
. We will verify this in the test.
src/controllers/ping.controller.test.ts
import PingController from "./ping.controller";
test("should return pong message", async () => {
const controller = new PingController();
const response = await controller.getMessage();
expect(response.message).toBe("pong");
});
npm test
After running the test
command, the test will fail as we are importing a typescript file in the test. We need to transpile it. Let's install ts-jest
as a dev dependency and create the Jest configuration file by ts-jest's config:init
command and re-run the test again after this.
npm i -D ts-jest
npx ts-jest config:init
npm test
The ts-jest's config:init
command will add the jest.config.js
file to the root of the project and after running the command the test will pass. The Jest setup is done, we can add some tests for the server.
Add Tests
Let's start with the user controller. Create the user.controller.test.ts
file in the controllers
folder next to the user.controller.ts
file. We will group the tests with describe
block.
Here we will be mocking the UserRepository
as in unit testing it is better to test just the unit (function/method) and mock the dependencies. Mocking the dependencies gives more control as now the developer can mock the behavior of the dependencies and can test multiple edge cases without messing up with the actual dependency. Like here will change the return value of the UserRepository.getUsers
method as our test cases without setting up the Database.
src/controllers/user.controller.test.ts
import UserController from "./user.controller";
import * as UserRepository from "../repositories/user.repository";
describe("UserController", () => {
describe("getUsers", () => {
test("should return empty array", async () => {
const spy = jest
.spyOn(UserRepository, "getUsers")
.mockResolvedValueOnce([]);
const controller = new UserController();
const users = await controller.getUsers();
expect(users).toEqual([]);
expect(spy).toHaveBeenCalledWith();
expect(spy).toHaveBeenCalledTimes(1);
spy.mockRestore();
});
});
});
Here we are creating a spy of UserRepository.getUsers
and changing its implementation to return a promise which will resolve as an empty array. This will help us to recreate the condition where the list is empty in the database and we can easily test what will be the behavior of the method in this condition.
In spying, we are replacing the original method with a mock function and change its implementation to return the value as per our need. This will help developers to recreate and test most of the edge cases which will difficult to do in manual testing. After running the test we are restoring the spy function to the original.
Let's test the condition where the list is not empty. The method is supposed to return the list as it is. We create a dummy list data and change the implementation of the UserRepository.getUsers
method to return the dummy list data. We can verify by just comparing the actual output and expected output.
src/controllers/user.controller.test.ts
import UserController from "./user.controller";
import * as UserRepository from "../repositories/user.repository";
describe("UserController", () => {
describe("getUsers", () => {
test("should return empty array", async () => {
const spy = jest
.spyOn(UserRepository, "getUsers")
.mockResolvedValueOnce([]);
const controller = new UserController();
const users = await controller.getUsers();
expect(users).toEqual([]);
expect(spy).toHaveBeenCalledWith();
expect(spy).toHaveBeenCalledTimes(1);
spy.mockRestore();
});
test("should return user list", async () => {
const usersList = [
{
id: 1,
firstName: "firstName",
lastName: "lastName",
email: "email@example.com",
posts: [],
comments: [],
createdAt: new Date(),
updatedAt: new Date(),
},
];
const spy = jest
.spyOn(UserRepository, "getUsers")
.mockResolvedValueOnce(usersList);
const controller = new UserController();
const users = await controller.getUsers();
expect(users).toEqual(usersList);
expect(spy).toHaveBeenCalledWith();
expect(spy).toHaveBeenCalledTimes(1);
spy.mockRestore();
});
});
});
Let's add a library to create dummy data for the test. We will use faker to generate fake data and structure the fake data as actual data in the test's utilities. We will create a test
folder in the root directory where we keep all the utilities and setup files.
npm i -D faker @types/faker
test/utils/generate.ts
import faker from "faker";
export function generateUserData(overide = {}) {
return {
id: faker.random.number(),
firstName: faker.name.firstName(),
lastName: faker.name.lastName(),
email: faker.internet.email(),
posts: [],
comments: [],
createdAt: new Date(),
updatedAt: new Date(),
...overide,
};
}
export function generateUsersData(n: number = 1, overide = {}) {
return Array.from(
{
length: n,
},
(_, i) => {
return generateUserData({ id: i, ...overide });
}
);
}
The generateUserData
function creates the fake user database object and generateUsersData
will just return the list of it. We can override these fake objects by just passing the extra params.
Let's give the test
folder an absolute path so we can easily import the utilities anywhere in the tests. We also have to exclude the test files from the build files as it is not needed to include the test files in the production server code.
tsconfig.json
{
"compilerOptions": {
...
"baseUrl": "./",
"paths": {
"test/*": ["test/*"]
}
},
"include": ["src/**/*"],
"exclude" : ["src/**/*.test.ts"]
}
jest.config.js
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
moduleNameMapper: {
"test/(.*)": "<rootDir>/test/$1",
},
};
Let's replace the dummy list data with the generateUsers
fake data. and instead of calling the spy.mockRestore
after every test. We will call the jest.resetAllMocks
in afterEach
which will reset all the mock after every test.
src/controllers/user.controller.test.ts
import UserController from "./user.controller";
import * as UserRepository from "../repositories/user.repository";
import { generateUsers } from "test/utils/generate";
afterEach(() => {
jest.resetAllMocks();
});
describe("UserController", () => {
describe("getUsers", () => {
test("should return empty array", async () => {
const spy = jest
.spyOn(UserRepository, "getUsers")
.mockResolvedValueOnce([]);
const controller = new UserController();
const users = await controller.getUsers();
expect(users).toEqual([]);
expect(spy).toHaveBeenCalledWith();
expect(spy).toHaveBeenCalledTimes(1);
});
test("should return user list", async () => {
const usersData = generateUsersData(2);
const spy = jest
.spyOn(UserRepository, "getUsers")
.mockResolvedValueOnce(usersData);
const controller = new UserController();
const users = await controller.getUsers();
expect(users).toEqual(usersData);
expect(spy).toHaveBeenCalledWith();
expect(spy).toHaveBeenCalledTimes(1);
});
});
});
Let's add tests for the UserController's addUser
and getUser
method. For the addUser
method tests we need the utility method to generate the fake data for the test. The tests are simple here as we don't have much logic in the controllers. We just have to mock the repository methods as per the test case and verify the output of the controller methods with the expected result.
We will also verify the number of calls and arguments for the spy functions. This is not needed but it is recommended to have this check.
test/utils/generate.ts
import faker from 'faker'
...
export function generateUserPayload() {
return {
firstName: faker.name.firstName(),
lastName: faker.name.lastName(),
email: faker.internet.email(),
}
}
src/controllers/user.controller.test.ts
import UserController from "./user.controller";
import * as UserRepository from "../repositories/user.repository";
import {
generateUsersData,
generateUserPayload,
generateUserData,
} from "test/utils/generate";
afterEach(() => {
jest.resetAllMocks();
});
describe("UserController", () => {
describe("getUsers", () => {
test("should return empty array", async () => {
const spy = jest
.spyOn(UserRepository, "getUsers")
.mockResolvedValueOnce([]);
const controller = new UserController();
const users = await controller.getUsers();
expect(users).toEqual([]);
expect(spy).toHaveBeenCalledWith();
expect(spy).toHaveBeenCalledTimes(1);
});
test("should return user list", async () => {
const usersData = generateUsersData(2);
const spy = jest
.spyOn(UserRepository, "getUsers")
.mockResolvedValueOnce(usersData);
const controller = new UserController();
const users = await controller.getUsers();
expect(users).toEqual(usersData);
expect(spy).toHaveBeenCalledWith();
expect(spy).toHaveBeenCalledTimes(1);
});
});
describe("addUser", () => {
test("should add user to the database", async () => {
const payload = generateUserPayload();
const userData = generateUserData(payload);
const spy = jest
.spyOn(UserRepository, "createUser")
.mockResolvedValueOnce(userData);
const controller = new UserController();
const user = await controller.createUser(payload);
expect(user).toMatchObject(payload);
expect(user).toEqual(userData);
expect(spy).toHaveBeenCalledWith(payload);
expect(spy).toHaveBeenCalledTimes(1);
});
});
describe("getUser ", () => {
test("should return user from the database", async () => {
const id = 1;
const userData = generateUserData({ id });
const spy = jest
.spyOn(UserRepository, "getUser")
.mockResolvedValueOnce(userData);
const controller = new UserController();
const user = await controller.getUser(id.toString());
expect(user).toEqual(userData);
expect(user?.id).toBe(id);
expect(spy).toHaveBeenCalledWith(id);
expect(spy).toHaveBeenCalledTimes(1);
});
test("should return null if user not found", async () => {
const id = 1;
const spy = jest
.spyOn(UserRepository, "getUser")
.mockResolvedValueOnce(null);
const controller = new UserController();
const user = await controller.getUser(id.toString());
expect(user).toBeNull();
expect(spy).toHaveBeenCalledWith(id);
expect(spy).toHaveBeenCalledTimes(1);
});
});
});
Let's add tests for the Post and Comment controllers. We will follow the same process as we did in the user controller tests. Right now the logic is the same in the controllers. It is easier to just copy-paste the test code and update the values & variables names as per the controller.
First, we need to add the utility functions to generate the fake data for the post-controller tests. we will add the functions the same utils file test/utils/generate.ts
test/utils/generate.ts
import faker from "faker";
import { User } from "../../src/models";
...
export function generatePostData(overide = {}) {
return {
id: faker.random.number(),
title: faker.lorem.sentence(),
content: faker.lorem.paragraph(),
userId: faker.random.number(),
comments: [],
user: new User(),
createdAt: new Date(),
updatedAt: new Date(),
...overide,
};
}
export function generatePostsData(n: number = 1, overide = {}) {
return Array.from(
{
length: n,
},
(_, i) => {
return generatePostData({ id: i, ...overide });
}
);
}
export function generatePostPayload() {
return {
title: faker.lorem.sentence(),
content: faker.lorem.paragraph(),
userId: faker.random.number(),
};
}
We will create the post.controller.test.ts
file next to the post.controller.ts
file. We will group the tests of the methods with the describe
block. We will mock the PostRepository
methods.
src/controllers/post.controller.test.ts
import PostController from "./post.controller";
import * as PostRepository from "../repositories/post.repository";
import {
generatePostsData,
generatePostPayload,
generatePostData,
} from "test/utils/generate";
afterEach(() => {
jest.resetAllMocks();
});
describe("PostController", () => {
describe("getPosts", () => {
test("should return empty array", async () => {
const spy = jest
.spyOn(PostRepository, "getPosts")
.mockResolvedValueOnce([]);
const controller = new PostController();
const posts = await controller.getPosts();
expect(posts).toEqual([]);
expect(spy).toHaveBeenCalledWith();
expect(spy).toHaveBeenCalledTimes(1);
});
test("should return posts list", async () => {
const postsData = generatePostsData(2);
const spy = jest
.spyOn(PostRepository, "getPosts")
.mockResolvedValueOnce(postsData);
const controller = new PostController();
const posts = await controller.getPosts();
expect(posts).toEqual(postsData);
expect(spy).toHaveBeenCalledWith();
expect(spy).toHaveBeenCalledTimes(1);
});
});
describe("createPost", () => {
test("should add post to the database", async () => {
const payload = generatePostPayload();
const postData = generatePostData(payload);
const spy = jest
.spyOn(PostRepository, "createPost")
.mockResolvedValueOnce(postData);
const controller = new PostController();
const post = await controller.createPost(payload);
expect(post).toMatchObject(payload);
expect(post).toEqual(postData);
expect(spy).toHaveBeenCalledWith(payload);
expect(spy).toHaveBeenCalledTimes(1);
});
});
describe("getPost", () => {
test("should return post from the database", async () => {
const id = 1;
const postData = generatePostData({ id });
const spy = jest
.spyOn(PostRepository, "getPost")
.mockResolvedValueOnce(postData);
const controller = new PostController();
const post = await controller.getPost(id.toString());
expect(post).toEqual(postData);
expect(post?.id).toBe(id);
expect(spy).toHaveBeenCalledWith(id);
expect(spy).toHaveBeenCalledTimes(1);
});
test("should return null if post not found", async () => {
const id = 1;
const spy = jest
.spyOn(PostRepository, "getPost")
.mockResolvedValueOnce(null);
const controller = new PostController();
const post = await controller.getPost(id.toString());
expect(post).toBeNull();
expect(spy).toHaveBeenCalledWith(id);
expect(spy).toHaveBeenCalledTimes(1);
});
});
});
We can verify the post controllers tests just by runnings the npm test
command. We will add the utility functions to generate the fake data for the comment controller tests in the same test's utilities file.
test/utils/generate.ts
import faker from 'faker'
import { User, Post } from '../../src/models';
...
export function generateCommentData(overide = {}) {
return {
id: faker.random.number(),
content: faker.lorem.paragraph(),
userId: faker.random.number(),
user: new User(),
postId: faker.random.number(),
post: new Post(),
createdAt: new Date(),
updatedAt: new Date(),
...overide,
};
}
export function generateCommentsData(n: number = 1, overide = {}) {
return Array.from(
{
length: n,
},
(_, i) => {
return generateCommentData(overide);
}
);
}
export function generateCommentPayload() {
return {
content: faker.lorem.paragraph(),
userId: faker.random.number(),
postId: faker.random.number(),
};
}
The logic of the test code for the comment controller will be the same as the user and post controller test code.
src/controllers/comment.controller.test.ts
import CommentController from "./comment.controller";
import * as CommentRepository from "../repositories/comment.repository";
import {
generateCommentsData,
generateCommentPayload,
generateCommentData,
} from "test/utils/generate";
afterEach(() => {
jest.resetAllMocks();
});
describe("CommentController", () => {
describe("getComments", () => {
test("should return empty array", async () => {
const spy = jest
.spyOn(CommentRepository, "getComments")
.mockResolvedValueOnce([]);
const controller = new CommentController();
const comments = await controller.getComments();
expect(comments).toEqual([]);
expect(spy).toHaveBeenCalledWith();
expect(spy).toHaveBeenCalledTimes(1);
});
test("should return comments list", async () => {
const commentsData = generateCommentsData(2);
const spy = jest
.spyOn(CommentRepository, "getComments")
.mockResolvedValueOnce(commentsData);
const controller = new CommentController();
const comments = await controller.getComments();
expect(comments).toEqual(commentsData);
expect(spy).toHaveBeenCalledWith();
expect(spy).toHaveBeenCalledTimes(1);
});
});
describe("createComment", () => {
test("should add comment to the database", async () => {
const payload = generateCommentPayload();
const commentData = generateCommentData(payload);
const spy = jest
.spyOn(CommentRepository, "createComment")
.mockResolvedValueOnce(commentData);
const controller = new CommentController();
const comment = await controller.createComment(payload);
expect(comment).toMatchObject(payload);
expect(comment).toEqual(commentData);
expect(spy).toHaveBeenCalledWith(payload);
expect(spy).toHaveBeenCalledTimes(1);
});
});
describe("getComment", () => {
test("should return comment from the database", async () => {
const id = 1;
const commentData = generateCommentData({ id });
const spy = jest
.spyOn(CommentRepository, "getComment")
.mockResolvedValueOnce(commentData);
const controller = new CommentController();
const comment = await controller.getComment(id.toString());
expect(comment).toEqual(commentData);
expect(comment?.id).toBe(id);
expect(spy).toHaveBeenCalledWith(id);
expect(spy).toHaveBeenCalledTimes(1);
});
test("should return null if comment not found", async () => {
const id = 1;
const spy = jest
.spyOn(CommentRepository, "getComment")
.mockResolvedValueOnce(null);
const controller = new CommentController();
const comment = await controller.getComment(id.toString());
expect(comment).toBeNull();
expect(spy).toHaveBeenCalledWith(id);
expect(spy).toHaveBeenCalledTimes(1);
});
});
});
Coverage
Let's collect coverage for the tests. The coverage tells how much source code is covered by the tests.
Jest support collecting coverage out of the box and doesn't need any other library. We just need to pass the coverage
options to the jest cli.
npm run test --coverage
It is better to enable the coverage in the Jest config file instead of passing the argument via the cli. By default Jest collect the coverage of only those files which are included in the test cases. We can pass the glob pattern of the source code files to override this behavior and collect the coverage from all the files.
jest.config.js
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
moduleNameMapper: {
"test/(.*)": "<rootDir>/test/$1",
},
collectCoverage: true,
collectCoverageFrom: ["src/**/*.{js,ts}"],
};
After running the coverage we can see that the controller files have good coverage but other files are not covered yet. Let's add some more tests to increase the coverage.
More Tests
Let's add tests for the repository files. The repositories have a dependency on a third-party library i.e. "typeorm". The typeorm helps to connect to the database. As in unit tests, it is ideal to not have any external dependencies like a database as well as any third-party libraries. So we will be mocking the typeorm then we can easily test the repository logic without setting up the database.
Let's add tests for the user repository. we will create a user.repository.test.ts
file next to the user.repository.ts
file inside the repositories
directory.
Here we are mocking the whole module, not just a method as we did in the controller's tests. We are mocking the getRepository
method which returns the mocked find
. In the getUsers
tests, we will mock the implementation of the find
method as per test case requirements. In beforeEach
we will clear the mock implementation of the find
method.
src/repositories/user.repository.test.ts
import * as UserRepository from "./user.repository";
import { getRepository } from "typeorm";
import { mocked } from "ts-jest/utils";
import { generateUsersData } from "test/utils/generate";
jest.mock("typeorm", () => {
return {
getRepository: jest.fn().mockReturnValue({
find: jest.fn(),
}),
PrimaryGeneratedColumn: jest.fn(),
Column: jest.fn(),
Entity: jest.fn(),
ManyToOne: jest.fn(),
OneToMany: jest.fn(),
JoinColumn: jest.fn(),
CreateDateColumn: jest.fn(),
UpdateDateColumn: jest.fn(),
};
});
const mockedGetRepo = mocked(getRepository(<jest.Mock>{}));
beforeEach(() => {
mockedGetRepo.find.mockClear();
});
describe("UserRepository", () => {
describe("getUsers", () => {
test("should return empty array", async () => {
mockedGetRepo.find.mockResolvedValue([]);
const users = await UserRepository.getUsers();
expect(users).toEqual([]);
expect(mockedGetRepo.find).toHaveBeenCalledWith();
expect(mockedGetRepo.find).toHaveBeenCalledTimes(1);
});
test("should return user list", async () => {
const usersData = generateUsersData(2);
mockedGetRepo.find.mockResolvedValue(usersData);
const users = await UserRepository.getUsers();
expect(users).toEqual(usersData);
expect(mockedGetRepo.find).toHaveBeenCalledWith();
expect(mockedGetRepo.find).toHaveBeenCalledTimes(1);
});
});
});
For the addUser
method tests we will mock the implementation of the save
method and will clear the mock in beforeEach
.
src/repositories/user.repository.test.ts
import * as UserRepository from './user.repository'
import {getRepository} from 'typeorm'
import { mocked } from 'ts-jest/utils'
import {generateUsersData, generateUserPayload, generateUserData} from 'test/utils/generate'
jest.mock('typeorm', () => {
return {
getRepository: jest.fn().mockReturnValue({
find: jest.fn(),
save: jest.fn()
}),
...
}});
const mockedGetRepo = mocked(getRepository(<jest.Mock>{}))
beforeEach(() => {
mockedGetRepo.find.mockClear()
mockedGetRepo.save.mockClear()
})
describe("UserRepository", () => {
...
describe("addUser", () => {
test("should add user to the database", async () => {
const payload = generateUserPayload()
const userData = generateUserData(payload)
mockedGetRepo.save.mockResolvedValue(userData)
const user = await UserRepository.createUser(payload);
expect(user).toMatchObject(payload)
expect(user).toEqual(userData)
expect(mockedGetRepo.save).toHaveBeenCalledWith(payload)
expect(mockedGetRepo.save).toHaveBeenCalledTimes(1)
})
})
})
We will mock the implementation of the findOne
method for the getUser
tests and clear its implementation in beforeEach
as we did with save
and find
methods.
src/repositories/user.repository.test.ts
import * as UserRepository from './user.repository'
import {getRepository} from 'typeorm'
import { mocked } from 'ts-jest/utils'
import {generateUsersData, generateUserPayload, generateUserData} from 'test/utils/generate'
jest.mock('typeorm', () => {
return {
getRepository: jest.fn().mockReturnValue({
find: jest.fn(),
save: jest.fn(),
findOne: jest.fn()
}),
...
}});
const mockedGetRepo = mocked(getRepository(<jest.Mock>{}))
beforeEach(() => {
mockedGetRepo.find.mockClear()
mockedGetRepo.findOne.mockClear()
mockedGetRepo.save.mockClear()
})
describe("UserRepository", () => {
...
describe("getUser", () => {
test("should return user from the database", async () => {
const id = 1
const userData = generateUserData({id})
mockedGetRepo.findOne.mockResolvedValue(userData)
const user = await UserRepository.getUser(id)
expect(user).toEqual(userData)
expect(user?.id).toBe(id)
expect(mockedGetRepo.findOne).toHaveBeenCalledWith({id})
expect(mockedGetRepo.findOne).toHaveBeenCalledTimes(1)
})
test("should return null if user not found", async () => {
const id = 1
mockedGetRepo.findOne.mockResolvedValue(null)
const user = await UserRepository.getUser(id)
expect(user).toBeNull()
expect(mockedGetRepo.findOne).toHaveBeenCalledWith({id})
expect(mockedGetRepo.findOne).toHaveBeenCalledTimes(1)
})
})
})
Let's move the mock implementation of the typeorm module from the user.repository.test.ts
file to a separate file. Then we can easily import and use it in other repository tests.
We will create a __mocks__
folder in the root directory and add create a typeorm.ts
file in it. We will move the typeorm mock implementation to this file. The __mocks__
is a special folder and Jest will pick the mock from here. You can read more about it here.
__mocks__/typeorm.ts
module.exports = {
getRepository: jest.fn().mockReturnValue({
find: jest.fn(),
save: jest.fn(),
findOne: jest.fn(),
}),
PrimaryGeneratedColumn: jest.fn(),
Column: jest.fn(),
Entity: jest.fn(),
ManyToOne: jest.fn(),
OneToMany: jest.fn(),
JoinColumn: jest.fn(),
CreateDateColumn: jest.fn(),
UpdateDateColumn: jest.fn(),
};
In the user.repository.test.ts
file, we just need to call jest.mock("typeorm")
and jest will automatically pick the implementation from the __mocks__
directory.
src/repositories/user.repository.test.ts
import * as UserRepository from "./user.repository";
import { getRepository } from "typeorm";
import { mocked } from "ts-jest/utils";
import {
generateUsersData,
generateUserPayload,
generateUserData,
} from "test/utils/generate";
jest.mock("typeorm");
We will follow the same process and write the tests for the post.repository.test.ts
and comment.repository.test.ts
same as the user repository.
CI Setup
Let's setup Github Actions to run tests. We will set up it to run tests on push
and pull_request
events. We will run the npm ci
command to install dependencies and the npm test
command to run the tests. We will set up GitHub Actions to run the tests on multiple node versions.
.github/workflows/tests.yml
name: Node.js CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [10.x, 12.x, 14.x, 15.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm test
All the source code for this tutorial is available on GitHub.