Episode 3 — NodeJS MongoDB Backend Architecture / 3.18 — Testing Tools
3.18 — Testing Tools: Quick Revision
Compact cheat sheet. Print-friendly.
How to Use This Material
- Skim before an exam or interview. This page covers every key concept from 3.18.a through 3.18.d in condensed form.
- Use the vocabulary table to verify you know every term. Cover the definition column and quiz yourself.
- Use the code cheat sheets as quick reference when writing tests. Copy patterns, not prose.
- Print this page (or save as PDF) for offline review. It is designed to fit on a few pages.
Core Vocabulary
| Term | Definition |
|---|---|
| Unit Test | Tests a single function/module in isolation; all dependencies are mocked |
| Integration Test | Tests multiple components working together (e.g., API endpoint + database) |
| E2E Test | Tests full user workflows through a real browser (Cypress, Playwright) |
| Test Suite | A group of related tests, created with describe() |
| Test Case | A single test verifying one behavior, created with it() or test() |
| Assertion | A statement that checks an expected outcome: expect(x).toBe(y) |
| Mock | A fake implementation replacing a real dependency (jest.mock()) |
| Stub | A mock with pre-programmed return values (mockFn.mockReturnValue(42)) |
| Spy | Wraps a real method to track calls without changing behavior (jest.spyOn()) |
| Fixture | Pre-defined static test data (JSON files, seed objects) |
| SUT | System Under Test -- the specific function/class being tested |
| AAA Pattern | Arrange (setup) - Act (execute) - Assert (verify) |
| TDD | Test-Driven Development: write failing test first, then code to pass it |
| Coverage | Percentage of source code executed during tests (statements, branches, functions, lines) |
| Flaky Test | A test that passes and fails non-deterministically without code changes |
| Regression Test | A test written to ensure a specific fixed bug does not reappear |
| Smoke Test | Quick check that critical features work after a deployment |
| Test Isolation | Each test is independent; no shared mutable state between tests |
| Supertest | HTTP assertion library for testing Express apps without a live server |
| mongodb-memory-server | Spins up an in-memory MongoDB instance for fast, isolated test databases |
Jest Cheat Sheet
Structure: describe / it / expect
describe('ModuleName', () => {
beforeAll(() => { /* once before all tests */ });
afterAll(() => { /* once after all tests */ });
beforeEach(() => { /* before each test */ });
afterEach(() => { /* after each test */ });
it('should do something', () => {
expect(actual).toBe(expected);
});
describe('methodName', () => {
it('handles normal case', () => { /* ... */ });
it('handles edge case', () => { /* ... */ });
it('throws on invalid input', () => {
expect(() => fn(null)).toThrow('Invalid');
});
});
});
Execution Order
beforeAll
beforeEach → test 1 → afterEach
beforeEach → test 2 → afterEach
beforeEach → test 3 → afterEach
afterAll
Common Matchers
EQUALITY
.toBe(value) Strict === (primitives)
.toEqual(value) Deep equality (objects, arrays)
.toStrictEqual(value) Deep + checks undefined props
TRUTHINESS
.toBeTruthy() Not false/0/''/null/undefined/NaN
.toBeFalsy() Is false/0/''/null/undefined/NaN
.toBeNull() === null
.toBeUndefined() === undefined
.toBeDefined() !== undefined
NUMBERS
.toBeGreaterThan(n) > n
.toBeLessThan(n) < n
.toBeCloseTo(n) Float-safe equality
STRINGS
.toMatch(/regex/) Regex match
.toMatch('substring') Substring match
ARRAYS
.toContain(item) Includes (===)
.toContainEqual(item) Includes (deep equal)
.toHaveLength(n) Length check
OBJECTS
.toHaveProperty(key, val) Property exists (+ optional value)
.toMatchObject(subset) Partial deep match
ERRORS
.toThrow() Throws any error
.toThrow('message') Throws with message
.toThrow(ErrorClass) Throws specific type
NEGATION
.not.toBe(x) Negate any matcher
Mocking Patterns
CREATE MOCKS
jest.fn() Standalone mock function
jest.mock('./module') Auto-mock entire module
jest.mock('./mod', () => ({...})) Module mock with factory
jest.spyOn(obj, 'method') Spy on existing method
CONFIGURE RETURN VALUES
mock.mockReturnValue(val) Always return val
mock.mockReturnValueOnce(val) Return val once
mock.mockResolvedValue(val) Async: resolve with val
mock.mockRejectedValue(err) Async: reject with err
mock.mockImplementation(fn) Custom implementation
VERIFY CALLS
expect(mock).toHaveBeenCalled()
expect(mock).toHaveBeenCalledTimes(n)
expect(mock).toHaveBeenCalledWith(arg1, arg2)
expect(mock).toHaveBeenLastCalledWith(arg)
expect(mock).not.toHaveBeenCalled()
mock.mock.calls Array of all call arguments
mock.mock.results Array of all return values
CLEANUP (use in afterEach)
jest.clearAllMocks() Clear call history
jest.resetAllMocks() Clear + remove implementations
jest.restoreAllMocks() Restore original methods (spyOn)
Async Testing
// async/await (recommended)
test('success', async () => {
const data = await fetchData();
expect(data).toBe('value');
});
test('error', async () => {
await expect(fetchBad()).rejects.toThrow('Error');
});
// .resolves / .rejects
test('resolves', () => {
return expect(fetchData()).resolves.toBe('value');
});
Supertest Cheat Sheet
Setup: Separate app.js from server.js
// app.js — export app, NO listen()
const app = express();
module.exports = app;
// server.js — production only
const app = require('./app');
app.listen(3000);
Request Patterns
const request = require('supertest');
const app = require('./app');
// GET with query params
await request(app)
.get('/api/users')
.query({ page: 1, limit: 10 })
.expect(200)
.expect('Content-Type', /json/);
// POST with JSON body
await request(app)
.post('/api/users')
.send({ name: 'Alice', email: 'alice@test.com' })
.expect(201);
// PUT / PATCH / DELETE
await request(app).put('/api/users/123').send({ name: 'Updated' }).expect(200);
await request(app).patch('/api/users/123').send({ name: 'Patched' }).expect(200);
await request(app).delete('/api/users/123').expect(200);
Auth Headers
await request(app)
.get('/api/protected')
.set('Authorization', `Bearer ${token}`)
.expect(200);
File Upload
await request(app)
.post('/api/upload')
.attach('file', filePath)
.field('description', 'photo')
.expect(200);
Common Assertions
const res = await request(app).get('/api/users');
expect(res.status).toBe(200);
expect(res.body.data).toHaveLength(3);
expect(res.body.data[0]).toHaveProperty('name');
expect(res.body.data[0]).not.toHaveProperty('password');
Test Database Setup (mongodb-memory-server)
const mongoose = require('mongoose');
const { MongoMemoryServer } = require('mongodb-memory-server');
let mongoServer;
beforeAll(async () => {
mongoServer = await MongoMemoryServer.create();
await mongoose.connect(mongoServer.getUri());
});
afterEach(async () => {
for (const key in mongoose.connection.collections)
await mongoose.connection.collections[key].deleteMany({});
});
afterAll(async () => {
await mongoose.connection.close();
await mongoServer.stop();
});
Cypress vs Playwright Comparison
| Feature | Cypress | Playwright |
|---|---|---|
| Browsers | Chromium, Firefox, Edge | Chromium, Firefox, WebKit (Safari) |
| Languages | JavaScript / TypeScript only | JS, TS, Python, Java, C# |
| Architecture | Runs inside the browser | Controls browser from outside (CDP) |
| Multi-tab | Not supported | Full support (browser contexts) |
| Test isolation | Manual (clear cookies/storage) | Automatic (fresh context per test) |
| Auto-waiting | Implicit in assertions | Implicit in actions and assertions |
| Network mocking | cy.intercept() | page.route() |
| Parallel | Paid (Cypress Cloud) | Free, built-in sharding |
| Debugging | Time-travel debugger (excellent) | Trace viewer, VS Code extension |
| Codegen | Cypress Studio (limited) | npx playwright codegen (powerful) |
| Best for | Single-browser, rapid dev, DX | Multi-browser, complex apps, CI |
Syntax Comparison
CYPRESS PLAYWRIGHT
cy.visit('/login') await page.goto('/login')
cy.get('[data-testid="email"]') page.locator('[data-testid="email"]')
.type('user@test.com') .fill('user@test.com')
cy.get('[data-testid="btn"]') await page.click('[data-testid="btn"]')
.click()
cy.url() await expect(page)
.should('include', '/dashboard') .toHaveURL(/dashboard/)
cy.contains('Welcome') await expect(page.locator('text=Welcome'))
.should('be.visible') .toBeVisible()
NETWORK INTERCEPTION
cy.intercept('GET', '/api/users', { await page.route('**/api/users',
body: { data: [...] } route => route.fulfill({
}).as('getUsers') body: JSON.stringify({ data: [...] })
cy.wait('@getUsers') }))
Testing Pyramid
/\
/ \
/ E2E \ ← 5-10% | Slow | Expensive | Browser
/--------\ Cypress, Playwright
/Integration\ ← 15-25% | Med | Medium | API + DB
/--------------\ Supertest + mongodb-memory-server
/ Unit Tests \ ← 65-80% | Fast | Cheap | Functions
/____________________\ Jest with mocks
MORE TESTS ←──────────────────────────────→ FEWER TESTS
FASTER ←──────────────────────────────→ SLOWER
CHEAPER ←──────────────────────────────→ MORE EXPENSIVE
LESS REALISTIC ←─────────────────────────→ MORE REALISTIC
CI/CD Testing Checklist
LOCAL DEVELOPMENT
[ ] Jest in watch mode (re-runs on save)
[ ] ESLint + Prettier on save
[ ] Pre-commit hook: lint staged files
PULL REQUEST (GitHub Actions)
[ ] Stage 1: Lint + type check (~30s)
[ ] Stage 2: Unit tests + coverage enforcement (~1-2min)
[ ] Stage 3: Integration tests with test DB (~3-5min)
[ ] Stage 4: E2E tests, Chromium only (~5min)
[ ] Coverage threshold: 80% lines, 80% branches
[ ] Upload coverage report as PR comment
[ ] Upload E2E artifacts (screenshots, videos)
MERGE TO MAIN
[ ] Full unit + integration suite
[ ] E2E tests on ALL browsers (Chromium + Firefox + WebKit)
[ ] Visual regression tests
[ ] Deploy to staging
POST-DEPLOYMENT
[ ] Smoke tests against staging
[ ] Smoke tests against production
[ ] Synthetic monitoring (scheduled E2E, every 5min)
[ ] Error rate monitoring (Sentry, Datadog)
NIGHTLY / WEEKLY
[ ] Full E2E suite on all browsers + mobile viewports
[ ] Load testing (Artillery or k6)
[ ] Visual regression comparison
[ ] Security scanning
Minimal GitHub Actions Workflow
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20, cache: npm }
- run: npm ci
- run: npm run lint
- run: npx jest --coverage
- run: npx playwright install --with-deps
- run: npx playwright test
Coverage Quick Reference
GENERATE REPORT
npx jest --coverage
FOUR METRICS
Statements % of executable statements run
Branches % of if/else/switch/ternary paths taken ← MOST IMPORTANT
Functions % of declared functions called
Lines % of code lines executed
PRACTICAL TARGETS
90-100% Excellent (beware of testing trivial code)
80-90% Very good (recommended target for most projects)
60-80% Acceptable (retroactive testing)
Below 60% Significant risk
ENFORCE IN jest.config.js
coverageThreshold: {
global: { branches: 80, functions: 80, lines: 80, statements: 80 }
}
REMEMBER
Coverage ≠ correctness (tests can cover lines without meaningful assertions)
100% coverage ≠ zero bugs (logic errors, missing edge cases, weak assertions)
Branch coverage reveals untested error-handling paths
Common Mistakes to Avoid
| Mistake | Fix |
|---|---|
| Not cleaning up between tests | afterEach: clear DB, jest.clearAllMocks() |
| Tests depend on execution order | Each test sets up its own state independently |
| No assertions (just calling code) | Every test() must have at least one expect() |
Committing it.only / describe.only | Use eslint-plugin-jest rule no-focused-tests |
Using sleep() in E2E tests | Use condition-based waits: waitForResponse, waitForSelector |
| Selectors tied to CSS classes | Use data-testid attributes for stability |
| Mocking what you do not own | Wrap third-party APIs in your own module, mock that module |
| Testing private/internal functions | Test through the public API |
| 100% coverage as the goal | Focus on meaningful assertions for critical paths |
| Ignoring flaky tests | Fix root cause immediately; quarantine as last resort |
End of 3.18 quick revision.