Unit Testing Guide — Best Practices & Examples (2026)
In this tutorial, you'll learn about Unit Testing Guidesting" >}} Guide. We cover key concepts, practical examples, and best practices.
Unit testing is the practice of verifying the smallest isolatable piece of code — a single function, method, or module — in isolation from the rest of the system, ensuring each unit behaves correctly under all conditions.
What You'll Learn
You'll master the Arrange-Act-Assert pattern, learn test naming conventions that document intent, understand what to test (and what not to), mock dependencies correctly, handle edge cases, and organize test suites for maintainability.
Why Unit Testing Matters
Without unit tests, every refactor is blind. You change one line and hope nothing breaks. In production, that hope costs money. At DodaTech, Durga Antivirus Pro runs thousands of unit tests per commit — catching signature parsing errors before they reach users.
Unit Testing Learning Path
flowchart LR
A[Testing Basics] --> B[Unit Testing Guide]
B --> C[Test-Driven Development]
B --> D[Code Coverage]
B --> E{Mocking}
style B fill:#f90,color:#fff
The AAA Pattern
Every unit test follows three phases: Arrange, Act, Assert.
// Jest example — testing a discount calculator
function calculateDiscount(price, code) {
if (code === 'SAVE10') return price * 0.9;
if (code === 'SAVE20') return price * 0.8;
return price;
}
test('applies 10% discount with SAVE10 code', () => {
// Arrange
const price = 100;
const code = 'SAVE10';
// Act
const result = calculateDiscount(price, code);
// Assert
expect(result).toBe(90);
});
Expected output:
PASS ./discount.test.js
✓ applies 10% discount with SAVE10 code (2 ms)
Test Naming Conventions
A test name should read like a sentence: "[Unit] should [expected behavior] when [condition]."
// Good names — document intent
test('rejects order when inventory is insufficient', () => {});
test('returns cached result when TTL has not expired', () => {});
test('throws ValidationError when email format is invalid', () => {});
// Bad names — vague and useless
test('test function 1', () => {});
test('order test', () => {});
test('check email', () => {});
What to Test
Test public behavior, not private implementation.
# Python unittest example
import unittest
class Calculator:
def add(self, a, b):
return a + b
def divide(self, a, b):
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
class TestCalculator(unittest.TestCase):
def setUp(self):
self.calc = Calculator()
def test_add_returns_sum_of_two_numbers(self):
result = self.calc.add(3, 5)
self.assertEqual(result, 8)
def test_divide_returns_quotient(self):
result = self.calc.divide(10, 2)
self.assertEqual(result, 5)
def test_divide_raises_error_when_dividing_by_zero(self):
with self.assertRaises(ValueError):
self.calc.divide(10, 0)
Expected output:
...
----------------------------------------------------------------------
Ran 3 tests in 0.001s
OK
FIRST Principles
Good unit tests follow FIRST:
| Principle | Meaning |
|---|---|
| Fast | Run in milliseconds |
| Isolated | No shared state or dependencies |
| Repeatable | Same result every run |
| Self-validating | Pass or fail, no manual inspection |
| Timely | Written before or alongside code |
Coverage vs Quality
100% coverage does not mean 100% bug-free. It means every line executed, not every scenario verified.
// High coverage, low quality test
test('add function works', () => {
expect(add(1, 2)).toBe(3);
});
// Line coverage: 100%, but edge cases: 0%
A better approach: focus coverage on critical paths and edge cases. 80-90% on core modules beats 100% on trivial code.
Testing Edge Cases
Edge cases are where bugs hide. Always test boundaries.
function processArray(items) {
if (!items || !items.length) return [];
return items.map(item => item.toUpperCase());
}
test('returns empty array for null input', () => {
expect(processArray(null)).toEqual([]);
});
test('returns empty array for undefined input', () => {
expect(processArray(undefined)).toEqual([]);
});
test('returns empty array for empty array', () => {
expect(processArray([])).toEqual([]);
});
test('transforms single element array', () => {
expect(processArray(['a'])).toEqual(['A']);
});
test('transforms multiple elements', () => {
expect(processArray(['a', 'b', 'c'])).toEqual(['A', 'B', 'C']);
});
Expected output:
PASS ./processArray.test.js
✓ returns empty array for null input (1 ms)
✓ returns empty array for undefined input (1 ms)
✓ returns empty array for empty array (1 ms)
✓ transforms single element array (1 ms)
✓ transforms multiple elements (1 ms)
Testing Private Methods
Don't test private methods directly. If a private method has complex logic, extract it into its own module where it becomes a public API.
// Instead of testing _parseConfig directly, test the public method
class ConfigLoader {
load(path) {
const raw = this._readFile(path);
return this._parseConfig(raw);
}
_readFile(path) { /* ... */ }
_parseConfig(raw) { /* complex logic */ }
}
// Test through the public API
test('load returns parsed config for valid file', () => {
const loader = new ConfigLoader();
// Mock only the external boundary
jest.spyOn(loader, '_readFile').mockReturnValue('{"key": "value"}');
const result = loader.load('/path/to/config.json');
expect(result.key).toBe('value');
});
Organizing Test Suites
Group related tests with describe blocks. Keep test files close to the code they test.
src/
├── services/
│ ├── paymentService.js
│ ├── paymentService.test.js
│ ├── userService.js
│ └── userService.test.js
├── utils/
│ ├── formatter.js
│ └── formatter.test.js
Common Mistakes
1. Testing Implementation Details
Tests that break on refactor without behavior change are testing how, not what.
2. Flaky Tests
Random failures from shared mutable state, timeouts, or test order dependency erode trust.
3. Mocking Everything
Testing against mocks of mocks verifies nothing real. Mock only external boundaries.
4. No Edge Case Tests
Happy-path-only tests miss nulls, empties, errors, and boundaries where real bugs live.
5. Slow Test Suites
Unit tests that touch the database or network aren't unit tests. Keep them fast.
6. Giant Test Files
A single test file with 500+ lines becomes unmaintainable. Split by module.
7. Ignoring Failed Tests
Red builds become normal. Fix failures immediately or disable the test.
Practice Questions
1. What are the three phases of a unit test? Arrange (set up), Act (execute), Assert (verify). This is the AAA pattern.
2. How do you test edge cases effectively? Test null/undefined inputs, empty collections, boundary values, error conditions, and invalid data shapes.
3. Should you test private methods? No. Test through the public API. If private logic is complex, extract it into its own module.
4. What is a flaky test and why is it dangerous? A test that passes and fails without code changes. It erodes team trust in the test suite.
5. Challenge: Write a test suite for a password strength checker.
Implement a function checkStrength(password) that returns 'weak', 'medium', or 'strong' based on length and character variety. Test all three levels plus edge cases.
Mini Project: Test Runner for a Validation Library
// validators.js
function isEmail(value) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
}
function isUrl(value) {
try {
new URL(value);
return true;
} catch {
return false;
}
}
function isInRange(value, min, max) {
return typeof value === 'number' && value >= min && value <= max;
}
module.exports = { isEmail, isUrl, isInRange };
// validators.test.js
const { isEmail, isUrl, isInRange } = require('./validators');
describe('isEmail', () => {
test('returns true for valid email', () => {
expect(isEmail('user@example.com')).toBe(true);
});
test('returns false for email without @', () => {
expect(isEmail('userexample.com')).toBe(false);
});
test('returns false for empty string', () => {
expect(isEmail('')).toBe(false);
});
});
describe('isUrl', () => {
test('returns true for valid URL', () => {
expect(isUrl('https://example.com')).toBe(true);
});
test('returns false for random text', () => {
expect(isUrl('not a url')).toBe(false);
});
});
describe('isInRange', () => {
test('returns true when value is within range', () => {
expect(isInRange(5, 1, 10)).toBe(true);
});
test('returns false when value is below range', () => {
expect(isInRange(0, 1, 10)).toBe(false);
});
test('returns false for non-numeric value', () => {
expect(isInRange('5', 1, 10)).toBe(false);
});
});
FAQ
What's Next
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro