An essential part of developing a fully functional software is testing. In this article, we focus on making our application more testable and implementing unit and integration tests using Jest and a library called SuperTest. This part of the TypeScript Express testing tutorial needs some basic understanding of tests and the knowledge of tools like Jest and I encourage you to check out the first part of the JavaScript tutorial testing.

As always, to code for this tutorial is in the express-typescript repository. Since this tutorial uses Postgres, the code for it is in the postgres branch.

Testing Express with unit tests

At the beginning of the first part of the JavaScript testing tutorial, we mention unit testing. In our current application architecture, it might be challenging to separate distinct units to tests, and because of that, the first concept to get to know is a service. It can implement complex logic so that the route handlers can use them instead of doing the heavy lifting. An additional advantage to them, so significant in this article, is that we can call them anytime, anywhere, and they can be used not only when a request is made, but we can also easily use them from within our Express tests.

Let’s expand on an example of registering users that we implemented before:

The only thing that is left to do is to create a Jest configuration file. The ts-jest library can do it for us:

1

npx ts-jest config:init

Once we got that down, we can begin testing Express! Let’s start with something simple:

src/authentication/tests/authentication.service.test.ts

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

import TokenData from'../interfaces/tokenData.interface';

import AuthenticationService from'./authentication.service';

describe('The AuthenticationService',()=>{

constauthenticationService=newAuthenticationService();

describe('when creating a cookie',()=>{

const tokenData:TokenData={

token:'',

expiresIn:1,

};

it('should return a string',()=>{

expect(typeofauthenticationService.createCookie(tokenData))

.toEqual('string');

})

});

})

You can execute it with npm run test

ConnectionNotFoundError: Connection “default” was not found.

And we have an error! The reason of it is the fact that in the service we access a TypeORM repository, but we don’t establish any connection. We need to fake the TypeORM functionalities so that our unit tests do not attempt to connect to a real database.

src/authentication/tests/authentication.service.test.ts

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

import*astypeorm from'typeorm';

import TokenData from'../interfaces/tokenData.interface';

import AuthenticationService from'./authentication.service';

(typeorm asany).getRepository=jest.fn();

describe('The AuthenticationService',()=>{

describe('when creating a cookie',()=>{

it('should return a string',()=>{

const tokenData:TokenData={

token:'',

expiresIn:1,

};

(typeorm asany).getRepository.mockReturnValue({});

constauthenticationService=newAuthenticationService();

expect(typeofauthenticationService.createCookie(tokenData))

.toEqual('string');

});

});

});

With the usage of the mockReturnValue function, we can mock our repository mock per test. It does mean that we can change it in every test. For this simple test, we don’t need anything more than just an empty object. We need to overwrite some of the TypeScript typings using any because by default the library functions are read-only.

The first thing we check here is if the register function throws an error if the user with that email is found. This situation happens if the findOne function returns something else than undefined. We make it return the user data, and that causes the service to throw the UserWithThatEmailAlreadyExistsException.

Since we are at it, we can test the opposite situation: if the service doesn’t find the user, an error should not be thrown:

src/authentication/tests/authentication.service.test.ts

1

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

import*astypeorm from'typeorm';

import CreateUserDto from'../user/user.dto';

import AuthenticationService from'./authentication.service';

(typeorm asany).getRepository=jest.fn();

describe('The AuthenticationService',()=>{

describe('when registering a user',()=>{

describe('if the email is not taken',()=>{

it('should not throw an error',async()=>{

const userData:CreateUserDto={

fullName:'John Smith',

email:'john@smith.com',

password:'strongPassword123',

};

process.env.JWT_SECRET='jwt_secret';

(typeorm asany).getRepository.mockReturnValue({

findOne:()=>Promise.resolve(undefined),

create:()=>({

...userData,

id:0,

}),

save:()=>Promise.resolve(),

});

constauthenticationService=newAuthenticationService();

await expect(authenticationService.register(userData))

.resolves.toBeDefined();

});

});

});

});

As you can see above, we need to mock more functions of the TypeORM repository. This is due to the fact that if the registration is successful, the User object is created and saved into the database.

As you can see, thanks to not connecting to any database, our tests are quite fast and don’t depend on external factors. Thanks to that, they are more reliable.

Integration tests with SuperTest

Testing just the services might prove not to be enough at some point. With the SuperTest library, we can make requests to our application and test its behavior. Let’s install it!

1

npm install-Dsupertest@types/supertest

One of the tasks that our controller does is attaching the Set-Cookie header with the token to the response. We can make a request to test that.

src/authentication/tests/authentication.service.test.ts

1

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

import*astypeorm from'typeorm';

import App from"../../app";

import AuthenticationController from"../authentication.controller";

import CreateUserDto from"../../user/user.dto";

import*asrequest from'supertest';

(typeorm asany).getRepository=jest.fn();

describe('The AuthenticationController',()=>{

describe('POST /auth/register',()=>{

describe('if the email is not taken',()=>{

it('response should have the Set-Cookie header with the Authorization token',()=>{

const userData:CreateUserDto={

fullName:'John Smith',

email:'john@smith.com',

password:'strongPassword123',

};

process.env.JWT_SECRET='jwt_secret';

(typeorm asany).getRepository.mockReturnValue({

findOne:()=>Promise.resolve(undefined),

create:()=>({

...userData,

id:0,

}),

save:()=>Promise.resolve(),

});

constauthenticationController=newAuthenticationController();

constapp=newApp([

authenticationController,

]);

returnrequest(app.getServer())

.post(`${authenticationController.path}/register`)

.send(userData)

.expect('Set-Cookie',/^Authorization=.+/)

})

})

})

});

In the example above we expect the Set-Cookie header to begin with the token specified using a regular expression. The ^ character means that it should be the very beginning of a string and the .+ means that there should be one or more characters afterward.

If you want to know more about regular expressions in JavaScript, check out my RegExp course

Thanks to mocking the connection to the database, our test passes!

1

2

3

4

5

PASS src/authentication/tests/authentication.controller.test.ts

The AuthenticationController

POST/auth/register

ifthe email isnottaken

✓response should have the set-cookie header with the Authorization token(208ms)

As you can see such tests, even without the database connection, are noticeably slower. This because we need to initialize a bigger part of our application, make it working and perform a request. Since we don’t use a real database connection, we don’t perform end-to-end Express testing here.

With the SuperTest library, you can test all types of requests as there are many possibilities. You can find more examples and the API specification in the documentation.

Summary

In this article, we covered writing unit and integration tests for our Express application with Jest and SuperTest. To avoid connecting to a real database, we mocked the functionalities of the TypeORM. If you are using something different, for example, Mongoose and MongoDB, you can surely do that as well. For the sake of making testing Express easier, we separated a part of our logic into a service. Hopefully, with implementing all of the above, your application can become more reliable making your job easier!