Jest
We use Jest for unit tests. All our API modules "should" have 100% coverage, the others (although ideally should be tested as well) aren't as important and the plan is to rely more on Cypress for end-to-end testing.
Test Utilities
We have a few test utilities in src/utils/testUtils.ts.
| Function | Description |
|---|---|
testAction | For testing actions/events |
testActionMeta | For testing actions/events with meta |
createTestReducer | For testing reducers |
testSelector | For testing selectors |
mockRestful | For mocking HTTP requests and responses in epics |
Module breakdown
Each module should have a test directory that contains test files. Below is a tree of the portfolio API module at the
time of writing. All API modules follow this structure, although not all modules require everything. Some modules may
not have hooks, utils or transorfmers for example. All modules do usually have constants, index, selectors
and state. Please refer to State Management for more info.
src/modules/api/portfolio
├── constants.ts
├── epics.ts
├── hooks.ts
├── index.ts
├── selectors.ts
├── state.ts
├── test
│ ├── data.ts
│ ├── epics.test.ts
│ ├── events.test.ts
│ ├── selectors.test.ts
│ ├── transformers.test.ts
│ └── utils.test.ts
├── transformers.ts
└── utils.ts
The examples below are all based on fetching portfolio data.
Test data
Test data is stored in ./test/data.ts to avoid cluttering test code. It is usually pulled from the API using
Insomnia and stored in the data file exported as plain objects.
Example
export const portfolio = {
success: {
request: '7917003f-0620-4145-8b58-d6741909ef69',
response: {
id: '7917003f-0620-4145-8b58-d6741909ef69',
createdByUserId: '64d177db-397a-4b0b-bcbd-513420eb6fcc',
updatedByUserId: '64d177db-397a-4b0b-bcbd-513420eb6fcc',
createdAt: 1551175703841,
updatedAt: 1551175846457,
tags: {
status: 'approved',
contract: 'active',
},
propertyId: 'dc6d11d4-a29b-4049-9293-bce0a50404a5',
contractContainer: {
type: 'ManagedContract_v1',
value: {
terms: {
serviceDepositAmount: 0,
damageDepositAmount: 0,
monthlyRentAmount: 300.15,
firstMonthRentAmount: 0,
leaseFee: {
netAmount: 0,
vatable: false,
},
applicationFee: {
netAmount: 0,
vatable: false,
},
},
},
},
tenants: [
{
id: '18d1cde9-7f2e-464a-85b2-90e00cf5b121',
partyId: '52424557-ac51-402b-b6a7-fb407e4fc04a',
isPrimary: true,
},
],
landlord: [
{
id: '72b7563f-4c48-4509-9ea9-eca3b6297295',
partyId: '03b08c13-e022-4949-996d-3abcb39fa8d5',
},
],
invoiceTemplates: [
{
id: '5ab4675f-b820-4de9-8c19-a65ed9596cb0',
category: 'RentInvoice',
invoicePartyId: '52424557-ac51-402b-b6a7-fb407e4fc04a',
interval: 'EndOfMonth',
netAmount: 300.15,
vatable: false,
autoDeliver: true,
paymentRules: [
{
beneficiary: {
type: 'PartyBeneficiary',
value: {
partyId: '2f44a0ee-4fe2-408d-a96d-93f9b7aeb35b',
partyTag: 'agency',
reference: 'Commission',
amount: 30.015,
},
},
clearance: {
type: 'FixedClearance_v1',
value: {
grossAmount: 30.015,
},
},
},
{
beneficiary: {
type: 'PartyBeneficiary',
value: {
partyId: '03b08c13-e022-4949-996d-3abcb39fa8d5',
partyTag: 'Owner',
reference: 'Rental Income',
amonut: 270.135,
},
},
clearance: {
type: 'FixedClearance_v1',
value: {
grossAmount: 270.135,
},
},
},
],
},
],
agents: [],
leaseIdentifier: 'AGE0000004',
leaseTerms: {
startDate: '2019-02-26',
endDate: '2020-02-26',
rolloverMonthToMonth: true,
},
settings: {
notes: 'Testing over payment',
customLeaseIdentifier: 'overpayment1',
autoSendPaymentReminderSms: true,
autoSendOwnerMonthlyEmail: true,
},
commission: {
managementFee: {
ofRentalAmount: {
type: commissionUndertakings.variable,
value: {
percentage: 10,
vatable: false,
},
},
splits: [
{
agentPartyId: '64d177db-397a-4b0b-bcbd-513420eb6fcc',
splitPercentage: 100,
},
],
},
},
status: 'draft',
},
},
}
Testing events (AKA Redux actions)
Responsible for asserting:
- The event exists (tests fail if the event is removed or renamed).
- The event returns the correct payload and meta, if used.
Example
describe('portfolio/events', () => {
test('portfolio_request', () => {
const { request } = data.portfolio.success
testAction(request, events.portfolio_request(request))
})
test('portfolio_success', () => {
const { response } = data.portfolio.success
testAction(response, events.portfolio_success(response))
})
})
Testing epics
Responsible for testing that when given a certain event(s) the correct output event(s) are returned as obervables. Please refer to writing tests for more information.
Example
let testScheduler: any
beforeEach(() => {
testScheduler = new TestScheduler(assertDeepEqual)
})
describe('describe('portfolio/epics', () => {', () => {
test('apiFetchEntity', () => {
testScheduler.run(({ hot, expectObservable }: any) => {
const { response, request } = data.portfolio.success
const action$ = hot('-a', {
a: events.portfolio_request(request),
})
const state$ = null
const get = () => of({ status: 200, response })
const output$ = epics.apiFetchEntity(action$, state$, { get, catchRestError })
expectObservable(output$).toBe('-a', {
a: events.portfolio_success(transformers.transformGetPortfolioResponse(response)),
})
})
})
})
Testing reducers
Responsible for ensuring the state is updated correctly for a specific event.
Example
const initialState = {
byId: {},
allIds: [],
}
describe('reducers', () => {
const testReducer = createTestReducer(reducer)
test('agency_success', () => {
testReducer(initialState, agencyApiEvents.agency_success(agencyResponse), {
...initialState,
allIds: uniq([...state.allIds, agencyResponse.id]),
byId: {
...initialState.byId,
[agencyResponse.id]: agencyResponse,
},
})
})
})
Testing transformers
Responsible for ensuring that the data transformations work as expected.
Example
it('transformGetPortfolioResponse - should transform portfolio response monetary values from decimals to cents', () => {
expect(transformGetPortfolioResponse(data.transformGetPortfolioResponse.source)).toEqual(
data.transformGetPortfolioResponse.expected,
)
})
Testing utils
Util tests are to ensure that given a certain input, the expected output is returned.
Example
test('setPrimaryParty on portfolio.parties.tenants', () => {
expect(setPrimaryParty(data.setPrimaryParty.input.parties.tenants, 's1-1d9c-43ec-9fab-b430f4fc47da')).toEqual(
data.setPrimaryParty.expected.parties.tenants,
)
})