When testing your Angular applications, make sure you don't forget to test your forms as well. But obviously forms, especially template-driven forms (ngModel
), are not something
you would write unit tests for. Instead, you want to write shallow component tests, where you get access
to native DOM elements that make up your application. Users will interact with your forms by typing, focusing, clicking, etc. so you want to trigger all those events in your tests as well.
Shallow test setup
Here's the setup for writing shallow component tests for MyComponent
:
describe('Component', () => {
let fixture: ComponentFixture<MyComponent>
let component: MyComponent
let debugElement: DebugElement
beforeEach(() => {
TestBed.configureTestingModule({
imports: [FormsModule],
declarations: [MyComponent],
}).compileComponents()
})
beforeEach(() => {
fixture = TestBed.createComponent(MyComponent)
component = fixture.componentInstance
debugElement = fixture.debugElement
})
// Specs
})
Now, in MyComponent
's template we have an input element with an ngModel
and a blur
event handler.
<input
id="myInput"
type="text"
[(ngModel)]="myInputValue"
(blur)="someMethod()"
/>
Potential tests that we may write for this input element is testing that the myInputValue
binding is valid and that someMethod
is called when the input loses focus.
In the test setup, we have already defined a debugElement
variable that will allow us to query the HTML of our component in order to get ahold of our input element.
// this spec isn't finished yet. no assertion.
it('should bind the input to the correct property', () => {
// first round of change detection
fixture.detectChanges()
// get ahold of the input
let input = debugElement.query(By.css('#myInput'))
let inputElement = input.nativeElement
})
Note: it's important to remember to trigger the initial round of change detection before trying to query the
debugElement
. Component's HTML only gets rendered out, after the first round of change detection.
.dispatchEvent()
Now we have access to the native input
element, which means we can set its value and assert that the two-way binding between myInputValue
in the component class and the input value is valid.
However, simply assigning a string to input's value
property is not enough, as that will not trigger a change event. Considering that ngModel
acts as an event listener, we need to
trigger an "input"
event ourselves, using .dispatchEvent
. We do that after we've assigned our input a value.
it('should bind the input to the correct property', () => {
// first round of change detection
fixture.detectChanges()
// get ahold of the input
let input = debugElement.query(By.css('#myInput'))
let inputElement = input.nativeElement
//set input value
inputElement.value = 'test value'
inputElement.dispatchEvent(new Event('input'))
expect(component.myInputValue).toBe('test value')
})
Which results in a passing test.
.dispatchEvent
has nothing to do with Angular, and is part of Web API.
With that in mind, we can now write our second test that asserts that someMethod
is called when the input loses focus.
it('should do something on blur', () => {
spyOn(component, 'someMethod')
// first round of change detection
fixture.detectChanges()
// get ahold of the input
let input = debugElement.query(By.css('#myInput'))
let inputElement = input.nativeElement
//set input value
inputElement.dispatchEvent(new Event('blur'))
expect(component.someMethod).toHaveBeenCalled()
})
So as you can see, we can trigger any event listeners on our elements with .dispatchEvent
.
Here is a list of all Javascript events.
Code example available here.