Jest unit test guidance

Currently the LiteFarm codebase has some unit tests for the Express API written using the Jest Javascript testing framework. There are individual test files for each of the user flows in the app and the naming convention for these files is <<userflow>>.test.js. Here is an excerpt.

const mocks = require('./mock.factories'); describe('Notification tests', () => { function getRequest(url, { user_id = user.user_id, farm_id = farm.farm_id }, callback) { chai.request(server).get(url).set('user_id', user_id).set('farm_id', farm_id).end(callback); } let user; let farm; let userFarm; beforeEach(async () => { [user] = await mocks.usersFactory(); [farm] = await mocks.farmFactory(); [userFarm] = await mocks.userFarmFactory({ promisedUser: [user], promisedFarm: [farm] }); const middleware = require('../src/middleware/acl/checkJwt'); middleware.mockImplementation((req, res, next) => { req.user = {}; req.user.user_id = req.get('user_id'); next(); }); }); describe('GET user notifications', () => { test('Users should get their notifications scoped for their current farm', async (done) => { const [notification] = await mocks.notification_userFactory({ promisedUserFarm: [userFarm], }); getRequest('/notification_user', {}, (err, res) => { expect(err).toBe(null); expect(res.status).toBe(200); expect(res.body.length).toBe(1); expect(res.body[0].user_id).toBe(user.user_id); expect(res.body[0].notification_id).toBe(notification.notification_id); done(); }); });

describe is used to organize groups of tests. test defines an individual test with an English description and a function that receives done. The argument is a callback function that the test must call when it has finished.

The tests make use of ChaiHttp to make requests to the endpoints under test, which must first be declared as a helper function (lines 4 to 6).

beforeEach configures the Jest framework to run the setup needed before each individual test.

The test shown relies on database contents created by beforeEach. The userFarm object contains details of these database contents, and is generated using mock.factories.js file which contains other mock data factories that are useful for testing the code (you can read more about this in the factories.README.md under the same directory). The test passes the userFarm object to notification_userFactory to create additional database content specific to this test. This establishes known database contents as a basis for testing data retrieval by the API route "under test". In some cases faker is also used to generate test data.

The test then uses the helper function to make a GET request to the URL for retrieving the user's notifications for their current farm. This executes the code under test, which we expect to retrieve known data values. When the request completes, Jest runs the anonymous function that receives two arguments representing any error, and the API response.

expect calls are the actual testing; they describe the expected results. Conditional logic is embedded within expect so that any unsatisfied expectations will cause the test to fail. If all expectations are met and the code calls done(), then the test passes.

Once a test is written, you can run the entire test suite as specified in the README file in the LITEFARM repository. You can also run specific test files using the command:

npm test -- <test_file>

The output below includes the English provided to the describe and test calls above. The second green check mark indicates that the test shown in the excerpt has passed.

Peer review process

Early in each sprint QA writes test plans for all tickets in the sprint, which can be found in the projects confluence page under the Sprints section and linked to individual tickets on Jira. These test plans should be used as guidance by developers when writing unit tests for the api and will specify the minimum test requirements for the ticket(can be found in the “Back end” column of the test plan page). Developers are encouraged to exceed this guidance when writing their tests based on their own unique knowledge of their solution. Apart from the current reviews of pull requests by members of the engineering team before merging into the integration branch, QA is required to also review each pull request and their approval(or lack there of) will also block merges into integration. QA will approve PR’s when the minimum test plan requirements have been implemented.

Example

LF-2229 includes changes to the API, the test plan for this ticket outlines some API testing requirements, the resulting Jest tests in the code base look to confirm these API testing requirements are met, one of these tests is below:

function patchTaskDateRequest({ user_id, farm_id }, data, task_id, callback) { chai .request(server) .patch(`/task/patch_due_date/${task_id}`) .set('user_id', user_id) .set('farm_id', farm_id) .send(data) .end(callback); } test('Farm owner must be able to patch task due date to today', async (done) => { const today = new Date(); const due_date = today.toISOString().split('T')[0]; const patchTaskDateBody = { due_date }; const [{ user_id, farm_id }] = await mocks.userFarmFactory({}, fakeUserFarm(1)); const [{ task_id }] = await mocks.taskFactory({ promisedUser: [{ user_id }] }); const [{ location_id }] = await mocks.locationFactory({ promisedFarm: [{ farm_id }] }); await mocks.location_tasksFactory({ promisedTask: [{ task_id }], promisedField: [{ location_id }], }); patchTaskDateRequest({ user_id, farm_id }, patchTaskDateBody, task_id, async (err, res) => { expect(res.status).toBe(200); const updated_task = await getTask(task_id); expect(toLocal8601Extended(updated_task.due_date)).toBe(due_date); done(); }); });

One of the requirements for LF-2229 is that only users with permission to modify the task date are allowed to modify the task date via the API, the test plan looks to test that this requirement is met in step 7 and the Jest test above also partially confirms that this requirement is met by ensuring that a farm owner account i.e. an account with administrative permission is indeed allowed to modify the task date via API and that the date is updated in the database.

The future

It has been agreed that a departure from the current pattern where mock factories are used to setup database state for Jest unit tests in favour of a seed database with pre-made farms, users, crops, etc. that create a few baseline environments for testing would be preferable as this would make the tests much shorter and more readable. But this approach is not without its drawbacks most notable of which is that a significant refactor of the current test suite would be required. Another drawback of this approach is it would also introduce the complexities of keeping the testing files in sync with the seed script.