Skip to main content

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.

FunctionDescription
testActionFor testing actions/events
testActionMetaFor testing actions/events with meta
createTestReducerFor testing reducers
testSelectorFor testing selectors
mockRestfulFor 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:

  1. The event exists (tests fail if the event is removed or renamed).
  2. 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,
)
})