Lesson Progress
0% Complete

Testing helps you catch bugs early, prevent regressions (old features breaking when you add new ones), and ship changes with confidence. In production-ready website development, you typically use multiple layers of tests because no single type of test covers everything well.

This topic covers:

  • What to test (and what not to)
  • How to structure tests so they stay maintainable
  • How to automate tests so they run every time you change code

1) What are we testing for?

A good test suite reduces risk in three main areas:

  • Correctness: Does the feature work as expected?
  • Stability: Do changes accidentally break existing behaviour?
  • Confidence to deploy: Can we release without “manual checking everything”?

You should prioritise tests for:

  • Business-critical journeys (login, checkout, contact form submission)
  • Complex logic (price calculations, validation rules, filtering/sorting)
  • Security-related flows (authorisation checks, session handling)
  • Areas that change often (hot spots for regressions)
  • Integrations (API calls, database operations, third-party services)

You normally avoid heavy testing for:

  • Purely visual tweaks that are better handled with visual review or visual regression tools
  • Third-party code you do not control (instead, mock it and test your integration behaviour)
  • Trivial getters/setters or framework internals

2) The testing pyramid (how to balance test types)

A common approach is the testing pyramid:

  • Many Unit tests (fast, cheap, isolated)
  • Some Integration tests (medium speed, covers connected parts)
  • Few End-to-End (E2E) tests (slower, most realistic)

This balance matters because:

  • Unit tests run quickly and give fast feedback.
  • Integration tests catch issues between modules.
  • E2E tests catch real user-flow problems but are slower and can be flaky if not designed well.

3) Unit Testing

3.1 What unit tests are

Unit tests check a small piece of code in isolation—usually a single function or component. They should be:

  • Fast (milliseconds)
  • Deterministic (same result every run)
  • Independent (no real database, no real network)

3.2 What to unit test (examples)

Unit tests are ideal for:

  • Input validation and formatting (e.g., email validation)
  • Utility functions (e.g., converting prices to currency)
  • Business rules (e.g., discount logic)
  • Component logic (e.g., “submit button disabled when form invalid”)

Example ideas

  • calculateTotal() returns correct total including VAT”
  • isStrongPassword() rejects short passwords”
  • “When isLoading=true, the button shows a spinner”

3.3 How to structure unit tests

A simple pattern is Arrange – Act – Assert (AAA):

  1. Arrange: set up inputs and initial state
  2. Act: call the function / trigger the behaviour
  3. Assert: check the output / state

Naming
Use descriptive names that read like requirements:

  • should return 0 when cart is empty
  • should show an error message when email is invalid

3.4 Mocks, stubs, and spies (when isolating code)

When the unit relies on something external, replace it with a test double:

  • Mock: a fake object with pre-set behaviour (e.g., fake API client)
  • Stub: returns fixed data (e.g., always returns a user)
  • Spy: records how something was called (e.g., “was fetch() called with this URL?”)

Guideline
Mock boundaries (network, database, time, randomness), not your own internal logic where possible—otherwise you risk “testing the mock” instead of the behaviour.


4) Integration Testing

4.1 What integration tests are

Integration tests verify that multiple parts work together correctly. Examples:

  • Front-end component + API client + mocked server response
  • Backend route + database (sometimes using a test database)
  • Authentication middleware + protected endpoints

Integration tests are usually slower than unit tests but catch more realistic problems such as:

  • Incorrect data shapes (fields missing, wrong types)
  • Broken wiring between modules
  • Mistakes in request/response handling
  • Database query and transaction issues

4.2 What to integration test (examples)

Good candidates:

  • API endpoints: request validation, response shape, status codes
  • Database interactions: saving, updating, querying, constraints
  • Auth flows: login, token refresh, access control
  • Front-end data fetching: loading/error states and rendering results

Example ideas

  • POST /api/users returns 201 and stores the user”
  • “Unauthenticated requests to /api/admin return 401/403”
  • “When API returns 500, UI shows ‘Try again’ message”

4.3 Integration test environment

To make integration tests reliable:

  • Use a separate test environment (config values for tests)
  • Use test databases or disposable containers
  • Reset state between tests (clear tables, re-seed data)
  • Avoid relying on real third-party services; use fakes or sandbox environments

5) End-to-End (E2E) Testing

5.1 What E2E tests are

E2E tests simulate real user behaviour through the full application stack:

  • Browser interaction (clicking, typing, navigation)
  • Real routing
  • Real backend (or a test version)
  • Real data flows

These tests answer: “Can the user complete the journey?”

5.2 What to E2E test (choose carefully)

Because E2E tests are slower and more expensive to maintain, focus on:

  • The most critical user journeys (happy paths)
  • A few high-risk edge cases
  • Smoke tests (quick checks that the app is basically working)

Example E2E scenarios

  • User can register → log in → view a dashboard
  • User searches for a product → adds to cart → checks out
  • User submits a contact form → sees a success message

5.3 Avoiding flaky E2E tests

Flaky tests fail randomly and damage trust in the pipeline. Reduce flakiness by:

  • Waiting for stable UI signals (e.g., element visible, request finished), not arbitrary timeouts
  • Using stable selectors (e.g., data-testid attributes) instead of brittle CSS paths
  • Running tests against predictable data (seeded database or mocked APIs where appropriate)
  • Keeping E2E flows short and focused
  • Ensuring the environment is consistent (same build, same config, same base URL)

6) Writing good tests (practical guidelines)

6.1 Test behaviour, not implementation

Prefer checking what the user or caller experiences:

  • Output values
  • Rendered text
  • Visible UI state
  • API responses and status codes

Avoid tests that are too tightly coupled to internal details (private functions, internal variables). Those tests break whenever you refactor, even if behaviour stays correct.

6.2 Make tests independent and repeatable

Each test should:

  • Set up its own state
  • Not rely on previous tests running first
  • Clean up after itself (or reset state automatically)

6.3 Keep tests readable

A test suite is part of your documentation. Keep it clear:

  • Use helper functions for repeated setup
  • Use meaningful sample data
  • Keep assertions focused (don’t assert 20 things in one test)

7) Automation: running tests to prevent regressions

7.1 When to run tests

A simple, effective strategy:

  • On every commit / pull request: unit + integration tests
  • Nightly or pre-release: full E2E suite (or a larger set)
  • Before production deploy: smoke E2E tests + key checks

7.2 Where tests run (local and CI)

You should be able to run tests:

  • Locally during development (fast feedback)
  • In CI (Continuous Integration) to enforce consistency

In CI, tests typically run:

  1. Install dependencies
  2. Lint (optional but recommended)
  3. Unit tests
  4. Integration tests
  5. Build
  6. E2E tests (either against a locally started server or a test deployment)

If tests fail, the pipeline stops—preventing broken code from being merged or deployed.


8) Quick checklist (use this before releasing changes)

  • [ ] Unit tests cover core logic and validations
  • [ ] Integration tests cover API routes, auth, and database behaviour
  • [ ] E2E tests cover the main user journey(s)
  • [ ] Tests are deterministic (no random failures)
  • [ ] External dependencies are mocked or use stable sandbox environments
  • [ ] Tests run automatically in CI on pull requests
  • [ ] Failures are easy to understand (clear test names and messages)

Key takeaway

Use unit tests for fast feedback on logic, integration tests to confirm components work together, and E2E tests to prove real user journeys function end-to-end. Automated testing in CI is what turns these checks into real protection against regressions before you release.