Episode 3 — NodeJS MongoDB Backend Architecture / 3.18 — Testing Tools

3.18 — Testing Tools: Quick Revision

Compact cheat sheet. Print-friendly.


<< 3.18 Overview


How to Use This Material

  1. Skim before an exam or interview. This page covers every key concept from 3.18.a through 3.18.d in condensed form.
  2. Use the vocabulary table to verify you know every term. Cover the definition column and quiz yourself.
  3. Use the code cheat sheets as quick reference when writing tests. Copy patterns, not prose.
  4. Print this page (or save as PDF) for offline review. It is designed to fit on a few pages.

Core Vocabulary

TermDefinition
Unit TestTests a single function/module in isolation; all dependencies are mocked
Integration TestTests multiple components working together (e.g., API endpoint + database)
E2E TestTests full user workflows through a real browser (Cypress, Playwright)
Test SuiteA group of related tests, created with describe()
Test CaseA single test verifying one behavior, created with it() or test()
AssertionA statement that checks an expected outcome: expect(x).toBe(y)
MockA fake implementation replacing a real dependency (jest.mock())
StubA mock with pre-programmed return values (mockFn.mockReturnValue(42))
SpyWraps a real method to track calls without changing behavior (jest.spyOn())
FixturePre-defined static test data (JSON files, seed objects)
SUTSystem Under Test -- the specific function/class being tested
AAA PatternArrange (setup) - Act (execute) - Assert (verify)
TDDTest-Driven Development: write failing test first, then code to pass it
CoveragePercentage of source code executed during tests (statements, branches, functions, lines)
Flaky TestA test that passes and fails non-deterministically without code changes
Regression TestA test written to ensure a specific fixed bug does not reappear
Smoke TestQuick check that critical features work after a deployment
Test IsolationEach test is independent; no shared mutable state between tests
SupertestHTTP assertion library for testing Express apps without a live server
mongodb-memory-serverSpins 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

FeatureCypressPlaywright
BrowsersChromium, Firefox, EdgeChromium, Firefox, WebKit (Safari)
LanguagesJavaScript / TypeScript onlyJS, TS, Python, Java, C#
ArchitectureRuns inside the browserControls browser from outside (CDP)
Multi-tabNot supportedFull support (browser contexts)
Test isolationManual (clear cookies/storage)Automatic (fresh context per test)
Auto-waitingImplicit in assertionsImplicit in actions and assertions
Network mockingcy.intercept()page.route()
ParallelPaid (Cypress Cloud)Free, built-in sharding
DebuggingTime-travel debugger (excellent)Trace viewer, VS Code extension
CodegenCypress Studio (limited)npx playwright codegen (powerful)
Best forSingle-browser, rapid dev, DXMulti-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

MistakeFix
Not cleaning up between testsafterEach: clear DB, jest.clearAllMocks()
Tests depend on execution orderEach test sets up its own state independently
No assertions (just calling code)Every test() must have at least one expect()
Committing it.only / describe.onlyUse eslint-plugin-jest rule no-focused-tests
Using sleep() in E2E testsUse condition-based waits: waitForResponse, waitForSelector
Selectors tied to CSS classesUse data-testid attributes for stability
Mocking what you do not ownWrap third-party APIs in your own module, mock that module
Testing private/internal functionsTest through the public API
100% coverage as the goalFocus on meaningful assertions for critical paths
Ignoring flaky testsFix root cause immediately; quarantine as last resort

End of 3.18 quick revision.