Firebase Functions with Tests
Serverless is just similar to hosting an application and deploy it somewhere with endpoints, since serverless gives us freedom to deploy functions / endpoints one by one, the release circle for each endpoint is comparatively shorter.
A stable serverless could be exiting in production for more than a year and still been used actively. In that sense, a testable and maintainable codebase is also important to serverless! The test cases written in code will help knowledge transfer, hence testing / TDD became important to serverless. In this article, let's dive into how to write unit test for firebase functions (serverless provide by firebase)
This article is a continue part for a previous article Firebase Functions, we will work in ./functions
directory for the test
Please check previous article for existing codebase and we are going to implement unit test for the code we wrote before.
# Set up
to begin with, we need install testing dependencies:
$ yarn add @types/jest firebase-functions-test jest ts-jest tslint typescript -D
We also need initialize some files for jest
:
In my jest.config.js
I have:
module.exports = {
setupFilesAfterEnv: ['./jest.setup.js'],
transform: {
'^.+\\.ts?$': 'ts-jest',
},
moduleDirectories: ['node_modules', 'src'],
moduleNameMapper: {
'tests/(.*)': '<rootDir>/__tests__/$1',
},
testPathIgnorePatterns: ['<rootDir>/lib'],
};
2
3
4
5
6
7
8
9
10
11
In my jest.setup.js
I have:
jest.setTimeout(10000); // in milliseconds
In my tsconfig.json
, need add, "esModuleInterop": true,
:
{
"compilerOptions": {
"module": "commonjs",
"noImplicitReturns": true,
"noUnusedLocals": true,
"esModuleInterop": true,
"outDir": "lib",
"sourceMap": true,
"strict": true,
"target": "es2017"
},
"compileOnSave": true,
"include": ["src"]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
Don't forgot having a script command in your package.json
with "test": "jest"
:
{
"name": "functions",
"scripts": {
"lint": "tslint --project tsconfig.json",
"build": "tsc",
"serve": "npm run build && firebase serve --only functions",
"shell": "npm run build && firebase functions:shell",
"start": "npm run shell",
"test": "jest",
"deploy": "firebase deploy --only functions",
"logs": "firebase functions:log"
},
"engines": {
"node": "8"
},
"main": "lib/index.js",
"dependencies": {
"firebase-admin": "^8.6.0",
"firebase-functions": "^3.3.0"
},
"devDependencies": {
"@types/jest": "^25.1.4",
"firebase-functions-test": "^0.2.0",
"jest": "^25.2.1",
"ts-jest": "^25.2.1",
"tslint": "^6.1.0",
"typescript": "^3.8.3"
},
"private": true
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
After this, once you have test files:
$ mkdir tests
$ touch tests/auth.offline.test.ts
2
for example in tests/auth.offline.test.ts
will be:
test('sample test!', () => {
expect(1).toBe(1);
});
2
3
the test can be run as yarn test
:
$ yarn test
yarn run v1.22.1
$ jest
PASS tests/auth.offline.test.ts
✓ Sample test! (10ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 5.305s
Ran all test suites.
✨ Done in 6.72s.
2
3
4
5
6
7
8
9
10
11
12
# Tests!
# Create test for auth (on user create)
in tests/auth.offline.test.ts
, we have:
import { addDefaultUserRole } from '../src/index';
import test from 'firebase-functions-test';
import admin from 'firebase-admin';
const testEnv = test();
/**
* mock setup
*/
var setCustomUserClaimsMock = jest.fn();
var docSetMock = jest.fn();
jest.mock('firebase-admin', () => ({
initializeApp: jest.fn(),
firestore: () => ({
doc: jest.fn((path) => ({
set: docSetMock.mockReturnValue(true),
})),
}),
auth: () => ({
setCustomUserClaims: setCustomUserClaimsMock.mockReturnValue(
Promise.resolve()
),
}),
}));
describe('#addDefaultUserRole', () => {
const wrapped = testEnv.wrap(addDefaultUserRole);
it('should give role with admin and update firestore', async () => {
const testUser = {
uid: 'id-1',
email: 'marvin5064@gmail.com',
displayName: 'marvin',
photoURL: 'handsome-face.jpg',
};
await wrapped(testUser);
expect(setCustomUserClaimsMock).toBeCalledWith('id-1', {
isAdmin: true,
role: 'admin',
});
expect(admin.firestore().doc('user/id-1').set).toBeCalledWith({
isAdmin: true,
});
});
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# Create test for firestore document(on update)
in tests/firestore.offline.test.ts
, we have:
import { userUpdated } from '../src/index';
import test from 'firebase-functions-test';
const testEnv = test();
/**
* mock setup
*/
var setCustomUserClaimsMock = jest.fn();
jest.mock('firebase-admin', () => ({
initializeApp: jest.fn(),
auth: () => ({
setCustomUserClaims: setCustomUserClaimsMock.mockReturnValue(
Promise.resolve()
),
}),
}));
describe('#userUpdated', () => {
const wrapped = testEnv.wrap(userUpdated);
it('should update role to non-admin and update claims', async () => {
const handler = {
after: {
data() {
return {
isAdmin: false,
};
},
},
};
const context = {
params: { uid: 'id-1' },
};
await wrapped(handler, context);
expect(setCustomUserClaimsMock).toBeCalledWith('id-1', {
isAdmin: false,
role: 'non-admin',
});
});
it('should update role to admin and update claims', async () => {
const handler = {
after: {
data() {
return {
isAdmin: true,
};
},
},
};
const context = {
params: { uid: 'id-1' },
};
await wrapped(handler, context);
expect(setCustomUserClaimsMock).toBeCalledWith('id-1', {
isAdmin: true,
role: 'admin',
});
});
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# Test endpoint (on request)
in the tests/request.offline.test.ts
:
import { helloWorld } from '../src/index';
/**
* mock setup
*/
jest.mock('firebase-admin', () => ({
initializeApp: jest.fn(),
}));
var responseSendMock = jest.fn();
var responseMock = {
send: responseSendMock.mockReturnValue(1),
};
describe('#helloWorld', () => {
it('should update role to non-admin and update claims', async () => {
const req = {} as any;
await helloWorld(req, responseMock as any);
expect(responseSendMock).toBeCalled;
expect(responseSendMock).toBeCalledTimes(1);
expect(responseSendMock).toBeCalledWith(
'My first try on firebase for on request!'
);
});
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Over all the file structure will be (excluding node_modules
and lib
):
$ tree
.
├── jest.config.js
├── jest.setup.js
├── package-lock.json
├── package.json
├── src
│ ├── auth.ts
│ ├── firestore.ts
│ ├── index.ts
│ ├── request.ts
│ └── types.ts
├── tests
│ ├── auth.offline.test.ts
│ ├── firestore.offline.test.ts
│ └── request.offline.test.ts
├── tsconfig.json
├── tslint.json
└── yarn.lock
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
This chapter only covers firebase offline unit test, which only do mocking and test, it is not really testing against firebase ecosystem, e.g.
- When a firestore data changed, is the claims structure really changed? This is not been covered!
- Offline unit test is not covering the function itself, onUpdate of document or onUserCreate during authentication, they were kind of getting tested upon wrapped function
worth a time to take look at official document (opens new window) for the first part (we covered second part), which is teaching how to setup a firebase instance for testing purpose and having some unit/integration test against a real firebase