Software Testing Literature: Guiding Principles for Mocking in Modern React Applications

Haddad
6 min readJun 1, 2023

--

Software testing is an essential process in the software development lifecycle. It guarantees the functional correctness and reliability of software, enhancing the overall quality and user satisfaction. This article will provide insights into software testing literature, focusing on a critical concept in the testing ecosystem, “Mocking,” using a practical example.

Understanding Mocking

Mocking is a testing technique where we replace real objects with fake objects that mimic the behavior of the real ones. This technique is widely used when the real objects are impractical or impossible to incorporate into the unit test.

Mocking becomes essential when we have external dependencies, like API calls, database connections, or even system-specific behavior that might not be available in the testing environment.

Best Practices for Mocking

When using mocking in tests, it’s essential to follow best practices to ensure that your tests are effective and manageable. Here are a few key points to consider:

  1. Isolate Units: When writing unit tests, make sure to mock all dependencies to ensure you’re only testing a single unit of code.
  2. Avoid Excessive Mocking: While it’s essential to mock dependencies to isolate the code you’re testing, over-mocking can lead to tests that are too tied to the implementation, making them brittle and less effective. It can also mask issues that could occur in the interaction between components.
  3. Mock with Realism: When you’re creating mock responses, they should be as close as possible to the real data your application will handle. This means considering edge cases and potential error states. This helps ensure your tests are reliable and relevant.
  4. Clean Up: After each test, reset your mocks to ensure that the state from one test doesn’t leak into another. In your example, you’ve done this with the mockNavigate.mockReset(); and server.resetHandlers(); calls in the afterEach block.

Mocking in Action

Let’s understand this using an example of a testing script for a React application. This is the working code that we’re going to breakdown

FormInput.jsx

This is the test file for the FormInput.jsx:
FormInput.test.jsx

import { render, fireEvent, waitFor, screen } from '@testing-library/react';
import { BrowserRouter as MockRouter } from 'react-router-dom';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { useNavigate } from 'react-router-dom';

import FormInput from '../Components/FormInput';

// Mock the API endpoint
const server = setupServer(
rest.put('http://localhost:8000/api/vessel-systems/4f52682e-f0d0-4d75-aa36-a17417dd68c9', (req, res, ctx) => {
return res(ctx.json({ success: true }));
})
);

// Mock navigate function
const mockNavigate = jest.fn();

jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useNavigate: () => mockNavigate,
}));

beforeAll(() => server.listen());
afterEach(() => {
server.resetHandlers();
mockNavigate.mockReset();
});
afterAll(() => server.close());

test('renders FormInput without crashing', () => {
render(<MockRouter><FormInput /></MockRouter>);
});

test('handles input changes', () => {
render(<MockRouter><FormInput /></MockRouter>);

const plannedRunningHourInput = screen.getByLabelText('Planned Running Hour (Hour) *');
fireEvent.change(plannedRunningHourInput, { target: { value: '500' } });
expect(plannedRunningHourInput.value).toBe('500');

const mainEngineHourInput = screen.getByLabelText('Main Engine (Hour) *');
fireEvent.change(mainEngineHourInput, { target: { value: '300' } });
expect(mainEngineHourInput.value).toBe('300');

const gearboxHourInput = screen.getByLabelText('Gearbox (Hour) *');
fireEvent.change(gearboxHourInput, { target: { value: '200' } });
expect(gearboxHourInput.value).toBe('200');

const auxiliaryEngineHourInput = screen.getByLabelText('Auxiliary Engine (Hour) *');
fireEvent.change(auxiliaryEngineHourInput, { target: { value: '100' } });
expect(auxiliaryEngineHourInput.value).toBe('100');

const shaftGeneratorHourInput = screen.getByLabelText('Shaft Generator (Hour) *');
fireEvent.change(shaftGeneratorHourInput, { target: { value: '100' } });
expect(shaftGeneratorHourInput.value).toBe('100');
});

test('submits form and shows notification', async () => {
render(<MockRouter><FormInput /></MockRouter>);

const plannedRunningHourInput = screen.getByLabelText('Planned Running Hour (Hour) *');
fireEvent.change(plannedRunningHourInput, { target: { value: '500' } });

const mainEngineHourInput = screen.getByLabelText('Main Engine (Hour) *');
fireEvent.change(mainEngineHourInput, { target: { value: '300' } });

const gearboxHourInput = screen.getByLabelText('Gearbox (Hour) *');
fireEvent.change(gearboxHourInput, { target: { value: '200' } });

const auxiliaryEngineHourInput = screen.getByLabelText('Auxiliary Engine (Hour) *');
fireEvent.change(auxiliaryEngineHourInput, { target: { value: '100' } });

const shaftGeneratorHourInput = screen.getByLabelText('Shaft Generator (Hour) *');
fireEvent.change(shaftGeneratorHourInput, { target: { value: '100' } });

const submitButton = screen.getByRole('button', { name: /Generate Forecast Maintenance/i });
fireEvent.click(submitButton);


// Here you could also check whether the correct path is shown in the URL
// but this would require further mocking of the react-router-dom useHistory hook.
});


test('handles server error response', async () => {
server.use(
rest.put('http://localhost:8000/api/vessel-systems/4f52682e-f0d0-4d75-aa36-a17417dd68c9', (req, res, ctx) => {
return res(ctx.status(500));
})
);

render(<MockRouter><FormInput /></MockRouter>);

// fill in form inputs...
const plannedRunningHourInput = screen.getByLabelText('Planned Running Hour (Hour) *');
fireEvent.change(plannedRunningHourInput, { target: { value: '500' } });

const mainEngineHourInput = screen.getByLabelText('Main Engine (Hour) *');
fireEvent.change(mainEngineHourInput, { target: { value: '300' } });

const gearboxHourInput = screen.getByLabelText('Gearbox (Hour) *');
fireEvent.change(gearboxHourInput, { target: { value: '200' } });

const auxiliaryEngineHourInput = screen.getByLabelText('Auxiliary Engine (Hour) *');
fireEvent.change(auxiliaryEngineHourInput, { target: { value: '100' } });

const shaftGeneratorHourInput = screen.getByLabelText('Shaft Generator (Hour) *');
fireEvent.change(shaftGeneratorHourInput, { target: { value: '100' } });
const submitButton = screen.getByRole('button', { name: /Generate Forecast Maintenance/i });
fireEvent.click(submitButton);

await screen.findByText('Update failed. Please try again.');
});

test('handles fetch error', async () => {
server.use(
rest.put('http://localhost:8000/api/vessel-systems/4f52682e-f0d0-4d75-aa36-a17417dd68c9', (req, res, ctx) => {
throw new Error('Mock fetch error');
})
);

render(<MockRouter><FormInput /></MockRouter>);

// fill in form inputs...
const plannedRunningHourInput = screen.getByLabelText('Planned Running Hour (Hour) *');
fireEvent.change(plannedRunningHourInput, { target: { value: '500' } });

const mainEngineHourInput = screen.getByLabelText('Main Engine (Hour) *');
fireEvent.change(mainEngineHourInput, { target: { value: '300' } });

const gearboxHourInput = screen.getByLabelText('Gearbox (Hour) *');
fireEvent.change(gearboxHourInput, { target: { value: '200' } });

const auxiliaryEngineHourInput = screen.getByLabelText('Auxiliary Engine (Hour) *');
fireEvent.change(auxiliaryEngineHourInput, { target: { value: '100' } });

const shaftGeneratorHourInput = screen.getByLabelText('Shaft Generator (Hour) *');
fireEvent.change(shaftGeneratorHourInput, { target: { value: '100' } });
const submitButton = screen.getByRole('button', { name: /Generate Forecast Maintenance/i });
fireEvent.click(submitButton);

await screen.findByText('Update failed. Please try again.');

});


test('submits form and navigates', async () => {
render(<MockRouter><FormInput /></MockRouter>);

const submitButton = screen.getByRole('button', { name: /Generate Forecast Maintenance/i });
fireEvent.click(submitButton);

// Wait for navigation
await waitFor(() => expect(mockNavigate).not.toHaveBeenCalledWith('/dashboard'));
});

The application allows input of vessel parameters, performs some calculation based on the input, and updates the same in a database through an API call.

In the test script, we are using the ‘msw’ (Mock Service Worker) library to intercept and mock the network requests. The ‘msw’ library provides an easy-to-use setup for creating a mock server that intercepts the network requests the application makes and returns controlled responses.

const server = setupServer(
rest.put('http://localhost:8000/api/vessel-systems/4f52682e-f0d0-4d75-aa36-a17417dd68c9', (req, res, ctx) => {
return res(ctx.json({ success: true }));
})
);

Here we are mocking a ‘PUT’ request to a specific API endpoint and returning a controlled response — a JSON object with a key ‘success’ set to true.

Mocking Navigation

Often, we may also need to mock behaviors that are outside the scope of the component that we are testing. A classic example is navigation using the ‘react-router-dom’ library in React.

In our example, we mock the useNavigate hook from ‘react-router-dom’ using jest.mock, which allows us to intercept and modify the behavior of this hook in our test environment.

jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useNavigate: () => mockNavigate,
}));

Error Handling in Mocking

Mocking also allows you to test the error handling capabilities of your application. You can mock a server response to return an error, allowing you to check if your application handles it correctly.

server.use(
rest.put('http://localhost:8000/api/vessel-systems/4f52682e-f0d0-4d75-aa36-a17417dd68c9', (req, res, ctx) => {
return res(ctx.status(500));
})
);

In this mock, we change the server response to return a 500 status code, simulating a server error.

Conclusion

In the dynamic world of software development, testing strategies must evolve to match the pace and complexity of application development. One such strategy is mocking, a powerful technique that has proven to be a game-changer in creating efficient, comprehensive, and robust test suites.

Using mocking, we can simulate real-world scenarios, isolate components for precise unit testing, and ensure our applications handle both successful and erroneous outcomes gracefully. Libraries like jest and msw have equipped developers with advanced mocking capabilities, making the process more streamlined and effective.

However, it’s essential to remember the guiding principles of mocking. These include isolating units, avoiding excessive mocking, striving for realism in mock responses, and cleaning up after each test. Adhering to these best practices allows us to maximize the benefits of mocking and maintain high-quality, reliable tests.

While mocking does introduce an additional layer of complexity to our tests, the ability to control and predict every aspect of the system during the testing phase is an undeniable advantage. It allows us to build software that is more resilient, more reliable, and better suited to meet user expectations.

As we continue to innovate and develop more complex applications, effective mocking will remain a cornerstone of successful testing strategies. So, let’s embrace it and remember that “With great mocking power, comes great testing responsibility!” Because, at the end of the day, the quality of our software is determined not just by the functionality it delivers, but also by its reliability, durability, and ability to handle unexpected situations gracefully.

“With great mocking power, comes great testing responsibility!”

That is the true power and promise of effective software testing and, in particular, effective mocking.

--

--

Haddad
Haddad

Written by Haddad

Computer Science Graduate from Univertas Indonesia. Exploring Backend Automation, AI/ML Engineering, and Sofware Infrastructure. Github: @haddad9

No responses yet