An Angular HttpClient test fails because HttpTestingController.expectOne reports 'found none' although the service method ran; what do you check, and why might verify() fail too?
answer
- cold until someone listens
- the full URL, query string included
- claimed requests leave the list
- unsubscribing is not removal
basics
~20 sCheck the observable was subscribed, the matcher equals the full URL with query string, the request was not sent later or already claimed, and the real backend was not re-provided; verify() reports anything left open, cancelled requests included.
solid answer
~40 s`HttpClient` observables are cold, so the request only enters the test backend's open list on subscribe; calling the method without subscribing is the first suspect. Next, string and `{ method, url }` matchers compare the full URL with its query string, so `params` or an interceptor-added prefix makes `expectOne('/api/users/42')` miss; the error message lists the requests actually received. The request may also be issued later (a timer or debounce), already claimed by an earlier `expectOne` or `match`, or sent to the real backend because `provideHttpClient()` was listed after `provideHttpClientTesting()`. `verify()` fails on any request still open: the one your matcher missed, an unexpected extra call, or a cancelled request, which stays open unless you pass `{ ignoreCancelled: true }`.
go deeper
Remember the two quick checks: did anything subscribe to the observable, and does the matcher equal the full URL including the query string.
Explain the open-request list: what adds to it (subscribe), what removes from it (expectOne, match, expectNone) and what verify() looks at.
Diagnose from the error text, treat 'found 2' and cancelled-request failures as defects in the code under test, and keep verify() in afterEach so regressions surface immediately.
Set suite-wide conventions, such as shared setup that fixes provider order, mandatory verify() and descriptive matchers, so HTTP tests fail with actionable messages across a large codebase.
## The symptom `HttpTestingController.expectOne()` throws `Expected one matching request for criteria "...", found none.` The service method was definitely called. Sometimes `verify()` in `afterEach` fails as well, with `Expected no open requests, found 1: GET /api/users/42?fields=name`. Two facts about the test backend explain almost every case: - A request enters the controller's **open list only when the `HttpClient` observable is subscribed**. `HttpClient` observables are cold. - `expectOne`, `match` and `expectNone` search the open list **at the moment you call them** and remove what they match. `verify()` then fails on whatever is **still open**. The error message helps: when other requests are open, it appends `Requests received are: ...` with their methods and full URLs. Read it before changing anything. ## Checklist for "found none" 1. **Nobody subscribed.** `service.getProfile('42')` returns an observable; calling it without `.subscribe()`, `firstValueFrom()` or a rendered `async` pipe sends nothing. In a component test the template has not rendered until change detection runs, which is the fixture's job. 2. **The URL does not match exactly.** String and `{ method, url }` matchers compare the **full URL with its query string**. A request built as `http.get('/api/users/42', { params: { fields: 'name' } })` is `/api/users/42?fields=name`, so `expectOne('/api/users/42')` misses it. So do a base-URL prefix added by an interceptor or environment config, and a trailing slash. Use a predicate (`req => req.url === '/api/users/42'`) and assert `req.request.params` separately. 3. **The request has not been made yet.** If the service waits on a timer, a `debounceTime`, or an awaited promise before calling `HttpClient`, the request is queued later than your `expectOne`. You need to advance fake time or await the step first; how to do that is the fake-clock topic. 4. **It was already claimed.** An earlier `expectOne` or a broad `match({ method: 'GET' })` removed it from the list. 5. **The real backend is in charge.** `provideHttpClient(...)` listed **after** `provideHttpClientTesting()` re-binds `HttpBackend` to the real transport. The request never reaches the test backend at all. ## The mirror symptom: "found 2 requests" `expectOne` also throws when **more than one** open request matches. The usual cause is **two subscriptions** to the same cold observable, for example two `async` pipes on one un-shared stream, or a service that subscribes internally and also returns the observable. Each subscription is a separate HTTP call. The fix belongs in the code under test (share the result), not in the test (switching to `match()` would hide a real duplicate request). ## Why verify() fails too | verify() message lists | Likely cause | Fix | |---|---|---| | The request you meant to claim | URL mismatch in your matcher | Match on the real URL or use a predicate | | A request you never expected | Extra call in the code under test | Fix the code, or claim it explicitly if it is intended | | A cancelled request | A `switchMap`, `takeUntilDestroyed` or unsubscribe cut it off | Claim it, or call `verify({ ignoreCancelled: true })` | The last row surprises people. Unsubscribing marks a `TestRequest` as **cancelled** but leaves it in the open list, so by default `verify()` still reports it. `verify({ ignoreCancelled: true })` filters cancelled requests out. Trying to `flush()` or `error()` a cancelled request throws, which is often the first sign that the code cancelled earlier than the test expected. Also note what `verify()` does **not** check: a request claimed with `expectOne` but never flushed is no longer open, so `verify()` passes. An unanswered claimed request usually shows up as a missing emission in your own assertion instead. ## Reading the failure message The messages carry more than people read. `Expected one matching request for criteria "load profile 42", found none. Requests received are: GET /api/users/42?fields=name.` already names the cause: the request exists, and only the matcher is wrong. `found none` with **no** "Requests received" suffix means the open list was empty, which points at causes 1, 3, 4 or 5 above rather than a URL typo. The description is the second argument you passed to `expectOne`; without it, Angular derives one from the matcher, which is less helpful when the matcher is a predicate. ## How a senior engineer keeps these tests stable - Put `verify()` in `afterEach` so every spec proves it made only the calls it meant to. - Give `expectOne` a **description** argument, so failures read as intent rather than URLs. - Prefer predicates over hand-built URL strings when parameters are involved. - Treat "found 2" and "cancelled request" failures as **findings about the code**, not noise to suppress.
- expectOne reports 'found 2 requests' for a profile GET. Should you switch to match()?Not before finding out why. Two open requests for the same URL usually mean two subscriptions to a cold `HttpClient` observable, which is a real duplicate call in production. Fix the code under test, for example by sharing the result; use `match()` only when several requests are genuinely intended, such as a batch fan-out.
- A test claims the request with expectOne but never flushes it. Does verify() catch that?No. Claiming removes the request from the open list, and `verify()` checks only for unclaimed open requests. The mistake shows up elsewhere: the subscriber never receives a value, so an assertion on the emitted result fails or, worse, an assertion that nothing was emitted passes by accident.
saying these in an interview costs you the question
- expectOne ignores query parameters when matching a URL string
- Unsubscribing removes the request from the controller entirely
- verify() also fails when a claimed request was never flushed
- Switching to match() is the right fix for a 'found 2' failure
- Provider order in TestBed cannot affect which backend handles requests
- HttpClient sends the request as soon as get() is called