Jest, Sinon, Supertest unit test with Express.js server
2022-07-31 00:00:23
# In this article I'll explain what's unit test and how to implement it on your express.js server. --- ## What is unit test? Unit tests are typically automated tests written and run by software developers to check if the behaviour of an individual function or method is as expected. ## A unit Test Example with Jest #### What is JEST? A quote from [JEST documentation](https://jestjs.io/) >Jest is a delightful JavaScript Testing Framework with a focus on simplicity. It works with projects using: Babel, TypeScript, Node, React, Angular, Vue and more! #### How do I use it? Let’s look at a quick example from their tutorial. First of all, you need to install jest. ``` npm install --save-dev jest ``` Create a `sum.js` file and write the function you wanna test. In this example, we create a sum function to calculate the sum of the two arguments. ```js function sum(a, b) { return a + b; } module.exports = sum; ``` Then, create a file named `sum.test.js`. This will contain our actual test code. (Note: Jest looks for files ending in `.spec.js` or `.test.js`, two popular community standards.) ```js const sum = require('./sum'); test('adds 1 + 2 to equal 3', () => { expect(sum(1, 2)).toBe(3); }); ``` Add the following scripts to your package.json: ``` { "scripts": { "test": "jest" } } ``` Finally, run `yarn test` or `npm test` and Jest will print this message ``` PASS ./sum.test.js ✓ adds 1 + 2 to equal 3 (5ms) ``` **Congratulation 🎉 You just successfully wrote your first test using Jest!** *** ### Before we start… In order to build unit tests for express.js backend controllers, we need other libraries’ help with that. 1. SuperTest [SuperTest](https://github.com/visionmedia/supertest) is a [Node.js](https://nodejs.dev/) library that helps developers test APIs. It extends another library called superagent, a JavaScript HTTP client for Node.js and the browser. Developers can use SuperTest as a standalone library or with JavaScript testing frameworks like Mocha or Jest. usage: ``` npm install supertest --save-dev ``` 2. Sinon.js Sinon is a standalone test spies, stubs and mocks for JavaScript. usage: ``` npm install sinon --save-dev ``` Before we write our code, we need to understand the concept of test doubles like `spy`, `stub` and `mock`. ***Test Spy:*** A test spy is a function that records arguments, return values, the value of this and exceptions thrown (if any) for all its [calls](https://sinonjs.org/releases/v13/spy-call/). Through all the spy methods sinon offers, we can spy on a function call and expect things like: * Has the function been called? * If yes, how many times? * What arguments? * Did it return something? …etc ***Test Stub:*** Stub substitutes the real function and returns a fake version of that function with pre-determined behavior, but the stub can only return the fixed response it was programmed to return. First of all, create a variable replaces object.method with a stub function. ```js const stub = sinon.stub(object, "method"); ``` With the stub methods sinon offers, we can override the function with assumption. ```js stub.returns(obj); //obj is the stuff you tell the stub to return ``` Use `resolves` if it returns a promise. ```js stub.resolves(value); ``` Basically, when you run the test and the test hit a function you stubbed, it’s not gonna call the actual function, it will just give you the value you programmed.(It’s useful for functions like querying database or API call) ***Test Mock:*** Mocks (and mock expectations) are fake methods (like spies) with **pre-programmed behavior** (like stubs) as well as pre-programmed expectations. First, create a mock for the provided object. ```js conts mock = sinon.mock(obj); ``` This does not change the object, but returns a mock object to set expectations on the object’s methods. And create an `expectation` variable so you can define your expectation with the object method. ```js const expectation = mock.expects("method"); ``` After that, you can use the methods sinon offers to define your expectation on the object method you just mocked. The example below is essentially expecting the mocked method only been called once. ```js expectation.exactly(1); ``` After you define all your expectations on the mocked method, use the code below to verify your expectations (if you have more than one). ```js expectation.verify(); ``` ***When to use which test double?*** Here is some quick guide to practice, but in the end of the day it’s your decision. ***Spy***: Verify something happened and use assertions to check many different results. ***Stub***: Something external — a network connection, a database, or some other non-JavaScript system. ***Mock***: Mocks should be used primarily when you would use a stub, but need to verify multiple more specific behaviors on it. *** ### Implementing on your server’s controller This is `/auth` route’s signup controller. ```js const signupLocal = async (req, res) => { //validating data const { error } = signupValidation(req.body); if (error) return res.status(400).send(error.details[0].message); const existEmail = await User.findOne({ email: req.body.email }); if (existEmail) return res.status(400).send("This email has already been registered!"); const auth = "someKey"; const url = "someURL"; let response = await fetch(url, { method: "GET", headers: { Accept: "application/json", Authorization: auth, }, }); let data = await response.json(); let index = Math.floor(Math.random() * 30); const fetchedBg = data.photos[index].src.large2x; const newUser = new User({ name: req.body.name, email: req.body.email, password: req.body.password, date: DateTime.utc(), background: fetchedBg, }); try { const savedUser = await newUser.save(); res.status(200).send("Sign up successful, you can login now!"); } catch (err) { res.status(500).send("User not saved."); } }; ``` Let’s take a look at a test suite for `/auth` route’s signup controller’s behaviors. ```js describe("signup", () => { let sandbox = sinon.createSandbox(); let findOneStub; let user; let newUserSaveStub; beforeEach(() => { // stub replace the function and make it return what we want. findOneStub = sandbox.stub(User, "findOne"); user = new User({ name: "asdasd", email: "333@333.com", password: "1234567", }); newUserSaveStub = sandbox.stub(User.prototype, "save"); }); afterEach(() => { // !important to call this in afterEach. sandbox.restore(); }); test("should signup w/ new email", async () => { // spy just spy on the function without replacing the function itself. findOneStub.resolves(null); newUserSaveStub.resolves(user); //should resolves stub before request. const result = await request(app) .post("/api/auth/signup") .send({ name: "333", email: "333@333.com", password: "1234567", }) .expect(200); expect(result.text).toBe("Sign up successful, you can login now!"); }); test("should not signup already existed email", async () => { findOneStub.resolves(user); const result = await request(app) .post("/api/auth/signup") .send({ name: "333", email: "333@333.com", password: "1234567", }) .expect(400); expect(result.text).toBe("This email has already been registered!"); expect(newUserSaveStub.notCalled).toBe(true); }); test("should not save user.", async () => { findOneStub.resolves(null); newUserSaveStub.throws(); const result = await request(app) .post("/api/auth/signup") .send({ name: "333", email: "333@333.com", password: "1234567", }) .expect(500); expect(result.text).toBe("User not saved."); }); }); ``` 1. The function `describe` is a Jest function, describe the thing you are testing (it’s also the title of the test suite, the test suite of related test cases). And the second parameter is a callback function, and I created my test cases inside it. 2. Before I started to write test cases, I defined my variables (sandbox, stub function and fake objects). And gave them value before each test, and restore the sandbox after each test. 3. Inside the test suite, I use `test` method to write my test case. It takes the title(a string) as the first parameter, and write the test function as second parameter. 4. Take the first test case as example, it’s supposed to test the controller to make sure the user can signup successfully. In order to test that, I made the `findOneStub` function resolve `null`, so that the test will assume the email isn’t already registered. 5. After that, I use the request method from `supertest`, sent a request and return a result, and use expect along with the [matchers](https://jestjs.io/docs/expect) provided by Jest to validate different things. FYI: This article is just a simple example of how I do unit test in my code, for more information about the tools, you should go check out the documentations.