Testing ngModel in Angular 2

Originally published on my blog.

Angular has always put an emphasis on automated testing. Angular 2 continues this tradition — the question of testing is ubiquitous, both the core design and the documentation, which is great.

Unfortunately, Angular 2 is a much more delicate and complex environment than Angular 1, so are the testing tools. This is a case study of testing a simple component with two-way binding, highlighting the pitfalls to avoid.

If you want to run the code examples, grab the example repository — it has a commit for each section.

Let’s create a simple component to test. I cut the boilerplate, here’s just the essence:

export class AppComponent {
theValue = 'lowercase';
}
<input [(ngModel)]="theValue">
<code>{{ theValue | uppercase }}</code>

Our requirement is pretty simple: What comes in the <input> should come out in uppercase in the <code>.

The naive approach

Let’s try (and fail miserably):

// put our test string to the input element
fixture.debugElement.query(By.css('input')).nativeElement.value = 'test';
// expect it to be the uppercase version
expect(fixture.debugElement
.query(By.css('code'))
.nativeElement
.textContent
).toEqual('TEST');

Result: Expected '' to equal 'TEST' :(

Of course it fails! For a good reason: Angular doesn’t (cannot) update the bindings instantly. This is familiar from Angular 1 — we needed to call the dreaded $scope.apply() to trigger the digest loop, or wait for it to happen by itself.

The change detector

The Angular2 equivalent of this is ChangeDetectorRef#detectChanges(). Int the test environment, it’s exposed onCompnentFixture, and calling it will detect the changes and update the binings. Nice.

it( 'should put the uppercased version of the input field\'s input into'
+ 'the code element', () => {
// put our test string to the input element
fixture.debugElement.query(By.css('input')).nativeElement.value = 'test';
fixture.detectChanges();
// expect it to be the uppercase version
expect(fixture.debugElement
.query(By.css('code'))
.nativeElement
.textContent
).toEqual('TEST');
});

Result: Expected 'LOWERCASE' to equal 'TEST'. :(

A bit better, right? Now at least we have the initial value going through. The reason is that TestBed doesn’t run the change detector at all, unless asked to — so by default, not even the initial bindings are executed. (This behaviour can be changed with Fixture#autoDetectChanges.)

Triggering NgModel

What we want to see, is the new value we put in the input field, not the initial one. The issue is that even though we updated the value property, that doesn’t trigger NgModel’s binding by itself. It listens to the input event, which is dispatched only on actual user input.

All we need to do is to dispatch an InputEvent (did you know that’s a thing?) on element and we’re good to go. Right?

it( 'should put the uppercased version of the input field\'s input into'
+ 'the code element', () => {
  // put our test string to the input element
let element = fixture.debugElement.query(By.css('input')).nativeElement
element.value = 'test';
element.dispatchEvent(new Event('input'));
fixture.detectChanges();
  // expect it to be the uppercase version
expect(fixture.debugElement
.query(By.css('code'))
.nativeElement
.textContent
).toEqual('TEST');
});

Result: Expected 'LOWERCASE' to equal 'TEST'. :(

NgModel is asynchronous

Now this is where it gets funky. After a bit searching, I found a commit to the changelog which explains it: NgModel updates became asynchronous, so fixture.detectChanges() won’t be reflected instantly. We have to use theFixture#whenStable method, which gives us a promise to the stabilised state.

it( 'should put the uppercased version of the input field\'s input into'
+ 'the code element', () => {
fixture.detectChanges();
  // put our test string to the input element
let element = fixture.debugElement.query(By.css('input')).nativeElement;
element.value = 'test';
element.dispatchEvent(new Event('input'));
  fixture.whenStable().then(() => {
// expect it to be the uppercase version
expect(fixture.debugElement
.query(By.css('code'))
.nativeElement
.textContent
).toEqual('TEST');
});
});

Result (wait for it): It works!

Note that I moved the fixture.detectChanges() call to the top, because alhtough we don’t need it anymore after we make changes, we still need an initial call to build the initial state of our component.

Now we are using promises. You don’t need to have a wild imagination to see this would look like a mess on more complicated test scenarios, if we had to chain every exception after a promise.

Making it readable again

Thankfully, Angular 2 provides a utility, called fakeAsync, which magically allows us to turn our code sync again, with the help of the tick function. We just have to put the it callback into a fakeAsync wrapper, and we can “suspend” our flow until the async operations are ready.

Behold:

it( 'should put the uppercased version of the input field\'s input into'
+ 'the code element', fakeAsync(() => {
fixture.detectChanges();
  // put our test string to the input element
let element = fixture.debugElement.query(By.css('input')).nativeElement;
element.value = 'test';
element.dispatchEvent(new Event('input'));
  tick();
fixture.detectChanges();
  // expect it to be the uppercase version
expect(fixture.debugElement
.query(By.css('code'))
.nativeElement
.textContent
).toEqual('TEST');
}));

It looks fine now, doesn’t it?