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,
)
})