How to Write Unit Tests in JavaScript/TypeScript Using Jest

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.


What is Unit Testing?

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’ll use the Jest framework for writing unit tests.
To set it up in your project, follow the Jest getting started guide.


Let's Start with a Simple Example

Let’s say you have a function to add two numbers:

// math.ts
export const add = (a: number, b: number) => a + b;

If I tell you to write a unit test for this —
How would you approach it?

What’s coming in your mind?

  • Only numbers are allowed as input
  • It must return a number
  • If 2 + 3 → result should be 5

Exactly. You’re thinking right.
But how do you write the 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
  • Gave it 2 and 3
  • Checked if the result was 5

That’s it.

Congrats — you just wrote your first unit test.


Let's Test a Divide Function

Let’s build a divide() function and test its logic + error case.

// divide.ts
export const divide = (a: number, b: number) => {
  if (b === 0) throw new Error('Cannot divide by zero');
  return a / b;
};

What’s coming in your mind?

  • Only numbers can be divided
  • We can’t divide a number by 0
  • Return must be a number

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');
});

Breakdown

  • 1st test: Valid division → returns number
  • 2nd test: Checks for error when divided by 0

That’s how you test both logic and error handling.


Advance Example — Form Component Validation

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 @
  • If valid → show Submitted
  • If invalid → show Invalid email

Exactly. Now how do you test it?

// 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();
});

What’s happening here?

  • You entered an invalid email (gaurav)
  • Clicked Submit
  • UI showed Invalid email
  • Test confirmed that message exists in the DOM

You just tested user interaction + validation — all in one test.


Expert Level — Async API Call with Mock

Here’s an async function that calls an API and returns user data:

// 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();
};

What’s the logic?

  • Takes a user id
  • Calls API endpoint
  • If res.ok is false → throws error
  • Else → returns JSON

But here's the thing:

You don’t call real APIs in unit tests
You mock them using jest.fn()


// 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');
});

Breakdown

  • Replaced fetch() with a mock
  • 1st test: returns mock success data
  • 2nd test: simulates API failure and expects error

This is how you test async logic with clean mocks.


Start small.
Test one thing today.

Don’t aim for 100% test 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.


Want a Part 2?

I'll cover:

  • Testing custom hooks
  • Mocking external libraries (axios, react-query)
  • Integration testing + setup

Let me know — I’ll write it.

Until then — write tests like a builder, not a robot.

I share raw, no-fluff posts on building products, writing better code, and growing as a developer.

👉 Follow me on X