Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions apps/cookbook/docs/angular/02-glossary.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
---
title: Glossary
slug: /angular/glossary
sidebar_position: 10
---

## 3X
Expand All @@ -11,6 +12,14 @@ Explore, Expand, and Extract are the three phases of software development that K

Affected tests are tests that are impacted by a change in the codebase. By running only the affected tests, developers can get faster feedback on the changes they have made. This can be achieved using features like [Nx Affected Graph](https://nx.dev/ci/features/affected#run-only-tasks-affected-by-a-pr) and/or [Vitest's `changed` option](https://vitest.dev/guide/cli.html#changed).

## Angular Synchronization

Angular synchronization is the process that keeps the UI in sync with the application state. It handles:

- flushing views _(i.e., change detection)_
- running reactive effects
- triggering render hooks

## Canary Release

Canary release is a technique used to reduce the risk of introducing a new feature or change to a large audience. By releasing the change to a small subset of users first, the team can monitor the impact and gather feedback before rolling out the change to the entire user base.
Expand Down Expand Up @@ -60,6 +69,10 @@ Narrow tests are tests that are fast, easy to isolate and parallelize, and have

Cf. [Narrow Tests Definition](./01-testing/01-beyond-unit-vs-integration/index.mdx#narrow-tests)

## Over-Narrow Tests

Over-narrow tests are more specific than necessary, often leading to over-specification.

## Over-Specification

Over-specification occurs when tests are too tightly coupled to the implementation details of the System Under Test. This can make tests brittle and hard to maintain, as any changes to the implementation will require corresponding changes to the tests.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
---
slug: /angular/flushing-flusheffects
sidebar_label: Flushing "flushEffects"
---

# V20 Flushes `flushEffects` Down the Sink

:::warning
In Angular 20, [`TestBed.flushEffects()`](https://v19.angular.dev/api/core/testing/TestBedStatic#flushEffects) didn't survive developer preview and has been removed in favor of [`TestBed.tick()`](https://next.angular.dev/api/core/testing/TestBedStatic#tick).
:::

`TestBed.tick()` is **not** a drop-in replacement for `TestBed.flushEffects()` — it does more than just flushing effects. It triggers Angular [synchronization](../../02-glossary.md#angular-synchronization) _(change detection, effects, etc...)_, making tests more symmetric to production, and therefore more reliable.

In most cases, that's an improvement, but some tests with questionable design might break.

:::tip TL;DR

1. [Monkey-patch](#incremental-migration) `TestBed.flushEffects()` **temporarily** and fix broken tests before migrating to v20 and `TestBed.tick()`.
2. Prefer using a utility such as [`runInAngular()`](#run-in-angular) when narrowing down your tests to reactive logic that lives beneath components and services.
3. Think twice before narrowing down your tests to such granularity.
:::

## `TestBed.tick()` Might Not Be What You Need

Angular's synchronization should be treated as an implementation detail. Tests should generally avoid interfering with it.

Let's start with a typical test using `TestBed.tick()`:

<div className="meh">

```ts
test('Favs auto-saves the favorite recipe in the storage', async () => {
const storage = TestBed.inject(StorageFake);
const favs = TestBed.inject(Favs);

favs.recipe.set('burger');

TestBed.tick();

expect(storage.getSync('favorite-recipe')).toBe('"burger"'); // ✅
});

@Injectable({ providedIn: 'root' })
class Favs {
readonly recipe = signal('babaganoush');
readonly saving = autoSave('favorite-recipe', this.recipe);
}

function autoSave<T>(key: string, data: Signal<T>) {
const storage = inject(Storage);
/* WARNING: as of 20.0.0-rc.0, `resource` is still **experimental**. */
const syncResource = resource({
params: data,
loader: async ({ params: value }) => {
await Promise.resolve();
storage.set(key, JSON.stringify(value));
},
});
return syncResource.isLoading;
}
```

</div>

After refactoring our code to wait for some async operation to complete, the test fails because the assertion is made before the microtask is flushed:

<div className="bad">

```ts
test('Favs auto-saves the favorite recipe in the storage', async () => {
const storage = TestBed.inject(StorageFake);
const favs = TestBed.inject(Favs);

favs.recipe.set('burger');

TestBed.tick();

// highlight-next-line
expect(storage.getSync('favorite-recipe')).toBe('"burger"'); // ❌ the microtask was not flushed yet
});

@Injectable({ providedIn: 'root' })
class Favs {
readonly recipe = signal('babaganoush');
readonly saving = autoSave('favorite-recipe', this.recipe);
}

function autoSave<T>(key: string, data: Signal<T>) {
const storage = inject(Storage);
/* WARNING: as of 20.0.0-rc.0, `resource` is still **experimental**. */
const syncResource = resource({
params: data,
loader: async ({ params: value }) => {
// highlight-next-line
await Promise.resolve();
storage.set(key, JSON.stringify(value));
},
});
return syncResource.isLoading;
}
```

</div>

## Alternatives

### 1. Wait for Stability

Use [`applicationRef.whenStable()`](https://angular.dev/api/core/ApplicationRef#whenStable) to ensure all pending tasks are completed:

<div className="good">

```ts
test('Favs auto-saves the favorite recipe in the storage', async () => {
const storage = TestBed.inject(StorageFake);
const favs = TestBed.inject(Favs);

favs.recipe.set('burger');

// highlight-next-line
await TestBed.inject(ApplicationRef).whenStable();

expect(storage.getSync('favorite-recipe')).toBe('"burger"'); // ✅
});
```

</div>

:::info
Under the hood, [`resource`](https://angular.dev/api/core/resource) tells Angular that it is loading by leveraging the [`PendingTasks`](https://angular.dev/api/core/PendingTasks) service.

If you want the same behavior in your own utilities, you should use `pendingTasks.run()`.
:::

### 2. Polling

Use [Vitest's `expect.poll()`](https://vitest.dev/api/expect.html#poll) — or [Angular Testing Library's `waitFor` utility](https://testing-library.com/docs/dom-testing-library/api-async/#waitfor) for other testing frameworks:

<div className="good">

```ts
test('Favs auto-saves the favorite recipe in the storage', async () => {
const storage = TestBed.inject(StorageFake);
const favs = TestBed.inject(Favs);

favs.recipe.set('burger');

// highlight-next-line
await expect.poll(() => storage.getSync('favorite-recipe')).toBe('"burger"'); // ✅
});
```

</div>

:::warning Potential for False Negatives
Polling may seem robust, but it can yield [false negatives](../../02-glossary.md#false-negative): the result might appear valid during a brief window, only to become invalid once the application stabilizes.
:::

## Testing Signal Factories

You will often want to test your signal factories such as `autoSave` without leveraging a component or service.
Given that under the hood, it is using dependency injection, you will have to run it in an injection context.

<div className="meh">

```ts
test('autoSave auto-saves when signal changes', async () => {
const storage = TestBed.inject(StorageFake);

const recipe = signal('babaganoush');

// highlight-next-line
TestBed.runInInjectionContext(() => autoSave('favorite-recipe', recipe));

recipe.set('burger');

await TestBed.inject(ApplicationRef).whenStable();

expect(storage.getSync('favorite-recipe')).toBe('"burger"'); // ✅
});
```

</div>

To improve readability, you can implement a `runInAngular` utility function:

<div className="good">

```ts
test('autoSave auto-saves when signal changes', async () => {
const storage = TestBed.inject(StorageFake);

const recipe = signal('babaganoush');

// highlight-start
await runInAngular(() => {
autoSave('favorite-recipe', recipe);
recipe.set('burger');
});
// highlight-end

expect(storage.getSync('favorite-recipe')).toBe('"burger"'); // ✅
});

// highlight-start
async function runInAngular<RETURN>(
fn: () => RETURN | Promise<RETURN>,
): Promise<RETURN> {
return TestBed.runInInjectionContext(async () => {
const appRef = inject(ApplicationRef);
const result = await fn();
await appRef.whenStable();
return result;
});
}
// highlight-end
```

</div>

## Incremental Migration

Before migrating to Angular 20, you can already check whether `TestBed.tick()` breaks anything by monkey-patching `TestBed.flushEffects()`:

```ts title="src/test-setup.ts"
/* DO NOT KEEP THIS. IT'S ONLY FOR MIGRATION PREPARATION. */
import { TestBed } from '@angular/core/testing';

TestBed.flushEffects = () => TestBed.inject(ApplicationRef).tick();
```

In the rare occurrence where switch to `tick()` causes trouble:

1. I'd love to see your tests 😊.
2. You can implement a transitional utility function to avoid the big-bang switch:

```ts
export function triggerTick() {
TestBed.inject(ApplicationRef).tick();
}
```

You can then incrementally replace calls to `TestBed.flushEffects()` with `triggerTick()` and fix your broken tests before migrating to Angular 20.

Happy migration!

## Today’s Dash: `runInAngular` {#run-in-angular}

_Ready to be Copied, Stirred, and Served._

```ts
async function runInAngular<RETURN>(
fn: () => RETURN | Promise<RETURN>,
): Promise<RETURN> {
return TestBed.runInInjectionContext(async () => {
const appRef = inject(ApplicationRef);
const result = await fn();
await appRef.whenStable();
return result;
});
}
```
Loading