Frontend Development using React.js or Angular

Chapter 11
Testing, Debugging, and Code Quality

Diploma in Full Stack Programming
Frontend Development using React.js or Angular   •   Diploma

Testing, Debugging, and Code Quality

Introduction

Frontend applications are executed in an environment that combines application code, browser APIs, network services, user interaction, build tools, and third-party libraries. Consequently, a program may fail even when its individual functions appear correct. A button may not respond because an event handler is attached incorrectly; a component may render stale data because state was mutated; a production build may fail because of an environment variable; or a successful API response may be displayed incorrectly because its shape was misunderstood.

Testing and debugging are complementary professional activities. Testing provides repeatable evidence that software behaves as specified. Debugging investigates why observed behaviour differs from expected behaviour. Code-quality practices reduce the probability that defects will be introduced and make existing defects easier to locate and repair.

This chapter uses examples applicable to React.js and Angular. The principles are framework-independent, although the tools differ. React projects commonly use Jest or Vitest, React Testing Library, and Playwright or Cypress. Angular projects commonly use Jasmine or Jest, Angular TestBed, Karma, and Cypress or Playwright. Tool names change over time; the essential skill is understanding test boundaries, observable behaviour, isolation, and evidence.

Learning Objectives

By the end of this chapter, a student should be able to:

  • distinguish unit, component, integration, and end-to-end tests;
  • use browser developer tools and framework tooling to diagnose defects;
  • design useful tests for rendering, interaction, asynchronous work, and errors;
  • apply mocks, stubs, spies, fixtures, and test doubles appropriately;
  • interpret code coverage without treating it as proof of correctness;
  • apply linting, formatting, type checking, and maintainability standards;
  • conduct constructive code reviews; and
  • plan a practical quality strategy for a frontend application.

1. A Quality Model for Frontend Software

Software quality is multidimensional. Functional correctness asks whether the application produces the required result. Reliability asks whether it continues to do so under expected conditions. Maintainability concerns how safely developers can understand and change it. Accessibility, security, performance, and usability are also quality attributes.

A useful testing question is not “How do I test this implementation?” but “What observable behaviour must remain true?” For example, a checkout component should not be tested merely for the existence of a particular internal variable. It should be tested for behaviours such as displaying validation messages, preventing submission of invalid data, and showing a confirmation after a successful order.

The defect-prevention and feedback cycle

Quality is improved through a short feedback cycle:

  1. define expected behaviour;
  2. implement a small change;
  3. run static checks and automated tests;
  4. inspect the result in a browser;
  5. review the code and test evidence;
  6. deploy to a controlled environment; and
  7. monitor and improve.

Automated tests are not a substitute for exploratory testing. Automated tests repeatedly check known expectations; exploratory testing investigates risks that were not fully anticipated when tests were written.

2. Testing Levels and Test Design

Testing levels describe the scope of the system exercised by a test.

LevelMain subjectTypical speedTypical tools
UnitFunction, reducer, pipe, validator, or service in isolationVery highVitest, Jest, Jasmine
ComponentA UI component and its user-visible behaviourHighTesting Library, Angular TestBed
IntegrationSeveral modules working togetherModerateTesting Library, TestBed, MSW
End-to-endA complete user journey in a real browserLowerPlaywright, Cypress

Unit testing

A unit test isolates a small unit and verifies its contract. A pure function is especially suitable because the same input should produce the same output and no external state is required.

export function formatCurrency(amount, currency = 'USD') {
  return new Intl.NumberFormat('en-US', {
    style: 'currency', currency
  }).format(amount);
}

A useful test checks ordinary values, boundary values, and invalid or unusual values:

import { describe, it, expect } from 'vitest';
import { formatCurrency } from './formatCurrency';

describe('formatCurrency', () => {
  it('formats a positive amount', () => {
    expect(formatCurrency(12.5)).toBe('$12.50');
  });

  it('formats zero', () => {
    expect(formatCurrency(0)).toBe('$0.00');
  });
});

A unit test should have one clear reason to fail. The Arrange–Act–Assert pattern is useful: prepare inputs, perform the action, and verify the result.

Component testing

A component test renders a component and interacts with it as a user would. It may include child components, but should avoid depending on private implementation details. Queries should normally reflect accessibility and user perception: getByRole, getByLabelText, and getByText are generally more robust than selectors based on CSS classes.

render(<LoginForm onSubmit={submitHandler} />);
await user.type(screen.getByLabelText(/email/i), '[email protected]');
await user.type(screen.getByLabelText(/password/i), 'secret123');
await user.click(screen.getByRole('button', { name: /sign in/i }));
expect(submitHandler).toHaveBeenCalledWith({
  email: '[email protected]', password: 'secret123'
});

Angular component tests commonly create a fixture using TestBed, detect changes, locate elements through the rendered DOM, and dispatch user-like events. The test should verify rendered output and public interactions rather than private fields.

Integration testing

Integration tests verify collaboration between units. Examples include a component using a form validator and an HTTP service, or a router rendering the correct page after navigation. Integration tests often reveal incorrect assumptions about data shape, dependency configuration, and asynchronous timing.

A network boundary should usually be controlled. Mock Service Worker (MSW), an Angular HTTP testing controller, or an equivalent facility can intercept requests while preserving realistic request and response behaviour. This is preferable to replacing every function with a mock because it tests the interaction at a meaningful boundary.

End-to-end testing

An end-to-end test launches the application and exercises a complete scenario, such as registering, signing in, adding a product, and signing out. It provides high confidence in integration but is slower and more sensitive to environment, data, and timing. A healthy suite therefore contains many focused unit and component tests, fewer integration tests, and a small number of critical end-to-end journeys.

This distribution is sometimes called the test pyramid. It is a guiding model rather than a strict numerical rule. The correct balance depends on the risks, architecture, and cost of failure.

3. Assertions, Test Doubles, and Asynchronous Behaviour

An assertion expresses an expectation. Strong assertions verify meaningful outcomes rather than incidental implementation details. For instance, asserting that an error message is visible is usually stronger than asserting that a variable named hasError equals true.

Test doubles

A test double replaces or controls a dependency during a test.

  • A stub supplies predetermined responses.
  • A spy records calls and may preserve or replace behaviour.
  • A mock is a configured test double with expectations about interaction.
  • A fake is a lightweight working implementation, such as an in-memory repository.
  • A fixture is controlled data or setup used repeatedly in tests.

Mocks are useful at unstable or expensive boundaries, such as payment providers and remote APIs. Excessive mocking creates tests that pass even when the real collaboration is broken. Mock the boundary, not every internal function.

Asynchronous testing

Frontend code uses promises, timers, event loops, and observable streams. Tests must wait for the condition they assert. A common error is asserting immediately after starting an asynchronous operation.

it('shows a loaded product', async () => {
  render(<ProductPage productId="p1" />);
  expect(await screen.findByRole('heading', { name: /keyboard/i })).toBeVisible();
});

Tests should also cover loading, success, empty, and failure states. Fake timers can control scheduled work, but they must be used carefully: a test that advances a timer without processing pending promises may produce misleading results. Framework-specific helpers such as Angular’s fakeAsync and tick should be used consistently with the project’s asynchronous model.

4. Browser and Framework Debugging

Debugging is a disciplined process, not random modification. Begin by reproducing the defect reliably. Record the input, browser, route, account state, network conditions, and expected versus actual behaviour.

Browser developer tools

The browser’s Elements panel reveals the actual DOM, computed styles, layout, and accessibility-related attributes. The Console displays exceptions, warnings, and deliberate diagnostic output. The Network panel shows request URLs, methods, status codes, payloads, response headers, timing, and whether a request was cached. The Sources panel supports breakpoints, stepping, watch expressions, call stacks, and conditional pauses. The Application panel helps inspect storage, cookies, service workers, and cache state.

A productive debugging sequence is:

  1. read the first meaningful error, not only the final cascade of errors;
  2. inspect the stack trace and source location;
  3. check the relevant request and response in Network;
  4. pause at the boundary where data changes;
  5. inspect state, props, or injected dependencies;
  6. form a specific hypothesis;
  7. change one thing and retest; and
  8. add a regression test after fixing the defect.

Source maps connect bundled browser code to source files. They should be enabled in development, while production exposure should be considered carefully because source maps may reveal implementation details.

React debugging

React Developer Tools shows component trees, props, state, and re-rendering behaviour. Common defects include mutating state instead of creating a new value, using an unstable list key, omitting a dependency from an effect, and triggering an effect when a derived value would suffice. Strict Mode may expose unsafe side effects during development; this is a development diagnostic, not automatically a production defect.

When debugging an effect, identify its trigger, inputs, cleanup behaviour, and external side effect. An effect that subscribes to an event source must unsubscribe when the component is removed or its relevant dependency changes.

Angular debugging

Angular DevTools can inspect component structure, change detection, and performance. Common defects include incorrect template bindings, missing imports in standalone components, lifecycle assumptions, unintended subscriptions, and change-detection problems. The async pipe can simplify subscription management and automatically handle cleanup in many cases.

5. Coverage and the Testing Strategy

Code coverage measures which portions of code were executed by a test suite. Common measures include statement, branch, function, and line coverage. A simple percentage can be expressed as:

Coverage percentage = (covered items ÷ total measurable items) × 100

The items may be statements, branches, functions, or lines; the denominator must therefore be identified. Coverage is evidence of execution, not evidence that the behaviour is correct. A test can execute a branch without asserting its result, and high line coverage can coexist with poor boundary and error testing.

For example, a conditional has two branches. If tests execute only the success path, branch coverage is 1 ÷ 2 × 100 = 50%, assuming each branch is measured once and no additional branches exist. This calculation is a reporting measure, not a quality law.

Coverage targets should be risk-informed. Financial calculations, authentication, permissions, and complex state transitions deserve stronger tests than trivial generated code. Teams should investigate uncovered code rather than blindly adding assertions to increase a percentage.

6. Static Analysis and Maintainability

Static analysis examines code without running the application. It includes:

  • linting for suspicious or inconsistent patterns;
  • formatting for a consistent visual style;
  • TypeScript type checking for invalid type relationships;
  • dependency and vulnerability scanning; and
  • architectural rules such as restricted imports.

ESLint can identify unused variables, unsafe patterns, accessibility problems, and framework-specific errors. Prettier can standardise formatting. TypeScript can prevent a component from treating a possibly absent value as definitely present, although types cannot prove correct runtime data from an untrusted server.

Maintainable frontend code tends to have small cohesive components, explicit data contracts, descriptive names, predictable state transitions, limited duplication, accessible markup, and clear separation between presentation and data access. Abstraction should be justified by repeated or complex behaviour; premature abstraction can hide simple logic and make testing harder.

Code review

A code review is a quality and knowledge-sharing activity, not a contest. A reviewer should examine:

  1. requirement coverage and user-visible behaviour;
  2. correctness on success, empty, loading, and failure paths;
  3. accessibility, keyboard operation, and semantic HTML;
  4. security concerns such as unsafe rendering and exposed secrets;
  5. state and asynchronous behaviour;
  6. test quality and meaningful assertions;
  7. performance risks; and
  8. clarity, scope, and maintainability.

Comments should distinguish required changes from suggestions. Authors should explain design decisions and respond to evidence rather than personal preference. Small pull requests are easier to review accurately.

7. Applied Case: Diagnosing a Failing Product Search

The following is an illustrative case. A diploma-level team develops a React product-search page. Users report that searching sometimes shows “No products found” even when matching products exist.

Background and problem

The page has a search input, a debounced request, a result list, and an empty-state message. The defect occurs intermittently, especially when users type quickly.

Analysis

The team first reproduces the problem with browser Network tools. Two requests are visible: an earlier request for lap and a later request for laptop. The response for laptop arrives first, but the older lap response arrives later and overwrites the state. This is a race condition. A second inspection reveals that the empty-state test treats a loading state as an empty result.

Approach and solution

The team writes tests before changing the implementation:

  • a unit test verifies the query-normalisation function;
  • a component test verifies loading, results, empty, and error states;
  • an integration test intercepts two requests and returns them in reverse order;
  • an end-to-end test verifies that a user sees results after searching.

The implementation is changed so that each request is associated with the current query and stale requests are aborted or ignored. Rendering is made mutually exclusive: loading is shown while loading, the empty message only when loading is false and the result collection is empty, and the error message only on failure.

A conceptual state model is:

idle → loading → success
             ↘ error
success → loading

The team also adds a test for rapid input and runs linting, type checking, and the complete test suite in continuous integration.

Outcome

The race condition no longer overwrites current results, and the empty message is not displayed during loading. The regression test protects the repair. The team documents that network responses cannot be assumed to arrive in request order.

Learning points

The case demonstrates that browser tools locate evidence, integration tests reveal boundary problems, and component tests protect user-visible states. It also shows why testing only the final successful response would not have exposed the defect.

8. Limitations, Ethics, and Contemporary Practice

No test suite can prove that an application contains no defects. Tests reflect their scenarios and assumptions. Flaky tests are particularly harmful because teams learn to ignore failure signals. A flaky test should be diagnosed, quarantined only temporarily, and repaired or removed with a documented reason.

Test data must protect privacy. Real customer records should not be copied into development systems without appropriate authorisation, minimisation, and security controls. Test logs must not expose passwords, tokens, personal identifiers, or payment details. Accessibility testing is an ethical and professional responsibility: keyboard operation, focus management, labels, contrast, and screen-reader semantics should be tested rather than assumed.

AI-assisted coding can generate tests and suggest fixes, but generated tests may merely reproduce the implementation, omit important edge cases, or contain insecure assumptions. Developers remain responsible for reviewing generated code, validating claims, protecting confidential data, and ensuring that tests express requirements. AI is most useful as a supplementary reviewer or test-idea generator, not as an authority on correctness.

A contemporary quality pipeline commonly runs formatting, linting, type checking, unit tests, component and integration tests, production builds, vulnerability checks, and selected end-to-end tests for every pull request. Results should be observable, reproducible, and linked to the change that caused them.

Best-Practice Checklist

  • Test observable behaviour before implementation details.
  • Include success, loading, empty, error, and boundary states.
  • Prefer deterministic tests and control external boundaries.
  • Use accessible queries and semantic markup.
  • Keep tests independent and clean up resources.
  • Treat coverage as a diagnostic measure, not a quality guarantee.
  • Run static checks and tests automatically in continuous integration.
  • Investigate flaky tests immediately.
  • Add a regression test for every significant defect.
  • Review security, privacy, accessibility, and maintainability as well as functionality.

Chapter Summary

Testing provides repeatable evidence about software behaviour, while debugging explains differences between expected and actual behaviour. Unit, component, integration, and end-to-end tests operate at different scopes and should be combined according to risk. Effective frontend tests focus on observable behaviour, realistic interaction, asynchronous states, and controlled external boundaries.

Browser developer tools support evidence-based diagnosis through DOM inspection, console errors, network traces, breakpoints, storage inspection, and framework component tools. Mocks, stubs, spies, fakes, and fixtures are useful when applied at appropriate boundaries. Coverage indicates what was executed but cannot demonstrate correctness. Linting, formatting, type checking, code review, accessibility testing, security practice, and continuous integration strengthen maintainability. A professional quality strategy is iterative: reproduce a defect, form a hypothesis, test the hypothesis, fix the cause, and preserve the solution with a regression test.

Key Terms

Unit test: A test of a small isolated unit of behaviour.

Component test: A test of a rendered component and its user-visible interactions.

Integration test: A test of collaboration between multiple units or modules.

End-to-end test: A test of a complete user journey through the application.

Assertion: A statement that verifies an expected result.

Test double: A replacement or controlled substitute for a dependency.

Stub: A test double that supplies predetermined responses.

Spy: A test double that records calls and interactions.

Mock: A configured double with interaction expectations.

Fixture: Controlled data or setup used by tests.

Regression test: A test that prevents a previously fixed defect from returning.

Flaky test: A test whose result changes without a relevant code change.

Code coverage: A measure of code elements executed by tests.

Branch coverage: The proportion of measured decision paths executed.

Static analysis: Examination of code without executing it.

Linting: Automated detection of suspicious or disallowed code patterns.

Source map: A mapping between bundled code and original source code.

Race condition: A defect caused by timing-dependent interaction between operations.

Continuous integration: Automated validation of changes in a shared development workflow.

Maintainability: The ease with which software can be understood, modified, and tested.

Review Questions

Short Answer Questions

  1. What is the primary difference between a unit test and an end-to-end test?
  2. Why are accessible queries generally preferred in component tests?
  3. Name three capabilities provided by browser developer tools.
  4. Why does high code coverage not prove that software is correct?
  5. What is a regression test?

Long Answer Questions

  1. Explain the roles of unit, component, integration, and end-to-end tests in a frontend test strategy.
  2. Describe a systematic debugging process for a failed API-driven component.
  3. Compare stubs, spies, mocks, fakes, and fixtures, giving one appropriate use for each.
  4. Explain how React Developer Tools or Angular DevTools can support diagnosis of state and rendering defects.
  5. Discuss how static analysis, code review, and continuous integration contribute to maintainability.

Analytical / Application Questions

  1. A search page displays results from an older request after a user has entered a newer query. Identify the likely defect, explain how browser tools could confirm it, and propose tests that would prevent its return.
  2. A team reports 95% statement coverage, but users encounter failures in loading and error states. Analyse why the coverage figure was insufficient and recommend a revised testing approach.
  3. Design a quality plan for a login component, including unit, component, integration, and end-to-end tests, plus accessibility, security, and privacy checks.

Multiple Choice Questions with Answers

Question 1

Which test level most directly verifies a complete user journey in a real browser?

A. Unit testing

B. Component testing

C. End-to-end testing

D. Static analysis

Correct Answer: C. End-to-end testing

Question 2

Which browser tool is most useful for inspecting request status, payloads, and response timing?

A. Network panel

B. Elements panel

C. Application panel

D. Sources formatter

Correct Answer: A. Network panel

Question 3

What is the main purpose of a stub?

A. To measure branch coverage

B. To supply predetermined responses

C. To render a complete application

D. To enforce CSS formatting

Correct Answer: B. To supply predetermined responses

Question 4

If 3 of 4 measured branches are executed, what is the branch coverage percentage?

A. 25%

B. 50%

C. 75%

D. 80%

Correct Answer: C. 75%

Question 5

Which assertion is generally most aligned with user-centred component testing?

A. A private state variable equals true

B. A CSS class has a particular name

C. An error message is visible to the user

D. A helper function was called twice internally

Correct Answer: C. An error message is visible to the user

Question 6

What defect occurs when an older network response overwrites newer results?

A. Race condition

B. Memory leak

C. Syntax error

D. Type narrowing

Correct Answer: A. Race condition

Question 7

Which practice is most appropriate for a test that uses an external payment provider?

A. Call the real provider in every unit test

B. Mock or fake the external boundary

C. Remove all assertions

D. Disable asynchronous handling

Correct Answer: B. Mock or fake the external boundary

Question 8

What does static analysis do?

A. Examines code without executing the application

B. Tests only database performance

C. Simulates a real customer journey exclusively

D. Replaces all code reviews

Correct Answer: A. Examines code without executing the application

Question 9

Which state should normally prevent an empty-results message from appearing?

A. Loading

B. Successful empty response

C. Completed search with zero items

D. Idle with no query, if specified as empty

Correct Answer: A. Loading

Question 10

What is the best response to a significant defect after fixing it?

A. Delete the failing test

B. Add a regression test

C. Increase formatting changes

D. Ignore it if the browser appears correct

Correct Answer: B. Add a regression test

Frontend Development using React.js or Angular
Chapter 11 of 14