ITea Talks with Martin Ivanov: Cracking the Code of Unit Testing: From Philosophies to Real-World Challenges
In today’s rapidly evolving software development landscape, testing is more critical than ever. Among the many layers of testing, unit testing stands out for its role in verifying the smallest functional components of an application. By catching bugs early, unit tests contribute to the overall reliability and maintainability of software systems, especially in complex applications.
How do modern software development teams approach testing? Where does unit testing fit into the test hierarchy? What are the philosophies behind unit testing? Are there unit testing challenges unique for frontend development? Today, we answer these questions with Martin Ivanov - Senior Software Engineer at adesso Bulgaria .
?
Hello, Martin! Can you provide a brief overview of the types of software tests that exist and where unit testing fits in?
?Hello! Testing is like building a safety net for your code. There are many types of tests, each with a unique purpose:
A balanced test suite typically incorporates multiple test types. For backend and frontend alike, unit tests are the foundation everything else is built on.
Could you explain the philosophies behind unit testing and their practical implications?
?Unit testing revolves around ensuring that the smallest units of functionality in an application behave as expected. Thus, they should be designed plain and simple, not much logic and few to no abstractions at all. If a unit test is perceived as a burden rather than as a friendly assistant, then something is wrong with the test design or implementation.
Over time, two distinct schools of thought with respect to unit testing have emerged:
- Benefits: Tests focus on the result, not the implementation, which fosters robust code refactoring.
- Drawbacks: Can be harder to isolate the unit under test, leading to more complex test setups. May also result in slower tests if real dependencies are used.
-??Benefits: Encourages strict adherence to component boundaries.
-? Drawbacks: Tends to overemphasise implementation details, making refactoring more challenging.
?
You mention stubs, fakes, mocks. Can you explain more about the different types of test doubles used in unit testing?
?Sure, in unit testing, the knack is to isolate the unit under test from its dependencies to ensure that the test results are reliable and focused solely on the unit's behaviour. This is where test doubles come into play: they are substitutes for real objects in a testing environment, allowing developers to isolate the unit under test and ensure reliable results. There are several types of test doubles:
?Of these kinds of doubles, only mocks insist upon behaviour verification. The other doubles can, and usually do, use state verification.
?
Which objects are usually replaced by test doubles?
The objects that are typically replaced by test doubles include:
?
As the focus of your expertise is frontend development, can you illustrate these concepts with frontend test examples?
Yes, here are two examples of unit tests using Jest with TypeScript and React:
?
1. Classic Approach:
import { render, screen } from '@testing-library/react'
import { http, HttpResponse } from 'msw'
import { setupServer } from 'msw/node'
?
import UserList from './UserList'
?
const server = setupServer(
??? http.get('/api/users', () => {
??????? return new HttpResponse.json([{ id: 1, name: 'John Doe' }])
??? })
领英推荐
)
?
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
?
test('renders a list of users', async () => {
??? render(<UserList />)
??? expect(await screen.findByText(/John Doe/i)).toBeInTheDocument()
})
?
Here, while the REST API call is mocked with the Mock Service Worker library, the function fetchUserData that makes the call is untouched. This ensures the test will catch bugs in that function and remain unaffected by changes to the implementation of data fetching.
?
2. Mockist Approach:
import { render, screen, fireEvent } from '@testing-library/react'
?
import { fetchUserData } from './api'
import UserProfile from './UserProfile'
?
jest.mock('./api')
const mockFetchUserData = fetchUserData as jest.Mock
?
mockFetchUserData.mockResolvedValue({ id: 1, name: 'Jane Doe' })
?
test('fetches and displays user data', async () => {
??? render(<UserProfile />)
??? expect(mockFetchUserData).toHaveBeenCalledTimes(1)
??? expect(await screen.findByText(/Jane Doe/i)).toBeInTheDocument()
})
?
Here, the function fetchUserData that makes the REST API call is mocked. Thus, if a bug occurs in that function, it will not be caught. If the implementation of fetching the user data is changed, e.g. fetchUserData is renamed to fetchData, the test will have to be adjusted; the same would be needed, if the user data structure is changed, e.g. id is renamed to userId.
?
In your opinion, what’s the biggest challenge in unit testing, particularly for frontend development?
Frontend unit testing has its own quirks. State management is probably the toughest nut to crack.? Frontend components often rely on a shared global state (e.g., React Context, Zustand, etc.).? Testing a component in isolation can require setting up mock states, which may become cumbersome if the store structure is complex. Moreover, mocking the global state, inadvertently leads to testing implementation details, which beats the purpose of unit testing. To address this, the following measures can be taken:
?
Which of two principal approaches to unit testing do you recommend, and why?
I prefer the classic approach because:
?
While both the classic and mockist approaches have their merits, the classic approach stands out for its focus on behaviour over implementation, making it particularly valuable in frontend development. However, it’s important to note that these approaches are not mutually exclusive. While one may generally be preferred, certain situations might objectively call for the other. As a best practice, I recommend adhering to one approach consistently, but remaining flexible and open to employing the other when common sense dictates.
?Unit testing isn’t just about writing tests—it’s about building confidence in your work. A well-designed test suite bridges the gap between code and quality, empowering developers across the stack to deliver reliable, maintainable software.