Unit-Testing REST API File Upload with Jest, Supertest and MZ in Node

Linuk
Linuk
Aug 28, 2017 · 5 min read

As mentioned in my last article about the 5 useful syntax for developing React app, I am currently building a CMS for my company, where React is for client and Node is for server side.

I follow the TDD which makes me feel more relaxed and safe whenever making a new commit, it reduced the possibility of finding some bugs come out from nowhere after another update lol.

Some of the REST API endpoints are to upload, rename and delete the documentation such as user guide, brochure or other files for the products in my company, and this post will simply describe how you can write unit-test for it with Jest, supertest and MZ.


My purpose is to test endpoints for uploading, renaming and deleting the documentation, and here are the endpoints:

  • POST /api/v1/documentations/upload
  • PUT /api/v1/documentations/rename
    (with parameters {filePath: path/To/File, newName:newFileName} )
  • DELETE /api/v1/documentations/delete/path

In the tests, we will upload the test file to the CDN host, rename it, and delete it in the end.

And the dependencies I use for testing:

  • JEST: Needless to say it’s one of the best JS test tool.
  • Supertest: For testing HTTP.
  • MZ: Use fs with a promise way.

To install them, just run

// yarn
yarn add jest supertest mz
// or npm
npm run --save install jest superset mz

Testing Uploading

To test uploading a real file to CDN, I use supertest to make the request, the syntax look like this:

request(app)
.expect(200)
.then((res) => {
// test assertions here
})

And we will wrap it in the it and describe function, which should look like this :
( But of course it depends on your test situation )

describe('POST /api/v1/endpoint - upload a new file', () => {  const filePath = `${__dirname}/testFiles/test.pdf`;

it('should do something', () => {
request(app)
.expect(200)
.then((res) => {
// test assertions here
})
})
})

To start, we include the modules and the app first.

const request = require('supertest');
const fs = require('mz/fs');
const app = require('../app');

Then start to write the test itself, I put the testing file which named test.pdf in the subdirectory testFiles , and in the test we will upload it to the real CDN host.

The flow is:

  1. Create a null file testFilePath to storing the filePath in the CDN which we will use later.
  2. Use MZ to test if the test file exists.
  3. If it exists, use supertest to make a request to the POST endpoint.
  4. Attach file the to request.
  5. Test the response contains correct response, the response contain success contains the success status, message contains the message for rendering the client, filePath which contain the new file path
  6. One of the properties of the response contains the file path in the CDN, store it in a variable, later we will use it for renaming test.
let testFilePath = null;describe('POST /api/v1/documentations/upload - upload a new documentation file', () => {  const filePath = `${__dirname}/testFiles/test.pdf`;

// Upload first test file to CDN
it('should upload the test file to CDN', () =>
// Test if the test file is exist
fs.exists(filePath)
.then((exists) => {
if (!exists) throw new Error('file does not exist');
return request(app)
.post('/api/v1/documentations/upload')
// Attach the file with key 'file' which is corresponding to your endpoint setting.
.attach('file', filePath)
.then((res) => {
const { success, message, filePath } = res.body;
expect(success).toBeTruthy();
expect(message).toBe('Uploaded successfully');
expect(typeof filePath).toBeTruthy();
// store file data for following tests
testFilePath = filePath;
})
.catch(err => console.log(err));
})
})

It is worth noting that we return the request, because the test functions like expect are asynchronous, and we need to return them to make sure the test won’t end before the HTTP request getting the response.

To read more about TESTING ASYNCHRONOUS CODE.


Testing Renaming

Supertest provides a simple way to add parameters to the request, for example, if you need to make a GET request with query string like /api/v1/image?colour=blue&country=Taiwan , just simple add query object in the request:

const qb = { colour: 'blue', country: 'Taiwan' }
request(app)
.put(/api/v1/image)
.query(qb)
.expect(200)
.then(res => { ... })

The flow of testing renaming endpoint:

  1. Make a new name, which I use timestamp to preventing unnecessarily duplicated possibility.
  2. Append query object to the request, here we need parameter filePath of the existing file and newName for the new name.
  3. Test the response contains correct response, the response contain success contains the success status, message contains the message for rendering the client, filePath which contain the new file path.
describe('PUT /api/v1/documentations/rename - rename an existing file', () => {  it('should rename existing file successfully', () => {    // Declare a newName contains the timestamp
const newName = Date.now().toString();
const filePath = testFilePath;
// Make sure you return the request to async execute the tests
return request(app)
.put('/api/v1/documentations/rename')
// Add query object here
.query({ filePath, newName })
.then((res) => {
const { success, message, filePath } = res.body;
expect(success).toBeTruthy();
expect(message).toBe('Rename successfully');
// Store the file path to test delete
testFilesPath = filePath;
})
.catch(err => console.log(err));
});
})

DELETE /api/v1/documentations/delete/path

Delete should be the easiest test compared to other tests, which should delete the test file we just finish uploading and renaming. The flow is:

  1. Making a delete request, and put the file path to the path.
  2. Test the response contain correct response, the response contain success contains the success status, message contains the message for rendering the client.
describe('DELETE /api/v1/documentations/:filePath - delete an existing file', () => {

it('should delete existing file successfully', () => {
return request(app)
.delete(`/api/v1/documentations/${testFilePath}`)
.then((res) => {
const { success, message } = res.body;
expect(success).toBeTruthy();
expect(message).toBe('Delete successfully');
})
.catch(err => console.log(err));
});
})

Add More Tests

Above are pretty much of the basic tests for uploading, renaming and deleting a file to a server, which means, of course, you will need to add more assertions like:

  • UPLOADING wrong file type
  • RENAMING a file with the same name of existing file.
  • DELETE with a non-existent file path.
  • you name it :)

Oh right, one more small tip, I encountered a situation that the CDN takes ages to log in, which exceeded the default timeout which raised the error:

Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL

If you encounter this issue, you can simply set a new timeout to the test like this:

const TIMEOUT_INTERVAL = 30000describe('DELETE /api/v1/... - delete an...', () => {

it('should delete...', () => {
// blablabla
}, TIMEOUT_INTERVAL);
})

In this way you can extend the time out for each test, easy peasy ;]


Hope you enjoy the post and find something useful. If there is anything you would like to say or share welcome to leave some comments below. Any response is welcome and clap is highly appreciated which will make my day, thank you :)

)

Linuk

Written by

Linuk

Full-time Computer Science Student | Web Developer | React Enthusiast | www.linuk.co.uk

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade