Overview
As the backend requirements grows with 0% test coverage, the harder it is to maintain the project or even scale due to bugs coming out from anywhere. Debugging without tests also takes so much time to have to manually open the app, login using the credentials that you remember for a particular user role, and finally following the necessary steps to replicate the bug only to read off the error message or find out if it's working. It's a waste of time writing functionalities without tests, you didn't actually "save" time by not writing tests.
Introduction
We're only going to cover how to setup Jest with TypeScript and a basic unit test and fairly simple mocking to a class that we want to mock a response value of a certain method.
Setup Jest TypeScript
Setting up Jest on Cloud Functions is fairly simple and straight-forward. Just run the following command and you're ready to go!
npm i jest @types/jest ts-jest typescript -D
The following are the dependencies that our Cloud Functions directory in functions
directory requires. We'll break it down below of what they are and what's their purpose.
Dependencies:
-
jest
a testing framework maintained by Facebook, works well with frontend frameworks like Vue, React, Angular also with TypeScript. We'll be using Jest for writing up all of our unit tests, integration tests, and other types of tests that you know of. -
@types/jest
all the TypeScript typings are found here, we're getting the auto complete sugar that we developers love -
ts-jest
there are some utility functions that with help us mock a few functions or methods from a third party library -
typescript
our favorite tool
Once all the dependencies are installed, then all is cool! Next up is we create a file called jest.config.js
at the root of our functions
directory and we'll have the following lines of code in the newly created file.
module.exports = {
"roots": [
"<rootDir>/src"
],
"testMatch": [
"**/__tests__/**/*.+(ts|tsx|js)",
"**/?(*.)+(spec|test).+(ts|tsx|js)"
],
"transform": {
"^.+\\.(ts|tsx)$": "ts-jest"
},
}
Now we'll add a script in our package.json
file on key scripts
// package.json
{
"scripts": {
// ... other scripts
"test": "jest"
}
}
Now by the following setup that we have, we will have all of our tests suites housed inside src
directory. Here are the following examples on how the structure would look.
functions
- src
- todo
- todo.service.ts
- todo.spec.ts // or you can name it as "todo.test.ts"
- todo-b
- todo-b.service.ts
- todo-b.spec.ts // or you can name it as "todo-b.test.ts"
And when we want to execute or run our tests cases, we'll use the following command
jest # or you can run with "npm run test" or "npm test"
You can read more for the full documentation from here
Writing our First Unit Test
Suppose that TodoService
(relative path todo/todo.service.ts
) is the class we want to test.
// todo.service.ts
export class TodoService {
public giveMeANumber(number: number): number {
return number;
}
}
Then we will proceed to create a test/spec file in the same directory as the service so we can easily find its corresponding test file.
// todo.service.spec.ts
import { TodoService } from './todo.service.ts'
describe('TodoService', () => {
let service: TodoService
beforeEach(() => {
service = new TodoService()
})
describe('giveMeANumber', () => {
test('when value is true, then should return true', () => {
expect(true).toBe(true)
})
it('should return 5, when given parameter is 5', () => {
const number = service.giveMeANumber(5)
expect(number).toBe(5)
})
})
})
Notice how we did not import describe
, test
, beforeEach
, it
, and expect
functions because the setup that we have for Jest already handled it for us, the functions are available on all *.spec.ts
or *.test.ts
files. And lastly it's the @types/jest
that will give us the convenience of auto-completes so we don't have to worry if we're passing down the correct arguments into those functions.
The describe
function describes the test suite or it groups a set of test cases in it. It is very similar to group
when we are writing tests in Flutter. And the test
function is the actual test case.
And now to run our tests we will use the following command in the terminal. Suppose that you are in the root directory of the project, in this case it's ./functions
jest # `npm test` or `npm run test`
Note: It will take about under 10s to run the test, not sure why it's this slow.
Then that should display the test results in your terminal.
When you want to run specific tests, you can use jest -t 'when value is true, then should return true'
make sure it matches that test case you want to run.
Writing our First Test with Mocking
And now we are going to make mocks to any third party libraries that we depend on so we don't have to make the test await for any HTTP calls.
Say TodoService
will have a dependency for AwesomeTodoAPI
where we want to make a call only to retrieve outstanding todos. And we just want to assert the array length of the response data just to make an assertion example as simple as possible. The only thing we want to learn now is how we can create mocks in Jest with TypeScript.
Now let's have the same service class but is dependent to a library AwesomeTodoAPI
where this dependency will handle the HTTP calls internally, so we're only calling methods from it that will give us the resources we require.
// todo.service.ts
import AwesomeTodoAPI from '@awesome/AwesomeTodoAPI'
type AwesomeTodo = {
title: string
description: string
}
export class TodoService {
private awesomeTodoApi: AwesomeTodoAPI
constructor() {
this.awesomeTodoApi = new AwesomeTodoAPI()
}
public async fetchOutstandingTodos(): Promise<AwesomeTodo[]> {
const response = await this.awesomeTodoApi.fetchOutstandingTodos()
return response.data
}
public giveMeANumber(number: number): number {
return number;
}
}
The method fetchOutstandingTodos
inside our TodoService
is dependent on the returned response from AwesomeTodoAPI
method fetchOutstandingTodos
. We can then proceed to creating a mock of the returned response for that. Without mocking, it will still make the actual call for fetchOutstandingTodos
but that'll take some time before the data gets returned and will result into making our tests running slow, and that's not what we want in this case since we'll only want to unit test our code and not make an integration test.
On the same spec/test file that we have,
// todo.service.spec.ts
import { mocked } from "ts-jest/utils";
import { TodoService } from './todo.service.ts'
import { AwesomeTodoAPI } from '@awesome/AwesomeTodoAPI'
// Let Jest automatically create all the mocks for us
jest.mock('@awesome/AwesomeTodoAPI')
describe('TodoService', () => {
let service: TodoService
const mockedAwesomeTodoApi = mocked(AwesomeTodoAPI, true)
beforeEach(() => {
service = new TodoService()
// Clear out all the mocks that we
// created before each test case
mockedAwesomeTodoApi.mockClear()
})
describe('fetchOutstandingTodos', () => {
test('when called, should return all the outstanding todos', async () => {
// Arrange: Mock the returned response value
AwesomeTodoAPI.prototype.fetchOutstandingTodos = jest.fn()
.mockImplementationOnce(() => {
return {
data: [
{ title: 'Todo A', description: 'lorem', },
{ title: 'Todo B', description: 'lorem', },
] as AwesomeTodo[]
} as AwesomeTodoAPIResponse
})
// Action
const todos = await service.fetchOutstandingTodos()
// Assert
expect(todos.length > 1).toBeTruthy()
})
test('when there are no outstanding todos, should return empty array', async () => {
// Arrange: Mock the returned response value
AwesomeTodoAPI.prototype.fetchOutstandingTodos = jest.fn()
.mockImplementationOnce(() => {
return {
data: [] as AwesomeTodo[]
} as AwesomeTodoAPIResponse
})
// Action
const todos = await service.fetchOutstandingTodos()
// Assert
expect(todos.length === 0).toBeTruthy()
})
})
})
Now to break it down for you on what's happening:
- We imported
AwesomeTodoAPI
from'@awesome/AwesomeTodoAPI'
and automatically created a mock for all of its method by callingjest.mock(importPath, factoryCallback)
(You can learn more about thefactoryCallback
I am referring to from the official Jest docs if you wish to, basically you just have to match all method names and its return values, and have the callback return an object. It's obviously a boilerplate code but maybe there can be a potential use case for manually setting up mocks) - Next is we create a variable called
mockedAwesomeTodoApi
where this is the actual mock and thanks to the utility functionmocked
fromts-jest
where we pass down in the first argument of the object we want to mock and its second argument is if we want to mock it deep so we settrue
- Inside
beforEach
is we setup our serviceTodoService
and clear out all the mocks that was created previously from each test case, so before each test case will be a fresh mock by callingmockClear
from the mocked object - Now inside our test suite for method
fetchOutstandingTodos
on the "Arrange" section is we setup the return response value of theAwesomeTodoAPI
for its methodfetchOutstandingTodos
since we don't want to make an actual call to the API. The way we mock the returned response is by accessing the class prototype itself, and accessfetchOutstandingTodos
and assign a new value to it by what is returned in the callback function formockImplementationOnce
- Then we make the actual call from our
TodoService
that callsfetchOutstandingTodos
fromAweomseTodoAPI
and pass down the returned value to thetodos
variable - Finally we assert if
todos
contains some data or if it's empty.
Wrapping Up
We learned how to setup Jest with TypeScript in just a few minutes. Now we can get our tests up and running in no time!
Thanks for taking the time to read. Good day!
Top comments (0)