Mastering Angular Tests: tick() and detectChanges() Demystified

Writing component tests is super important for making sure our components behave just the way we expect.
The Angular framework provides us with many different testing utilities, and in this article, we shine a light on two such helpers: tick()
and detectChanges()
.
In just a few minutes, you'll understand exactly what they do and how they differ.
So let's get started! đ
tick()
vs. detectChanges()
When we're testing an Angular component, we often use the TestBed.createComponent()
method to bring our component to life in a testing environment. Think of this as setting up a mini stage for our component to perform on.
However, unlike in a live application where Angular constantly watches for changes and updates the screen, TestBed.createComponent()
does NOT automatically trigger this update process called change detection.
This is intentional and useful!
It gives us testers the chance to inspect and change the state of the component before Angular updates the part of the test browser's DOM that shows this component.
This is where detectChanges()
and tick()
come into play, each with their own unique role.
detectChanges()
Our friend detectChanges()
is all about the view!
When we call fixture.detectChanges()
, we're explicitly telling Angular, "Okay, take a look at the component's current state (its properties and data) and update the part of the test browser's DOM that shows this component!".
Imagine your component's data is like the script for a play, and the template is the stage. When you change the script (update a component property), the stage (the DOM) doesn't magically update itself in tests right away. You need to tell the stage crew (Angular's change detection) to read the updated script and arrange the props accordingly. That's what fixture.detectChanges()
does.
In tests without automatic change detection, if we create a component or change a component property, we need to call fixture.detectChanges()
for the changes to appear in the template's DOM.
For example, if a component has a title
property displayed in an <h1>
tag, after creating the component, the <h1>
might be empty.
Calling fixture.detectChanges()
updates it to show the initial title value.
If we then change component.title
, we need another call to fixture.detectChanges()
to see the new title in the DOM.
it('should display a different test title', () => {
fixture = TestBed.createComponent(BannerComponent);
component = fixture.componentInstance;
let h1 = fixture.nativeElement.querySelector('h1');
// Before detectChanges, the title is still empty
expect(h1.textContent).toEqual('');
// Run change detection to update the view
fixture.detectChanges();
// Now the title is displayed
expect(h1.textContent).toContain(component.title);
// Change the title
component.title = 'Test Title';
// Run change detection to update the view again
fixture.detectChanges();
// Now the new title is displayed
expect(h1.textContent).toContain('Test Title');
});
ComponentFixtureAutoDetect
provider or by using fixture.autoDetectChanges()
after updating the component's state to trigger automatic change detection. But even with automatic detection, we might still need to wait for updates, especially with signals or async operations, using
fixture.whenStable()
. Explicitly calling
fixture.detectChanges()
gives us precise control over when the view updates in our test.tick()
The wonderful tick()
is the time traveler for our tests!
But there's a catch â we can only use tick()
inside a special test zone created by the fakeAsync()
helper.
When we wrap our test code in fakeAsync()
, Angular's testing utilities take control of the browser's asynchronous APIs like setTimeout
, setInterval
, promises, and certain RxJS operations.
Instead of waiting for real-world time delays, fakeAsync()
lets us simulate the passage of time.
Calling tick(100)
, for example, makes the simulated clock jump forward by 100 milliseconds.
Any pending asynchronous operations that were scheduled to run within that 100ms window will now complete. It's like fast-forwarding a video player through the boring waiting parts to get to the action!
This is incredibly useful for testing things like delays, debouncing, or asynchronous service calls that return Observables or Promises after a delay.
For instance, if our component makes an asynchronous call that updates a property after a short delay, we can use fakeAsync
and tick()
to simulate that delay and ensure the async operation completes.
After calling tick()
to let the async task finish and potentially update the component's data, we can call fixture.detectChanges()
to propagate that data change to the template and check the updated DOM.
We can also use tick(0)
to simply flush out any microtasks (like resolved Promises) or macrotasks (like setTimeout
with 0 delay) that are currently queued up:
it('should show quote after getQuote', fakeAsync(() => {
fixture.detectChanges();
// Initially, the quote element shows a placeholder
expect(quoteEl.textContent).toBe('...');
// Flush the asynchronous observable to get the quote
tick();
// Update view with the received quote
fixture.detectChanges();
// Now the quote is displayed
expect(quoteEl.textContent).toBe(testQuote);
}));
detectChanges()
+ tick()
= harmony
detectChanges()
is about updating the view to reflect the component's current state. It tells Angular to run its change detection cycle and update the DOM.
In contrast, tick()
is about simulating the passage of time within a fakeAsync
zone to complete pending asynchronous operations. It doesn't inherently update the view, but it allows async tasks to finish, which might lead to changes in the component's state.
We may sometimes want to use them together, especially when testing components that interact with asynchronous services or timers.
First, we use tick()
(in fakeAsync
) to complete the async operation that updates the component's data. Then, we use detectChanges()
to ensure the template is updated with this new data.
Summary
When writing component tests, we need control over the view over time. tick()
and detectChanges()
give us just that!
detectChanges()
helps us control when the view is updated based on the component's data.
tick()
(within fakeAsync
) helps us control the passage of time and the completion of asynchronous tasks.
By understanding the joyful dance between updating the view with detectChanges()
and simulating time with tick()
, we are well-equipped to write robust and clear component tests for our Angular applications!
Keep coding, keep testing, and keep smiling! đđ
Have a great one! âī¸
Thank you! đđ