Learn unit testing from scratch with real examples, clear explanations, and practical code you can use today.
No one taught us testing the right way.
They said, “Write tests” but never showed us how.
We'll start from very basics and go all the way to advanced logic and React component testing.
No theory. Just clear examples and explanations.
Unit testing means testing one small piece of logic — like a function or a component in isolation.
You don’t test everything at once — just one unit.
We will gonna use the Jest framerwork for writing unit test for set it up in project you can read the official docs here: https://jestjs.io/docs/getting-started
Let's start it with simple example,
Let's say you've a simple example of adding to numbers
// math.ts
export const add = (a: number, b: number) => a + b;If i tell you write unit test for this function
How would you approach this ?
What's coming in your mind ?
Now will say that in input only numbers are allowed and it return only numbers also.
Exactly! you're thinking right, but how do you write test for this ?
// math.test.ts
import { add } from './math';
test('adds two numbers', () => {
expect(add(2, 3)).toBe(5);
});
What happened here?
We imported add() from math.ts file
We gave it 2 and 3
We checked if the result was 5
That's all you've to do.
Congrats — you wrote your first unit test.
Now let's take another example.
Make a divide function and test its error
// divide.ts
export const divide = (a: number, b: number) => {
if (b === 0) throw new Error('Cannot divide by zero');
return a / b;
};
In this what's coming in your mind ?
only numbers can be divided.
We can't divide a number from 0.
Return must be number also.
Exactly, now how do you write the test for this ?
// divide.test.ts
import { divide } from './divide';
test('divides two numbers', () => {
expect(divide(10, 2)).toBe(5);
});
test('throws error on divide by zero', () => {
expect(() => divide(10, 0)).toThrow('Cannot divide by zero');
});In this 1st test we wrote the test for 1 & 3 condition that numbers can be divided and returned value must be number.
In 2nd test, wrote the test for the error that can come (2nd condition) that Number cant divided by 0.
That's how you write the test for this.
Let's take 2 more advance examples so that you can master the unit testing and all your doubts become clear.
You built a form. You want to test if error shows when input is wrong.
// EmailForm.tsx
import { useState } from 'react';
export function EmailForm() {
const [email, setEmail] = useState('');
const [msg, setMsg] = useState('');
const handle = () => {
if (!email.includes('@')) {
setMsg('Invalid email');
} else {
setMsg('Submitted');
}
};
return (
<>
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Enter email"
/>
<button onClick={handle}>Submit</button>
<p>{msg}</p>
</>
);
}
In this what's coming in your mind ?
Input must be a string
It must contain @
It must end with .com
If all good → return true
If not → throw error
Exactly, now how do you write the test for this ?
// emailForm.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { EmailForm } from './EmailForm';
test('shows error on invalid email', () => {
render(<EmailForm />);
fireEvent.change(screen.getByPlaceholderText('Enter email'), {
target: { value: 'gaurav' },
});
fireEvent.click(screen.getByText('Submit'));
expect(screen.getByText('Invalid email')).toBeInTheDocument();
});
In this test, what’s coming to your mind?
You entered invalid email: gaurav (no @)
You clicked Submit
The UI should show Invalid email
You’re checking if that exact text is in the DOM
This is a unit test that covers:
User typing
Validation logic
Final message showing correctly
That’s how you test form behavior without running a full app
And the last example is
// getUser.ts
export const getUser = async (id: string) => {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error('User not found');
return res.json();
};It takes a user id
Calls an API endpoint
If status is not ok → throws error
If success → returns JSON data
But here's the thing:
You don’t hit the real API. that's where mocking comes. we create a response just like coming from API so that we can prevent api for unnecessary hitting.
// getuser.test.ts
import { getUser } from './getUser';
global.fetch = jest.fn();
beforeEach(() => {
(fetch as jest.Mock).mockReset();
});
test('returns user data when API succeeds', async () => {
(fetch as jest.Mock).mockResolvedValueOnce({
ok: true,
json: async () => ({ id: '123', name: 'Gaurav' }),
});
const result = await getUser('123');
expect(result.name).toBe('Gaurav');
});
test('throws error when API fails', async () => {
(fetch as jest.Mock).mockResolvedValueOnce({ ok: false });
await expect(getUser('123')).rejects.toThrow('User not found');
});
We're not calling real API — we replaced fetch() with a fake (mock).
First test returns success JSON with user
Second test simulates a failed API and expects error
All tests are isolated, fast, and don’t depend on network
This is how you test async logic with clean mocks.
And that's how you write unit tests.
Start small.
Test one thing today.
Don’t aim for 100% coverage. Aim for 100% confidence.
If a bug could have been caught with a 5-line test, write the test.
You’ll thank yourself later.
0
8
0