Never thought I would have to do this before!

We accept card-not-present payment via a Stripe, and we use the API provided by Stripe.js library to render Stripe iframes that handle the user input for us.
The overall structure is:

1
2
3
4
5
6
7
<Element stripe> //provider
<OurComponentToCollectCardDetails>
// ..
<CardElement>
// ..
</OurComponentToCollectCardDetails>
</Element>

Challenge 1: Mock all the moving parts of the package

Found a mock that got me to render the CardCollectionForm (but not the iframe) at this GitHub issue with this tweak to make TS happy

The most helpful takeaway is the usage of jest.requireActual, which largely reduces the volume of job.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
const mockElement = () => ({
mount: jest.fn(),
destroy: jest.fn(),
on: jest.fn(),
update: jest.fn(),
})

const mockElements = () => {
const elements: { [key: string]: ReturnType<typeof mockElement> } = {}
return {
create: jest.fn((type) => {
elements[type] = mockElement()
return elements[type]
}),
getElement: jest.fn((type) => {
return elements[type] || null
}),
}
}

const mockStripe = () => ({
elements: jest.fn(() => mockElements()),
createToken: jest.fn(),
createSource: jest.fn(),
createPaymentMethod: jest.fn(),
confirmCardPayment: jest.fn(),
confirmCardSetup: jest.fn(),
paymentRequest: jest.fn(),
_registerWrapper: jest.fn(),
})

jest.mock('@stripe/react-stripe-js', () => {
const stripe = jest.requireActual('@stripe/react-stripe-js')

return {
...stripe,
Element: () => {
return mockElement()
},
useStripe: () => {
return mockStripe()
},
useElements: () => {
return mockElements()
},
}
})

Challenge 2: The child component CardElement needs to invoke callback from the tested parent component

I mocked the implementation of the CardElement:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
jest.mock('@stripe/react-stripe-js', () => {
const stripe = jest.requireActual('@stripe/react-stripe-js')

return {
...stripe,
Element: () => mockElement(),
useStripe: () => mockStripe(),
useElements: () => mockElements(),
CardElement: ({ onChange, options }: any) =>
cardElementSimulationButtons(onChange),
}
})

const cardElementSimulationButtons = (onChange: any) => (
<>
<button
data-testid={simulateTestIds.error}
onClick={() =>
onChange!(
generateStripeCardElementChangeEvent(false, false, {
type: 'validation_error',
code: '110',
message: testErrorMessage,
})
)
}
/>
<button
data-testid={simulateTestIds.complete}
onClick={() =>
onChange!(generateStripeCardElementChangeEvent(true, false, undefined))
}
/>
<button
data-testid={simulateTestIds.empty}
onClick={() =>
onChange!(generateStripeCardElementChangeEvent(false, true, undefined))
}
/>
<button
data-testid={simulateTestIds.nonEmpty}
onClick={() =>
onChange!(generateStripeCardElementChangeEvent(false, false, undefined))
}
/>
</>
)

const generateStripeCardElementChangeEvent = (
complete: boolean,
empty: boolean,
error?: any
): StripeCardElementChangeEvent => ({
complete,
error,
empty,
elementType: 'card',
value: {
postalCode: '',
},
brand: 'visa',
})

Then I could fire click event to simulate onChange invocations.

Are these practices actually good?

Follow up readings

https://maksimivanov.com/posts/dont-mock-what-you-dont-own/