Skip to content

Test First React part 2: API calls and Mocks

In the previous article we looked at the practice of writing test first React and built a simple component. This article demonstrates some more advanced test first React practices. Specifically, API calls and mocks.

Test First React example: A component with API calls

In the previous example, we built a simple user registration component that asks for a username, password and password confirmation. We’ll extend this example by checking the password against Troy Hunt’s Pwned Passwords API. This API checks if a given password appears in a set of passwords exposed in data breaches. If the password is pwned, the component shows a warning.

In order to build this, we have 2 new tasks:

  1. Call the Pwned Passwords API
  2. Add the warning to the component when the password is pwned.

We can build out both using test first practices. The completed source code and tests are in GitHub.

Test first API calls with fetch

We’ll use fetch to check the password against the Pwned Passwords service. The Pwned Passwords API is not trivial so it’s worth building this with tests. To protect the password, we don’t send a full cleartext password to the API. Instead, we:

  1. Take the SHA-1 hash of the password
  2. Determine the password range. This is the first 5 characters of the hash.
  3. GET a list of passwords in the range in the from the API, passing only the partial hash
  4. Search for the full SHA-1 hash in the API response

We’d like to wrap this in a simple function that takes the password plain text and returns true or false:

export function isPasswordPwned(password) {
    // return true if the given password is pwned
}

Mocking fetch

We don’t want our tests to call the real API so we’ll want to mock fetch. We want our mock to do two things. First, it should behave like the real thing and return a value:

const apiResponse = '1E2AAA439972480CEC7F16C795BBB429372:1\n' +
    '1E3687A61BFCE35F69B7408158101C8E414:1\n' +
    '1E4C9B93F3F0682250B6CF8331B7EE68FD8:3533661'; // This hash corresponds to 'password'

beforeEach(() => {
    fetch.resetMocks();
    fetch.mockResponseOnce(apiResponse);
});

it('should return true when password is pwned', () => {
    const result = isPasswordPwned(password);
    expect(result).resolves.toBe(true);
});

it('should return false when password is not pwned', () => {
    const result = isPasswordPwned(uniquePassword);
    expect(result).resolves.toBe(false);
});

Here we’ve used mockResponseOnce to define a mock response of fetch. Then when we call our function isPasswordPwned, we know exactly what it will receive. We can then test the logic of our function in isolation.

Second, we want to inspect calls to it to make sure they’re expected:

const expectedHashPrefix = '5baa6';
const password = 'password';

it('should request pwned passwords by partial hash', () => {
    isPasswordPwned(password);
    expect(fetch.mock.calls[0][0]).toBe('https://api.pwnedpasswords.com/range/' + expectedHashPrefix);
});

In this case, we check the first argument of the first call to fetch (retrieved as fetch.mock.calls[0][0] ) and expect it to contain a particular prefix. This allows us to test that we interact with the API correctly.

The full test case is in GitHub, along with the code it tests.

More component testing: mocking a module

We also want to test the new features of the user registration React component. We’ve already tested the Pwnedpasswords module so we should assume that works. We’ll mock the Pwnedpasswords module in the component test so we can test the component in isolation.

import * as pwn from "./Pwnedpasswords.api";

const goodPassword = 'password';
pwn.isPasswordPwned = jest.fn((pwd) => {
    return Promise.resolve(pwd !== goodPassword)
});

Here we import the Pwnedpasswords api module but then override its behaviour to be a Jest mock. The isPwnedPassword function is asynchronous – it returns a Promise. So that’s what our mock must do. The Promise resolves to false if the password is good and true otherwise (meaning the password is pwned).

it('should show warnings when password is pwned', async () => {
    const wrapper = shallow(<UserRegistration/>);
    // Set field values in the component and wait for the UI to update
    await setFieldValuesAndUpdate(wrapper, username, pwnedPassword, pwnedPassword);

    const inputField = wrapper.find('#passwordField input');
    const errorIcon = wrapper.find('#passwordField .icon');

    // Verify that input field shows validation failure
    expect(inputField.hasClass('is-danger')).toBe(true);
    expect(errorIcon.exists()).toBe(true);
});

it ('should hide warnings when password is not pwned', async () => {
    const wrapper = shallow(<UserRegistration/>);
    // Set field values in the component and wait for the UI to update
    await setFieldValuesAndUpdate(wrapper, username, goodPassword, goodPassword);

    const inputField = wrapper.find('#passwordField input')
    const errorIcon = wrapper.find('#passwordField .icon');

    // Verify that input field does not show validation failure
    expect(inputField.hasClass('is-danger')).toBe(false);
    expect(errorIcon.exists()).toBe(false);
});

Note specifically that this test deals with the component behaviour only. This test is interested in component fields, icons, form submits and so on. It is not interested in password hashes or calls to fetch. We’ve abstracted that into the Pwnedpasswords module and simply mocked expected behaviours in this test.

Again, the full example test code and React module code are in GitHub.

Putting it all together

We’ve tested and built a React component and a JavaScript module. The full working example is in GitHub. I hope that this has shown that it is easy to build components and applications using the test first React process. This gives us confidence that the components we’ve built work as expected. It will also give us the confidence to change, refactor and improve them later with less risk of regression.

Published inTestingWeb Technologies

One Comment

Leave a Reply

Your email address will not be published. Required fields are marked *