This Is a Test: A Practical Guide to Fast, Reliable Testing
If you have ever merged a quick fix and crossed your fingers, this post is for you. Below is a practical, copy paste friendly path to get tests in place fast, keep them reliable, and wire them into CI.
What you will get:
- A minimal testing stack (JS and Python examples)
- Project layout that scales
- Patterns for fixtures, factories, and deterministic tests
- API testing examples
- CI setup that runs in minutes
1) Pick a minimal stack
Choose the smallest tool that solves the problem now and can grow later.
- Node: Jest for unit and integration, Supertest for HTTP, Playwright for browser
- Python: pytest for unit and integration, httpx or requests for HTTP, Playwright for browser
- Databases: SQLite in memory for speed when possible; Postgres with Testcontainers or a local service when you need parity
Keep it boring, fast, and easy to run locally and in CI.
2) Suggested project layout
project/
src/
index.js
api/
server.js
lib/
math.js
tests/
unit/
math.test.js
integration/
api.test.js
fixtures/
factories.js
py/
app/
__init__.py
api.py
math.py
tests/
test_math.py
test_api.py
factories.py
- Keep tests close but separate
- Name tests predictably: thing.test.js or test_thing.py
- A factories module avoids ad hoc data construction
3) First test in JavaScript (Jest)
Install:
npm i -D jest
npx jest --init
Code under test:
// src/lib/math.js
function sum(a, b) {
return a + b
}
module.exports = { sum }
Test:
// tests/unit/math.test.js
const { sum } = require('../../src/lib/math')
describe('sum', () => {
it('adds two numbers', () => {
expect(sum(2, 3)).toBe(5)
})
})
Run:
npx jest
4) First test in Python (pytest)
Install:
pip install pytest
Code under test:
# py/app/math.py
def sum(a, b):
return a + b
Test:
# py/tests/test_math.py
from app.math import sum
def test_sum_adds_two_numbers():
assert sum(2, 3) == 5
Run:
pytest -q
5) Test an API fast
JavaScript with Express and Supertest:
npm i express supertest
// src/api/server.js
const express = require('express')
const app = express()
app.get('/health', (req, res) => res.status(200).json({ ok: true }))
module.exports = app
// tests/integration/api.test.js
const request = require('supertest')
const app = require('../../src/api/server')
describe('GET /health', () => {
it('returns ok', async () => {
const res = await request(app).get('/health')
expect(res.status).toBe(200)
expect(res.body).toEqual({ ok: true })
})
})
Python with FastAPI and httpx:
pip install fastapi uvicorn httpx pytest
# py/app/api.py
from fastapi import FastAPI
app = FastAPI()
@app.get('/health')
def health():
return {'ok': True}
# py/tests/test_api.py
from httpx import AsyncClient
from app.api import app
import pytest
@pytest.mark.asyncio
async def test_health():
async with AsyncClient(app=app, base_url='http://test') as ac:
res = await ac.get('/health')
assert res.status_code == 200
assert res.json() == {'ok': True}
6) Fixtures and factories
Avoid brittle hand made data. Use factories.
JavaScript example:
// tests/fixtures/factories.js
let idCounter = 1
const userFactory = (overrides = {}) => ({
id: idCounter++,
email: `user${Date.now()}_${Math.random().toString(36).slice(2)}@test.dev`,
name: 'Test User',
...overrides,
})
module.exports = { userFactory }
Python example:
# py/tests/factories.py
import time, random
def user_factory(**overrides):
base = {
'id': int(time.time() * 1000),
'email': f'user{int(time.time()*1000)}_{random.randint(0,9999)}@test.dev',
'name': 'Test User',
}
base.update(overrides)
return base
Tip: if randomness leaks into assertions, your tests will flake. Inject clocks and id generators so you can replace them in tests.
7) Make tests deterministic
- Time
- JavaScript: jest.useFakeTimers(); advanceTimersByTime
- Python: freezegun to freeze time
- Randomness
- Inject a rng function and replace with a fixed seed in tests
- Network
- Mock external HTTP with nock (JS) or responses (Python)
- Concurrency
- Avoid sleeps. Wait on signals or events. Use timeouts with clear failure messages
JavaScript fake time example:
jest.useFakeTimers()
const fn = jest.fn()
setTimeout(fn, 1000)
jest.advanceTimersByTime(1000)
expect(fn).toHaveBeenCalled()
Python freezegun example:
pip install freezegun
from freezegun import freeze_time
from datetime import datetime
@freeze_time('2024-01-01 00:00:00')
def test_time_is_frozen():
assert datetime.now().year == 2024
8) Fast feedback: run the right tests
- Use watch mode locally (jest --watch, pytest -f)
- Tag slow tests and skip them by default
- Jest: use a naming or tag convention
- pytest: use markers like @pytest.mark.slow and run with -m 'not slow'
- Parallelize: jest runs in parallel by default; pytest with -n auto via pytest xdist
pip install pytest-xdist
pytest -n auto
9) Data and databases
- Prefer pure functions where possible
- For persistence tests:
- Use SQLite in memory for speed when it matches your SQL dialect
- Else, spin up a real Postgres using Testcontainers or docker compose
- Wrap tests in transactions and roll back to keep tests isolated
Minimal docker compose for Postgres:
version: '3.9'
services:
db:
image: postgres:16
environment:
- POSTGRES_PASSWORD=pass
- POSTGRES_USER=user
- POSTGRES_DB=app
ports:
- 5432:5432
10) CI in minutes (GitHub Actions)
name: ci
on:
pull_request:
push:
branches: [ main ]
jobs:
test-js:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npx jest --ci --reporters=default --coverage
test-py:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- run: pip install -r requirements.txt || true
- run: pip install pytest
- run: pytest -q
Speed tips:
- Cache dependencies with setup actions cache inputs
- Split jobs by area (unit, integration)
- Run affected tests only using path filters or a test selection tool
11) Flaky test checklist
- Are you waiting for a timer or a fixed sleep? Replace with an event or poll with timeout
- Does the test depend on real time, rand, or global state? Inject and control them
- Is there shared data across tests? Reset between tests
- Do tests run reliably on a slow CI runner? Add generous timeouts and clear diagnostics
- External services: mock them or run local emulators
12) Day one checklist
- Unit tests run locally with one command
- A couple of integration tests cover the critical path
- Deterministic time and id generation in tests
- Factories replace ad hoc data
- CI runs tests on every PR in under 10 minutes
Ship it. The best test suite is the one you actually run every time.