4. Unit testing with jest and chai (May 2021)

This file was copied from the packages/webapp/src/stories/docs folder. Its accuracy as of August 8, 2024 has not been verified

The current endpoint test set up is far from perfect. First tests can't run in parallel. Second, even if tests run in sequence, clean up script might interfere with set up of the next test. Third, put location tests fail randomly for unknown reason

If you have better idea on how tests should be set up, feel free to pitch to Kike and Kevin. Or you can create a Tech Debt story on Jira

| Resources | | --------- | | UpsertGraph - objection js | | faker | | knex.js | | setup and tear down - jest | | expect - jest |

1. Copy paste an existing test file as template under packages/api/tests directory

Here is a list of tests that can be used as templates

  • managementPlan.test.js

  • crop.test.js

  • disease.test.js

  • expense.test.js

  • expense_type.test.js

  • fertilizer.test.js

  • pesticides.test.js

  • price.test.js

  • sale.test.js

You can follow location.test.js or mock.factories.test.js if you think api will be stable

These tests are more abstracted. They are difficult to read/debug/modify. You should use previous templates if tests need to cover edge cases.

A list of test that will be deprecated

  • authorization.test.js

  • insightAPI.test.js

  • yield.test.js

  • price.test.js

  • logs.test.js

2. Set up database before each test cases

** 1. Create new mock factory in file mock.factories.js **

  • First copy paste a pair of fakeEntity() and entityFactory() functions as template

async function fieldFactory({ promisedStation = weather_stationFactory(), promisedFarm = farmFactory(), promisedLocation = locationFactory({ promisedFarm }), promisedArea = areaFactory({ promisedLocation }, fakeArea(), 'field'), } = {}, field = fakeField()) { const [station, location] = await Promise.all([promisedStation, promisedLocation, promisedArea]); const [{ station_id }] = station; const [{ location_id }] = location; return knex('field').insert({ location_id: location_id, station_id, ...field }).returning('*'); } function fakeField() { return { organic_status: faker.helpers.arrayElement(['Non-Organic', 'Transitional', 'Organic']), transition_date: faker.date.future(), }; } module.exports = { fieldFactory, fakeField }

Copy

  • Then use the template to complete fakeEntity() and entityFactory() for your new objection model.

  • You can also use IDE search function to find necessary faker / knex methods

  • Run mock.factories.test.js to make sure everything passes

  1. Update clean up script /api/tests/testEnvironment

If tests involve new tables, we need to update clean up script so that all rows in database will be deleted after one test file finishes.

  • If we just added location table, which depends on farm, we need to delete location before userFarm/users/farm, otherwise clean up script will return foreign key violation error

async function tableCleanup(knex) { return knex.raw(` DELETE FROM "location"; // new table DELETE FROM "userFarm"; DELETE FROM "farm"; DELETE FROM "users"; `); }

Copy

  1. Set up database before each test

There are two ways to set up

  • Create set up functions and use them in each test (example: location.test.js).

function appendFieldToFarm(farm_id, n = 1) { return Promise.all( [...Array(n)].map(() => mocks.fieldFactory({ promisedLocation: mocks.locationFactory({ promisedFarm: [{ farm_id }] }) })) ); } describe('GET /location by farm', () => { test('should GET 2 fields linked to that farm', async (done) => { await appendFieldToFarm(farm, 2); getLocationsInFarm({user_id: user, farm_id: farm}, farm, (err, res) => { expect(res.status).toBe(200); done(); }); }); })

Copy

Copy

To test patch field endpoint, we need to set up all field dependencies such as user/userFarm/location/area/figure tables.

 

We also need to set up farm owner/manager/extension officer/worker for authorization tests

 

In the test scope, we need to have access to user_id, farm_id, and location_id to make the request and check result

3. Test driver functions

Here are four example driver functions:

Copy

4. Writing tests

Copy

Jest documentation

There are four cases we need to test

  • Success with 200/201

  • req.body validation fails

When user posts a field, req.body.location.figure.type === "area". If type is any other string such as "line" or "point", endpoint should return 400

  • Authorization tests

For this test, we need to make sure new user permissions are inserted into permission and rolePermission tables

 

If we want to test post field endpoint, we need to make sure owner/manager/extension officer of current farm can post successfully but owner/manager/extension officer of another farm and worker of current farm can't post field

 

Requests should fail with 403

  • Patch protected column fails

If we allow a farm owner to patch user info of a worker but farm owner should not be able to patch worker's email address to gain access to the account. In this case, we need to write tests trying to patch email from different perspective and make sure all attempts fail

 

Objection provide upsertGraph method, which allows us to update multiple tables with single query. However, if the query is not written properly, one can update user table through user_id by sending following request. If upsertGraph is used, we need to write test to make sure the endpoint can't modify unintended tables

Copy

5. Run tests to make sure tests fail as expected

6. Middleware mock implementation

After controllers are implemented, if test always fails, we might need to add mock implementations to functions that cause tests to fail

Most endpoints are guarded by checkJwt() (jwt authentication middleware) . Authorization middleware checkScope() will then use the result (res.user) returned by jwt authentication middleware to check user permissions. In this case, we need to mock checkJwt() (jwt authentication middleware) so that checkJwt() will never return 401 and driver function can pass user_id into req.auth for authorization.

Copy

Steps for a backend - frontend story from a data flow perspective

  • Create knex migration to create/modify tables in database

  • Create objection models

  • Endpoints and empty controllers

  • Backend jest unit testing

  • Implement controllers

  • Create authorization/validation middlewares

  • Run tests and make sure everything passes

  • Create pure frontend components in storybook

  • res.data normalizer, redux slice, redux selectors to hold and access the data

  • Redux saga for get/post/put/patch request

  • Container component to connect saga actions/selector with pure components

  • Connect Route component with container components with react router

  • Manual testing