Angular development

Inherited Angular With No Tests – Patch or Build Properly?

You have inherited an Angular application with no reliable tests, a release deadline, and a request to make it faster. I would spend the first small budget on a narrow regression harness, not an architectural cleanup. That may feel like postponing the proper fix, but changing unverified code is a poor way to buy reliability: you cannot tell whether the cleanup preserved the behavior people use.

Freeze a few critical behaviors before you improve the architecture

The first question is not whether the application uses sensible component boundaries. It is whether anyone can identify a broken release before users do. Start by tracing the routes that lead to a completed action: a form submitted, a record saved, or a permission denied. Ask support staff and developers which failures have caused rollbacks, because their incident history is more useful than an unranked inventory of components. Pick three journeys as an adjustable starting limit; a larger initial suite costs more to keep stable before you know which assertions earn their place.

For each journey, record what the user does, what network request follows, and what visible result proves success or failure. Include the failure path for at least one request, because an Angular app can display a loading state indefinitely while a happy-path test passes. Capture current behavior even if it looks odd, but mark questionable behavior as observed rather than required. That distinction prevents a characterization test from turning an existing defect into a permanent specification.

Do not begin by moving every component into a new feature folder or replacing services with a preferred state-management pattern. I would not make that refactor first, because it changes many dependencies while leaving you unable to distinguish a regression from a pre-existing defect. Draw a route-to-component-to-service map instead. Angular DevTools can help inspect component relationships, while the Angular CLI can identify build and test commands already present in package.json. Test the architectural argument in Angular Front-End Architecture for Scalable Web Applications against that map before adopting its boundaries; a useful boundary in a planned application may cut straight through an undocumented workflow in yours.

Save evidence that another engineer can reproduce: the route, an account role, the request and response shape, and a screenshot only where appearance determines the outcome. Check whether a test account can create disposable data, because tests that depend on yesterday’s records will fail for reasons unrelated to the release. Keep a short list of behaviors you cannot safely exercise in staging. That list is a risk register, not a reason to invent assertions from code you have not seen run.

Playwright wins the first week; unit tests win once the seams are known

Playwright browser tests and Jasmine/Karma unit tests solve different problems in this inherited codebase. Playwright wins first when the main uncertainty is whether a route still works across templates, dependency injection, HTTP calls, and browser navigation. Its cost is slower execution and a need for stable test data. Jasmine/Karma wins when you have isolated a service or component with a clear contract, because focused cases run without a full browser journey; its initial cost is often substantial setup work when constructors and shared state were never designed for isolation. Neither option deserves a blanket coverage target until you know what a passing test would prove.

Put one browser test around a journey you can reset. This example assumes the local Angular app is running on port 4200 and has an orders route; replace the route and API pattern with ones you have verified. The intercepted response makes the UI assertion independent of a shared database, while still exercising routing and rendering. With @playwright/test installed and Chromium available through Playwright, the file can be run with npx playwright test tests/orders.spec.ts.

import { test, expect } from '@playwright/test';

test('shows a returned order', async ({ page }) => {
  await page.route('**/api/orders', route =>
    route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify([{ id: 42, name: 'Order 42' }])
    }));
  await page.goto('http://localhost:4200/orders');
  await expect(page.getByText('Order 42')).toBeVisible();
});

This is a characterization test, not proof that the server returns correct orders, because the response is mocked. Add one separate staging check for the real API contract if a broken response would block the journey. Prefer role-based Playwright locators such as getByRole when the interface exposes accessible names; they tend to survive CSS changes and reveal missing labels. Avoid asserting an exact DOM tree, because a harmless template rearrangement would then consume the same investigation time as a real failure.

As the service map becomes clearer, add unit tests around calculations, authorization decisions, and error handling. Angular’s HttpTestingController can verify an HttpClient request without a live server; its value is strongest when the service contract is stable enough to name the expected URL and response. Use Istanbul or the coverage report produced by the project’s existing runner to find untouched files, not to declare that a percentage equals safety. A line can execute while the test fails to check the result. Keep the first browser suite small enough that a failure gets investigated rather than routinely retried.

A faster build is not evidence of a faster user journey

The performance request needs its own baseline. Record the Angular CLI production build settings, including whether budgets are configured in angular.json, but do not treat a smaller bundle as the final result. A bundle reduction may leave the slowest route unchanged if its delay comes from an API response or expensive rendering after navigation. Likewise, a Lighthouse run is useful for finding candidate problems, but a lab run on one machine does not describe every user’s connection or device.

Use Chrome DevTools Performance to inspect the inherited route, then compare it with real-user measurements where available. Google’s published “good” Core Web Vitals boundaries include a Largest Contentful Paint of 2.5 seconds or less, Interaction to Next Paint of 200 milliseconds or less, and Cumulative Layout Shift of 0.1 or less. Those are external reference points, not measurements of your application. If no Real User Monitoring data exists, establish a repeatable staging baseline with the same account, data volume, browser, and throttling settings before claiming an improvement.

Do not optimize the entire application from one Lighthouse score. Examine the slow journey’s waterfall for delayed API responses, its long tasks for JavaScript work, and its layout shifts for unstable elements. A hypothetical route that spends 900 milliseconds waiting for an orders response would not become much quicker through a component-folder reorganization, because that organization does not shorten the request. By contrast, repeated expensive work during a user action gives you a reason to investigate change detection and template expressions. Use Web Vitals and the browser trace to locate the cost before selecting the fix.

I disagree with treating High-Performance Web Apps with Angular and Front-End Expertise as a performance checklist for an untested inheritance: the first optimization should be the one your baseline and regression test can evaluate. For example, lazy-loading a route may improve initial loading yet delay that route’s first visit; measure both experiences before calling it a win. Add a Playwright assertion for the visible outcome of the changed journey, then rerun the same performance procedure. This pairs “did it get faster?” with “does it still work?” without pretending that a browser test is a precise performance benchmark.

Doing it properly means paying for repeatability, not a larger test count

A small-budget effort can stop after a few deterministic journeys, a documented baseline, and a manual release check. That is a defensible temporary choice when releases are infrequent and the team can name the remaining risks. It is not the same as having a maintainable quality system. The proper investment begins when tests run on every pull request, failures have an owner, and test data can be created and removed without coordinating with another team. Otherwise, growing the suite merely grows the queue of ambiguous failures.

Make the CI job run the existing production build, the targeted unit tests, and the small Playwright suite. Keep browser versions pinned through the Playwright package and CI image so a local-to-CI mismatch is less likely to masquerade as an application defect. Require developers to attach the failing route and trace to a regression report. Do not silently rerun failures until they pass: retries may help diagnose an intermittent test, but treating the eventual pass as clean hides the instability that makes the gate untrustworthy.

Spend engineering time on isolation before multiplying scenarios. Give tests accounts with known permissions, generate records with unique identifiers, and reset those records through a supported API or disposable environment. Use data-testid only where a stable semantic locator is unavailable, because selectors tied to implementation details make routine UI work unnecessarily expensive. Add API contract checks where the Angular client relies on a specific response shape, and include authentication and authorization cases where a wrong result would expose data or block work. These checks cost setup time, but each addresses a failure that a mocked browser journey cannot catch.

Set an initial CI runtime allowance of 15 minutes as a planning constraint to revisit with the team, rather than a universal speed standard. If the gate grows beyond that allowance, split fast pull-request checks from broader scheduled checks according to the risk each detects; do not simply delete the slowest tests. Review failures after releases to see whether the suite caught the defect, missed the journey, or asserted the wrong outcome. That feedback determines the next test worth buying far better than a coverage percentage does.

The next change should create evidence you can use

Before changing code, choose one frequently used route and write down its successful result, one failure result, and the API response each requires. Run that journey manually, then automate its visible outcome with resettable data. Once the test passes against the inherited application, make one small change and run it again. You will have a baseline for deciding what to improve next, rather than a cleanup you can only hope was safe.