Angular testing with fake async
Let’s mix two scary concepts: asynchronism and Angular testing. And a directive to make the hellfire formula. It’s easier than you think using a couple of tricks.
TLDR: Use a host component to test your directive in operation. Make use of the async capabilities of Angular.
In this post, I will show you an example of those techniques you may need when testing pipes, components, or directives affected by some time-based feature.
🎒Prerequisites
- Proficient as a modern Angular developer.
- Basic knowledge of testing with Jest (read my other articles if you need to refresh something)
Our Subject Under Test is the search directive.
This directive provides a simple way to add search functionality to an input element, with configurable debounce time and minimum search term length.
It defines an output property, search, which is an observable that emits search terms as the user types. The source is created using the fromEvent function and piped through a series of operators to debounce the typing, filter out short search terms, and remove duplicate queries.
type EventArg = { target: { value: string } };
@Directive({
selector: '[appSearch]',
standalone: true,
})
export class SearchDirective {
@Input() debounceTime = 300;
@Input() minLength = 2;
@Output() search: Observable<string> = fromEvent<EventArg>(
this.hostRef.nativeElement,
'input'
).pipe(
debounceTime(this.debounceTime),
map((eventArg: EventArg) => eventArg.target.value),
filter((term: string) => term.length >= this.minLength),
distinctUntilChanged()
);
constructor(private hostRef: ElementRef) {}
}
Using a directive is really easy. You apply it as an attribute to any element, which will be sent as a reference as a constructor parameter.
<input nwdSearch
[debounceTime]="debounceTime"
[minLength]="minLength"
(search)="onSearch($event)" />
Job done; let’s probe if it works as expected.
The host container component for the SUT.
This is our first test trick of the day. It is useful for testing directives but also for pipes and child components. We will create a fake component to apply our SUT (the search directive in this particular case.)
Such a component should be as simple as possible, with no relevant naming or selector, as it will be important by its role as a host container for the SUT. You can write it prior the describing function in the same spec file.
@Component({
selector: 'app-test-container',
template: `
<input
appSearch
[debounceTime]="500"
[minLength]="3"
(search)="onSearch($event)"
/>
`,
})
class TestContainerComponent {
onSearch(term: string) {}
}
By doing so, we now need to adjust our arrangement section to compile the host and use the SUT as a dependency to be imported. To start with confidence, I check that all is wired correctly.
describe('The SearchDirective', () => {
let component: TestContainerComponent;
let fixture: ComponentFixture<TestContainerComponent>;
beforeEach(async () => {
TestBed.configureTestingModule({
declarations: [TestContainerComponent],
imports: [SearchDirective],
}).compileComponents;
fixture = TestBed.createComponent(TestContainerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create the component and apply the directive', () => {
expect(component).toBeTruthy();
const inputEl = fixture.nativeElement.querySelector('input');
expect(inputEl.getAttribute('appSearch')).toBeDefined();
});
});
Use the beforeEach function to arrange some useful variables. Namely, the input element to act with, a spy to be aware of the behavior, and some constants to avoid repetitive code.
describe('The SearchDirective', () => {
// ...
let inputEl: HTMLInputElement;
let spy: jest.SpyInstance;
const inputEvent = new Event('input');
const inputOne = 'hello';
const inputTwo = 'world';
const inputZero = 'he';
beforeEach(async () => {
// ...
inputEl = fixture.nativeElement.querySelector('input');
spy = jest.spyOn(component, 'onSearch');
//...
We are done with the arrangement; let’s act and assert.
Waiting for events and dealing with delays.
Are you eager for some async rock and roll? Wait no more… or… wait a little. Enter the fakeAsync wrapper and the tick function.
it('should emit the input after a while', fakeAsync(() => {
inputEl.value = inputOne;
inputEl.dispatchEvent(inputEvent);
tick(DEBOUNCE_TIME);
fixture.detectChanges();
expect(spy).toHaveBeenCalledWith(inputOne);
}));
The fake async is applied to the assertion function, making it run in a special controlled zone mode. This allows us to simulate the passage of time as we need.
For example, if we need to wait a little bit after the user changes the input, then there is time for the tick function to shine.
Without this configuration, the input DOM event (simulating the user typing or pasting the term to search) will not fire the Angular output event. The directive does this: wait a little before sending the content to avoid unnecessary partial requests.
You can assert that also using this traditional way of code.
it('should not emit the input immediately', () => {
inputEl.value = inputOne;
inputEl.dispatchEvent(inputEvent);
fixture.detectChanges();
expect(spy).not.toHaveBeenCalled();
});
🌅 Summary.
You learned how to test those UI elements as part of something bigger. This a very common situation for directives, pipes, and small dumb components. Also, you have seen how to control the passing of time by faking it with Angular utilities.
Keep testing your Angular stuff to elevate your code quality.