When you generate a new Angular component, you get a test like this:
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SkrunchWiggleComponent } from './skrunch-wiggle.component';
describe('SkrunchWiggleComponent', () => {
let component: SkrunchWiggleComponent;
let fixture: ComponentFixture<SkrunchWiggleComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [SkrunchWiggleComponent]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(SkrunchWiggleComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
When we call fixture.detectChanges()
in the beforeEach
, that causes Angular
to call ngOnInit
. If you need to set up mock services that will be called in
the ngOnInit
method of the component, there is no good place to put those
mocks in this test. If you put it inside the test body, it’s too
late – ngOnInit
has already been called. If you put it in beforeEach
, it
applies to all tests. You could call ngOnInit
directly in your test, but then
it is invoked multiple times, which is not a good imitation of what happens in
production. Depending on what you’re doing in the init method, calling twice
could either cause or mask issues that don’t happen when it is called once.
In general, we avoid directly calling any component lifecycle methods so that our tests are more faithful to what really happens in production.
initialize Fixture
To avoid directly calling ngOnInit
, you can postpone the first call
to fixture.detectChanges()
until after your mocks are set up. We use the “initializeFixture” pattern for
this.
function initializeFixture(options: { itemId?: number } = {}) {
const fixture = TestBed.createComponent(SkrunchWiggleComponent);
const component = fixture.componentInstance;
component.itemId = options.itemId || 1;
const element = fixture.nativeElement as HTMLElement;
fixture.detectChanges();
return { component, fixture, element };
}
Some observations about initializeFixture
:
- you can add any parameters needed to initialize the fixture
- it can be helpful to provide default values so you don’t have to provide parameters that are not relevant to your test
- it calls
fixture.detectChanges()
- it casts
fixture.nativeElement
toHTMLElement
so thatelement
does not haveany
type
Use initializeFixture
like so:
it('styles the title differently when this item is edited', () => {
wiggleService.getDirectionOfRotation.and.returnValue('CLOCKWISE');
const { element } = initializeFixture({ itemId });
expect(element.textContent).toContain('Skrunch Wiggle Clockwise');
});
The example above deals with the most common lifecycle hook to call in
test: ngOnInit
.
ng On Changes
If you need to test ngOnChanges
, a natural guess would be to change an
input with component.myInput = 'new value'
and you would expect ngOnChanges
to be called. But the test bed does not call it automatically like that.
To cause the test bed to call ngOnChanges
, you would need an example
parent component. When you change the value on the parent, it passes
it to the child (the test subject) and Angular calls ngOnChanges
for
you. Here is an example parent component which you could write in your
test file.
@Component({
selector: 'test-subject',
template: `
<app-skrunch-wiggle [itemId]="itemId">
</app-skrunch-wiggle>
`
})
class ExampleParent {
@Input() itemId!: number;
}
In your test, you add the example parent in the declarations
block for
the TestBed.configureTestingModule()
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ExampleParent, SkrunchWiggleComponent],
}).compileComponents();
});
When you initialize the fixture, use the example parent:
const fixture = TestBed.createComponent(ExampleParent);
Now, when you change the ExampleParent
component’s itemId
input,
Angular calls the ngOnChanges
in the test subject (child). This
lets you execute the ngOnChanges
in test organically.
Overall, this ExampleParent approach lets you test what happens to the child if the parent does one thing or another to it. “If a parent component did X to you, how would you behave?”
Mocking components
Another pattern that is similar to the ExampleParent is mocking a
component. This is useful if the component has complex dependencies that you
don’t need as part of your test, or if it has some runtime behavior that you
don’t want as part of your test. You can create a component in the spec file
that has the same selector as the component you want to mock. In
the declarations
section of your TestBed setup, use the fake component instead
of the real one.
Instead of:
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [SkrunchWiggleComponent, WiggleIconComponent],
}).compileComponents();
})
use a fake component
@Component({
selector: 'app-wiggle-icon',
template: ''
})
class FakeWiggleIconComponent {
@Input() direction!: string;
}
describe('ScrunchWiggleComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
// replace the real icon with the fake one:
declarations: [SkrunchWiggleComponent, FakeWiggleIconComponent],
}).compileComponents();
})
})
Now, when you render the SkrunchWiggle component, it will render the fake wiggle icon component instead of the real thing. You can assert on the inputs to the fake wiggle like you would have asserted on the inputs to the real component.
Asserting on child component inputs
When you have a fake child, it is sometimes helpful to assert on the inputs passed to the child. You can do that like so:
it('passes the direction to the wiggle icon', ()=> {
wiggleService.getDirectionOfRotation.and.returnValue('COUNTERCLOCKWISE');
const { fixture } = initializeFixture({ itemId });
const icon = fixture.debugElement.query(By.directive(FakeWiggleIconComponent)).injector.get(FakeWiggleIconComponent);
expect(icon.direction).toEqual('COUNTERCLOCKWISE');
})