Hey, hey, 👋
Happy Monday to everyone! 🫶
Recently, I wrote an article about designing a testing strategy for a frontend application and how, most of the time, this part is being skipped due to specific reasons. You can check it here: ⬇️
The article prompted some follow-up questions that I would like to address in today's discussion:
How do we separate presentation logic from the views in React?
How to unit test the API wrapper?
How to unit test components that use third-party libraries?
Huge thanks to for raising these questions! I really appreciate it! 🙏
I also encourage you to read his newsletter as I am 100% sure that you will find valuable insights in it!
With these questions, I encourage everyone reading and engaging with my content (and not only mine, of course 😁) to raise any inquiries or curiosities. I would be more than happy to address them in future articles.
Now let’s dive in!
When designing a testing strategy for a frontend application, some question arising could create the base for a set of best practices in this context. I believe that by answering the questions mentioned above, I’ll address three essential topics for testing in frontend development: separating presentation logic from views, unit testing API wrappers, and testing components that use third-party libraries.
Let's explore each of them in detail and understand their specific characteristics.
1. Separating Presentation Logic from Views in React
In React, ensuring that components remain focused on rendering UI while business logic is extracted into separate layers sets the base for the SoC principle implementation (Separation of Concerns ).
Practical implementation in React:
Custom Hook
In this case, where we would have the logic included directly in the component like in this snippet ⬇️, we could apply SOC and extract the logic in a custom hook.
Fetching user data can be extracted in a custom hook - useUser()
and in this way, this small logical piece can be easily tested.
By doing this separation we will get the following benefits:
useUser
isolates data fetching logic.UserProfile
remains focused on rendering.useUser
can be unit tested separately.
Another way through which we can ensure a separation of logic, related to custom hooks, is by implementing Headless Components. These ones are decoupled from the UI and you can focus on testing the component's functionality and logic without the need for rendering or interacting with the DOM. A relevant example here, would be the case of a toggle component - the logic would be hold in useToggle
custom hook and used if different UI components such as ToggleComponent
and StyledToggleComponent
.
Container/ Presentational Components
Presentational components, being purely focused on the UI rendering, becomes very easy to test. You provide them different sets of props and assert if the component renders correctly.
In this case ⬆️, the component:
receives an arrays of
users
via propsrenders a list of users
handles the case where there are no users
Testing this component will have two main goals:
providing different sets of props
checking how the component renders and test against empty
users
prop.
Container components, on the other side, hold the entire logic and can be tested in isolation. Unit tests can focus on verifying that the container component correctly handles data, state changes, and interactions, and that it passes the correct props to the presentational components.
In this case ⬆️, the component:
handles data fetching using
useEffect
andfetch
.manages the
users
,isLoading
, anderror
state.passes the
users
array as a prop to theUserList
presentational component.handles loading and error states.
Testing this component will focus on:
mocking the
fetch
API call to simulate different responses (success, error).testing that the
users
state is updated correctly after a successful fetch.testing that the loading and error states are handled correctly.
asserting that the correct data is passed to the
UserList
component.
2. Unit Testing API Wrappers
An API wrapper typically abstracts network requests and testing it is crucial in order to ensure that the applications’ data layers are robust and reliable.
Let’s see what is the purpose of testing an API wrapper:
to test its logic in isolation without relying on the actual API or network requests
to verify that wrapper handles correctly different responses (success, error and any edge cases might arise)
to make sure that any changes made to the API wrapper won’t break anything or introduce unexpected behavior
to ensure that the API wrapper consistently transforms and manages data in the expected format
When testing an API wrapper, there are some key areas meant to be tested:
Successful API Calls:
Verify that the wrapper correctly makes API requests with the appropriate parameters.
Assert that the wrapper parses and transforms the API response data into the expected format.
Test that the wrapper returns the correct data when the API call is successful.
Error Handling:
Simulate API errors (e.g., 400, 500 status codes, network errors).
Ensure that the wrapper catches and handles these errors gracefully.
Verify that the wrapper returns appropriate error messages or objects.
Test that the wrapper handles network connectivity issues.
Parameter Handling:
Test that the wrapper correctly passes parameters to the API.
Verify that the wrapper handles different types of parameters (e.g., query parameters, request bodies).
Test that the wrapper handles missing or invalid parameters.
Data Transformation:
If the wrapper transforms the API response data, ensure that the transformation is performed correctly.
Test that the wrapper handles different data types and structures.
Verify that the transformed data matches the expected format.
Caching (if applicable):
If the wrapper implements caching, test that it correctly caches and retrieves data.
Verify that the cache invalidation logic works as expected.
Testing techniques and tools used when testing an API wrapper:
Mocking - use mocking libraries (e.g., Jest's
mock
,fetch-mock
,axios-mock-adapter
) to simulate API responses without making actual network requests. Mock thefetch
API or your HTTP client (e.g., Axios).Test runners - use test runners like Jest or Mocha to execute your unit tests tests
Assertion libraries - use assertion libraries (e.g., Jest's
expect
, Chai) to verify that the wrapper's output matches the expected values.async/await
- useasync/await
to handle asynchronous API calls in your tests
Let’s take a look at a basic example of a unit test for an API wrapper where most of the techniques and tools mentioned above are being used:
3. Testing components that use third-party libraries
Testing components that use third-party libraries is important, but there are some things we need to consider before that:
Some third-party libraries can introduce external dependencies that can make unit testing more complex.
You'll often need to mock parts of the third-party library to isolate your component and avoid relying on external services or complex logic.
It's important to distinguish between unit tests (testing the component in isolation) and integration tests (testing the component with the third-party library). Unit tests should focus on the component's logic, not the library's behavior.
Many third-party libraries involve asynchronous operations, requiring careful handling in your tests.
If the library manipulates the DOM, you'll need to use appropriate testing utilities (e.g.,
@testing-library/react
) to simulate DOM interactions.
There are some testing strategies and tools we can use in most of the scenarios:
Mocking Third-Party Modules:
Use Jest's
jest.mock()
to replace third-party modules with mock implementations.Create mock functions or objects that simulate the behavior of the library's functions or components.
This allows you to control the library's output and isolate your component's logic.
Mocking Specific Functions:
Use
jest.spyOn()
to mock specific functions within a third-party module.This allows you to track function calls, set return values, and assert that the component interacts with the library correctly.
Using Mocked Components:
If the third-party library provides React components, create mock components that render simple placeholders.
This prevents the actual library components from rendering and simplifies your tests.
Testing Library Utilities:
Use testing libraries like
@testing-library/react
to simulate user interactions and assert on the rendered output.This library encourages testing the component from a users perspective.
This helps with testing DOM interactions, and allows for easier testing of component side effects.
Handling Asynchronous Operations:
Use
async/await
andwaitFor
from@testing-library/react
to handle asynchronous operations.Mock the library's asynchronous functions to return promises that resolve or reject with controlled values.
Let’s take a look at a simple example of a unit test on a Button component integrating Firebase Analytics:
What happens there ⬆️ ?
jest.mock(’firebase/analytics’ …)
- replaces thefirebase/analytics
module with a mock objectlogEvent:jest.fn()
- creates a mock function that we can control and assert on.const { logEvent } = require('firebase/analytics')
- extract the mocked function to inspect its calls laterrender(<TrackButton />)
mounts the component in a virtual DOM.fireEvent.click(..)
- simulating a user clickasserting that
logEvent
was called correctly -expect()
,toHaveBeenCalled()
Conclusion
By separating presentation logic, mocking API calls, and testing third-party integrations properly, we can write robust tests that improve maintainability and reliability. These practices covered in today’s discussion can make it easier to scale and debug React applications efficiently.
Until next time 👋,
Stefania
P.S. Don’t forget to like, comment, and share with others if you found this helpful!
💬 Let’s talk:
What are your thoughts on this topic? Are you doing things differently? If yes, how?
Let’s discuss in the comments! 🚀
Other articles from the ♻️ Knowledge seeks community 🫶 collection: https://stefsdevnotes.substack.com/t/knowledgeseekscommunity
👋 Get in touch
Feel free to reach out to me here on Substack or on LinkedIn.
Hey Stefania, thank you for the shout-out. I really appreciate it! 😌
Also, thank you for following up on my questions. I think this post is a great addition to the other one and promotes many best practices for front-end development with React, but it is not limited to it.
I highly recommend them to all software engineers who want to level up their skills.
A great addition to this series would be the process of implementing an entire feature (like adding and listing users) where all these practices are combined:
- separating view logic from presentation logic
- testing hooks
- testing the external API/services
- integration testing
- ens-2-end testing
Of course, this could make the case for a series of issues and include an open-source project.
Nevertheless, it seems you are very passionate about front-end development and doing a great job mastering it!