Skip to content
Unit Testing: Complete Guide with Examples and Best Practices

Unit Testing: Complete Guide with Examples and Best Practices

DodaTech Updated Jun 20, 2026 7 min read

Unit testing is the practice of verifying individual units of source code — functions, methods, or classes — in isolation from the rest of the system to ensure each unit behaves correctly under all expected conditions.

What You’ll Learn

  • What defines a unit test and how it differs from other test types
  • How to write effective unit tests using Jest, pytest, and JUnit
  • Mocking strategies for external dependencies
  • Code coverage metrics and how to use them wisely
  • Common unit testing anti-patterns to avoid

Why Unit Testing Matters

Unit tests catch 60-70% of defects at the earliest possible stage — during development, before code reaches code review, integration testing, or production. They also serve as executable documentation, enable safe refactoring, and provide rapid feedback. A well-maintained unit test suite pays for itself many times over by reducing debugging time and preventing regressions.

DodaZIP relies on unit tests for every compression and decompression algorithm — a single off-by-one error in a buffer calculation could corrupt archives.

Learning Path

    flowchart LR
  A[TDD] --> B[Unit Testing<br/>You are here]
  B --> C[Integration Testing]
  C --> D[Mocking & Stubs]
  D --> E[CI/CD Pipeline]
  style B fill:#f90,color:#fff
  

What Makes a Good Unit Test

A good unit test follows the FIRST principles:

PrincipleMeaning
FastRuns in milliseconds
IsolatedNo network, database, or filesystem
RepeatableSame result every run
Self-validatingPass/fail, no manual inspection
TimelyWritten before or alongside the code

Each test should verify one specific behavior and have a descriptive name that explains what it tests and what the expected outcome is.

Unit Test Structure: AAA Pattern

Every unit test follows three phases:

# Arrange-Act-Assert pattern
def test_withdraw_reduces_balance():
    # Arrange
    account = BankAccount(balance=100)

    # Act
    account.withdraw(30)

    # Assert
    assert account.balance == 70

Arrange sets up the test data and objects. Act performs the operation being tested. Assert verifies the result matches expectations.

Example: Testing a Password Validator

# password_validator.py
import re

class PasswordValidator:
    def validate(self, password: str) -> dict:
        errors = []
        if len(password) < 8:
            errors.append("Must be at least 8 characters")
        if not re.search(r"[A-Z]", password):
            errors.append("Must contain an uppercase letter")
        if not re.search(r"[0-9]", password):
            errors.append("Must contain a number")
        if not re.search(r"[!@#$%^&*]", password):
            errors.append("Must contain a special character")
        return {"valid": len(errors) == 0, "errors": errors}

# test_password_validator.py
from password_validator import PasswordValidator

def test_valid_password_passes():
    validator = PasswordValidator()
    result = validator.validate("Secure1@pass")
    assert result["valid"] is True
    assert result["errors"] == []

def test_short_password_fails():
    validator = PasswordValidator()
    result = validator.validate("Ab1@")
    assert result["valid"] is False
    assert "8 characters" in result["errors"][0]

def test_missing_uppercase_fails():
    validator = PasswordValidator()
    result = validator.validate("secure1@pass")
    assert result["valid"] is False
    assert "uppercase" in result["errors"][0]

def test_missing_number_fails():
    validator = PasswordValidator()
    result = validator.validate("Secure@pass")
    assert "number" in result["errors"][0]

Expected output:

4 passed in 0.03s

Mocking External Dependencies

Mocking replaces real dependencies with controlled substitutes. Use mocks for code that touches the network, filesystem, database, or other slow or non-deterministic resources.

# user_service.py
class UserService:
    def __init__(self, db, email_service):
        self.db = db
        self.email_service = email_service

    def register(self, email, password):
        existing = self.db.find_user(email)
        if existing:
            raise ValueError("Email already registered")
        user = self.db.create_user(email, password)
        self.email_service.send_welcome(user)
        return user

# test_user_service.py
from unittest.mock import Mock
from user_service import UserService

def test_register_creates_user_and_sends_email():
    db = Mock()
    db.find_user.return_value = None
    db.create_user.return_value = Mock(id=1, email="test@example.com")
    email_service = Mock()

    service = UserService(db, email_service)
    user = service.register("test@example.com", "Secure1@pass")

    assert user.id == 1
    db.create_user.assert_called_once()
    email_service.send_welcome.assert_called_once_with(user)

def test_register_rejects_duplicate_email():
    db = Mock()
    db.find_user.return_value = Mock(id=1)
    email_service = Mock()

    service = UserService(db, email_service)
    try:
        service.register("test@example.com", "Secure1@pass")
        assert False, "Should have raised"
    except ValueError as e:
        assert "already registered" in str(e)

What Not to Mock

Avoid mocking value objects, data classes, or simple utilities. Only mock dependencies that cross a boundary (network, I/O, third-party services).

Code Coverage

Code coverage measures which lines of code are executed during tests. Common metrics:

MetricWhat It Measures
Line coveragePercentage of lines executed
Branch coveragePercentage of decision points (if/else) tested
Function coveragePercentage of functions called
Statement coveragePercentage of statements executed
# pytest with coverage
pytest --cov=myapp --cov-report=term-missing

# Output
Name                 Stmts   Miss  Cover   Missing
--------------------------------------------------
myapp/validator.py      25      2    92%   34-35
myapp/service.py        48      5    90%   67-70, 82
--------------------------------------------------
TOTAL                   73      7    90%

Coverage Traps

  • 100% coverage doesn’t mean bug-free — it means every line runs, not that every scenario is tested
  • Chasing coverage targets leads to tests that assert trivial things just to hit the number
  • Untested error paths — coverage tools show lines executed, not edge cases covered

Focus coverage efforts on critical business logic. For most projects, 80-90% line coverage on core modules is a healthy target.

Unit Testing in JavaScript with Jest

// utils.js
function calculateDiscount(price, isMember) {
  if (price <= 0) throw new Error('Invalid price');
  if (isMember) return price * 0.9;
  return price;
}

module.exports = { calculateDiscount };

// utils.test.js
const { calculateDiscount } = require('./utils');

describe('calculateDiscount', () => {
  test('applies 10% discount for members', () => {
    expect(calculateDiscount(100, true)).toBe(90);
  });

  test('no discount for non-members', () => {
    expect(calculateDiscount(100, false)).toBe(100);
  });

  test('throws for invalid price', () => {
    expect(() => calculateDiscount(0, true)).toThrow('Invalid price');
    expect(() => calculateDiscount(-5, false)).toThrow('Invalid price');
  });

  test('handles zero after discount', () => {
    expect(calculateDiscount(0.01, true)).toBeCloseTo(0.009);
  });
});

Common Unit Testing Mistakes

1. Testing Implementation Details

Testing private methods or internal state makes tests brittle — they break when behavior hasn’t changed.

Fix: Test public APIs and observable behavior only.

2. Sharing Mutable State Between Tests

Tests that modify shared state (class variables, global config) interfere with each other.

Fix: Create fresh instances in each test. Use setup methods to reduce duplication.

3. Over-Mocking

Mocks produce false confidence — they verify the mock behaves as expected, not the real system.

Fix: Mock only at system boundaries. Prefer real objects for in-process dependencies.

4. Flaky Tests

Tests that pass sometimes and fail sometimes erode trust in the suite.

Fix: Eliminate randomness, time dependencies, and test ordering dependencies.

5. Testing Through the UI

Unit tests should not require a browser or render components.

Fix: Test business logic separately from UI rendering.

6. Giant Test Classes

Single test files with thousands of lines are hard to navigate and maintain.

Fix: One test file per module or class. Keep tests focused and organized.

7. Low Signal-to-Noise Ratio

Tests that repeat the same setup code make it hard to see what each test actually verifies.

Fix: Use descriptive test names and helper functions for common setup.

Practice Questions

1. What does FIRST stand for in unit testing?

Fast, Isolated, Repeatable, Self-validating, Timely.

2. What are the three phases of the AAA pattern?

Arrange (set up), Act (perform operation), Assert (verify result).

3. When should you use mocking?

For dependencies that cross system boundaries — network calls, database queries, filesystem access, third-party APIs.

4. Why is 100% code coverage not a guarantee of quality?

Coverage measures which lines execute, not which behaviors are verified. You can have 100% coverage with zero meaningful assertions.

5. How do you handle tests that share common setup?

Use setup fixtures (pytest fixtures, Jest beforeEach/describe blocks) to reduce duplication while keeping each test isolated.

Challenge: Take a legacy module with no tests. Write unit tests that cover its core behavior without refactoring the module first. Document what makes the module hard to test.

FAQ

What is the difference between a unit test and an integration test?
Unit tests verify a single unit in isolation with mocked dependencies. Integration tests verify that multiple units work together with real or near-real dependencies.
How many unit tests should I write?
Enough to cover all behaviors of each unit. For most functions, that means 3-7 tests: happy path, edge cases, and error conditions.
Should I test private methods?
No. Test the public methods that use private methods. If a private method has complex logic, extract it into its own module.
How long should unit tests take to run?
Unittests should complete in seconds. If your suite takes minutes, you have integration tests masquerading as unit tests, or you’re hitting external resources.
Do I need 100% branch coverage?
Not necessarily. Focus on critical business logic and complex algorithms. Simple getters, setters, and delegation methods don’t need tests.

What’s Next

TutorialWhat You’ll Learn
Integration Testing StrategiesTesting components that work together
Mocking and Stubbing GuideAdvanced mocking techniques
Test Automation FrameworksComparing test frameworks

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro. Updated 2026-06-20.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro