Unit Testing
What it is
Section titled “What it is”A unit test verifies the smallest piece of behaviour in isolation — a function, a method, a class. It runs in memory, with all collaborators replaced by test doubles (stubs, mocks, spies), so a failure points at exactly one unit.
┌─────────────┐ │ Unit under │ ← only this is real │ test │ └──────┬──────┘ stub ───┤├─── mock ← collaborators are fakeThe defining properties: fast (milliseconds), deterministic (no clock, network or disk), isolated (no shared state between tests), and focused (one reason to fail).
When to use it / when to avoid it
Section titled “When to use it / when to avoid it”Use it for
- Pure logic: calculations, parsers, formatters, state reducers, validators
- Boundary conditions and edge cases (
0, empty, max, unicode) - Branches and error paths you can reach cheaply
Avoid or reduce it when
- The unit is thin glue over a framework — you would be testing the framework
- Everything is mocked away, so the test still passes when the wiring is broken
- Behaviour is really about several units cooperating → use an integration test
Frameworks & tools
Section titled “Frameworks & tools”| Framework | Ecosystem | Notes |
|---|---|---|
| Vitest | JS/TS | Vite-native, Jest-compatible API, built-in coverage |
| Jest | JS/TS | Most widely used; large plugin ecosystem |
| Mocha + Chai/Sinon | JS/TS | Flexible, unopinionated runner |
| pytest | Python | Fixtures, parametrise, huge plugin ecosystem |
| unittest | Python | Standard library, no dependencies |
Go testing (go test) |
Go | Built in; table-driven tests are idiomatic |
| Testify | Go | Assertions and mocks on top of go test |
| JUnit 5 | Java/JVM | The JVM standard; parameterized tests |
| TestNG | Java/JVM | Suites, groups, parallel execution |
| xUnit | .NET | Modern, [Fact]/[Theory] |
| NUnit | .NET | Mature, data-driven [TestCase] |
Rust #[test] |
Rust | Built in via cargo test |
| PHPUnit | PHP | The PHP standard |
Mocking/doubling: Sinon, Jest mocks, unittest.mock, mockito (Java), Moq/NSubstitute (.NET), testify/mock (Go), mockall (Rust).
Quick setup
Section titled “Quick setup”npm install -D vitestnpx vitest run # or `npx vitest` in watch mode// vite.config.ts (optional)import { defineConfig } from 'vite';export default defineConfig({ test: { environment: 'node' } });pip install pytestpytest -qgo test ./...<!-- Maven: JUnit 5 + Surefire --><dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>5.11.0</version> <scope>test</scope></dependency>dotnet new xunit -o MyApp.Testsdotnet testExample
Section titled “Example”import { describe, it, expect, vi } from 'vitest';import { calculateDiscount } from './discount';
describe('calculateDiscount', () => { it('applies 10% for orders over 100', () => { expect(calculateDiscount(200)).toBe(20); });
it('applies no discount at the boundary', () => { expect(calculateDiscount(100)).toBe(0); });
it('does not call the logger on the happy path', () => { const log = vi.fn(); calculateDiscount(200, log); expect(log).not.toHaveBeenCalled(); });});import pytestfrom discount import calculate_discount
@pytest.mark.parametrize( ("total", "expected"), [(200, 20), (100, 0), (0, 0)],)def test_calculate_discount(total, expected): assert calculate_discount(total) == expectedfunc TestCalculateDiscount(t *testing.T) { tests := []struct { name string total int want int }{ {"over threshold", 200, 20}, {"at boundary", 100, 0}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { if got := CalculateDiscount(tc.total); got != tc.want { t.Fatalf("want %d, got %d", tc.want, got) } }) }}import static org.junit.jupiter.api.Assertions.assertEquals;import org.junit.jupiter.params.ParameterizedTest;import org.junit.jupiter.params.provider.CsvSource;
class DiscountTest { @ParameterizedTest @CsvSource({"200,20", "100,0", "0,0"}) void appliesDiscount(int total, int expected) { assertEquals(expected, Discount.calculate(total)); }}using Xunit;
public class DiscountTests{ [Theory] [InlineData(200, 20)] [InlineData(100, 0)] [InlineData(0, 0)] public void AppliesDiscount(int total, int expected) { Assert.Equal(expected, Discount.Calculate(total)); }}Anti-patterns & best practices
Section titled “Anti-patterns & best practices”- Test behaviour, not implementation. Assert on outputs and observable state, not on how many times a private helper ran.
- One main reason to fail per test. Name it after the behaviour
(
applies 10% for orders over 100), not the method. - Avoid over-mocking. If a test needs five mocks to run, the unit is too coupled.
- No shared mutable fixtures. Tests must pass in any order and in parallel.
- Cover boundaries. Off-by-one bugs live at
0,1,max,empty,null. - Do not chase 100% coverage. Coverage shows what ran, not what was verified — use mutation testing for that.
Self-assessment quiz
Section titled “Self-assessment quiz”Self-assessment quiz
Pick one answer per question, then press Check answers.