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()
andentityFactory()
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()
andentityFactory()
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
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
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
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