In this article, I will provide a collection of some important statements used to unit test angular components. You can use any of the following examples directly in your project, or you may prefer to extract some of them into separate helper functions and reuse them all over your project. This article covers testing the following scenarios:
- Text interpolation
- User Input Value Change
- Clicking HTML Element
- Access Child (nested) Component
- Content projection
- Component inputs and outputs
- Component Dependencies
For this purpose lets assume we have the following simple example component generated using Angular CLI ng g c ExampleComponent:
<h1>{{ header }}</h1> | |
<p>{{ name }}</p> | |
<form (ngSubmit)="submitForm()"> | |
<label for="name">Name:</label> | |
<input type="text" name="name" [(ngModel)]="name" /> | |
<input type="submit" value="Submit" /> | |
</form> |
import { ComponentFixture, TestBed } from '@angular/core/testing'; | |
import { ExampleComponent } from './example-component.component'; | |
describe('ExampleComponent', () => { | |
let component: ExampleComponent; | |
let fixture: ComponentFixture<ExampleComponent>; | |
beforeEach(async () => { | |
await TestBed.configureTestingModule({ | |
declarations: [ ExampleComponent ] | |
}) | |
.compileComponents(); | |
}); | |
beforeEach(() => { | |
fixture = TestBed.createComponent(ExampleComponent); | |
component = fixture.componentInstance; | |
fixture.detectChanges(); | |
}); | |
it('should create', () => { | |
expect(component).toBeTruthy(); | |
}); | |
}); |
@Component({ | |
selector: 'app-example-component', | |
templateUrl: './example-component.component.html', | |
styleUrls: ['./example-component.component.scss'] | |
}) | |
export class ExampleComponent{ | |
@Input() header = 'Initial Header'; | |
@Output() nameChange: EventEmitter<string> = new EventEmitter(); | |
public name = 'Initial Name'; | |
submitForm() { | |
this.nameChange.emit(this.name); | |
} | |
} |
A very basic component consists of one input header
and one property name
displayed in the template using a direct interpolation, a form with one input field and a submit button and one output nameChange
which will emit an event when the user submits the form.
When you create the above component using Angular CLI you will get automatically a unit test file in the same directory as your component. All the next sections in this article are based on this file, especially the fixture object let fixture: ComponentFixture;
. If you don't use Angular CLI to generate your component file, you may copy the above file in your project, and replace ExampleComponent
with your component class name.
Text interpolation:
Here we make sure that our component will bind the correct values in the template. Don't forget to call fixture.detectChanges()
which forces the TestBed to perform data binding and update the view.
it('should bind the header and the name correctly', () => { | |
let headerElement: HTMLHeadElement = fixture.debugElement.query(By.css('h1')).nativeElement; | |
let nameElement: HTMLHeadElement = fixture.debugElement.query(By.css('p')).nativeElement; | |
component.header = 'test header'; | |
component.name = 'test name'; | |
fixture.detectChanges(); | |
expect(headerElement.textContent).toEqual(component.header); | |
expect(nameElement.textContent).toEqual(component.name); | |
}); |
User Input Value Change:
Here we test that the user interaction with the text input is reflected correctly into our component class. Notice here the use of fakeAsync and tick, because the forms binding involves some asynchronous execution.
it('should update the model when the user change the input value and vice versa', fakeAsync(() => { | |
let inputElement: HTMLInputElement = fixture.debugElement.query(By.css('input[type=text]')).nativeElement; | |
// Changes from the model to the input | |
component.name = 'new name'; | |
fixture.detectChanges(); | |
tick(); | |
expect(inputElement.value).toEqual(component.name); | |
// Changes from input to the model | |
inputElement.value = 'new name 2'; | |
inputElement.dispatchEvent(new Event('input')) | |
expect(component.name).toEqual('new name 2'); | |
})); |
Clicking HTML Element:
it('should trigger submitForm when clicking the submit button', () => { | |
let inputElement: HTMLButtonElement = fixture.debugElement.query(By.css('input[type=submit]')).nativeElement; | |
spyOn(component, 'submitForm').and.callThrough(); | |
inputElement.click(); | |
expect(component.submitForm).toHaveBeenCalled(); | |
}); |
Access Child (nested) Component:
Lets assume that our component contains a nested child component:
<app-nested-component></app-nested-component>
You can access the child component and interact it as the following:
it('should display a nested component', () => { | |
let nestedDebugElement: DebugElement = fixture.debugElement.query(By.directive(NestedComponent)); | |
let nestedInstance = nestedDebugElement.componentInstance; | |
expect(nestedDebugElement).toBeTruthy(); | |
// You can also access the nested instance properties | |
expect(nestedInstance.property).toEqual('property value'); | |
}); |
Content projection:
Testing content projection is not straightforward, to do so we need to add a wrapper component around the component being tested and use this wrapper component to pass content through projection. Let's add the following projected content to the view of our component
<div class="projected-content>
<ng-content select="[description]"></ng-content>
</div>
And we can test is by adding a wrapper ExampleWrapperComponent
as the following:
@Component({ | |
selector: 'app-example-component-wrapper', | |
template: `<app-example-component> | |
<ng-container description>{{projectedContent}}</ng-container> | |
</app-example-component>` | |
}) | |
export class ExampleWrapperComponent{ | |
public projectedContent = ''; | |
} | |
describe('ExampleComponent', () => { | |
let wrapper: ExampleWrapperComponent; | |
let component: ExampleComponent; | |
let fixture: ComponentFixture<ExampleWrapperComponent>; | |
beforeEach(async () => { | |
await TestBed.configureTestingModule({ | |
imports: [FormsModule], | |
declarations: [ ExampleWrapperComponent, ExampleComponent ] | |
}) | |
.compileComponents(); | |
}); | |
beforeEach(async () => { | |
fixture = TestBed.createComponent(ExampleWrapperComponent); | |
wrapper = fixture.componentInstance; | |
component = fixture.debugElement.query(By.directive(ExampleComponent)).componentInstance; | |
fixture.detectChanges(); | |
}); | |
it('should display the projected content correctly', () => { | |
let projectedDiv: HTMLDivElement = fixture.debugElement.query(By.css('.projected-content')).nativeElement; | |
expect(projectedDiv.textContent).toEqual(''); | |
wrapper.projectedContent = 'new projected content'; | |
fixture.detectChanges(); | |
expect(projectedDiv.textContent).toEqual(wrapper.projectedContent); | |
}); | |
}); |
Component inputs and outputs:
You can test component input similar to any normal component property. on the other hand the outputs can be spied on and check if it emits the correct value.
it('should bind the name input correctly', () => { | |
let nameELement: HTMLButtonElement = fixture.debugElement.query(By.css('p')).nativeElement; | |
component.name = 'test name'; | |
fixture.detectChanges(); | |
expect(nameELement.textContent).toEqual(component.name); | |
}); | |
it('should emit the correct value when the submitForm triggered', () => { | |
spyOn(component.nameChange, 'emit').and.callThrough(); | |
component.name = 'test name'; | |
component.submitForm(); | |
expect(component.nameChange.emit).toHaveBeenCalledWith(component.name); | |
}); |
Component Dependencies:
Components usually have dependencies (services) that help the component to function correctly, and the component needs to interact with these dependencies. When testing a component we need to provide our tests with those dependencies in order to run correctly. Here we need to distinguish between two way of providing a dependency:
Dependencies provided in the root injector:
When the component has a dependency on a service that is provided in the root injector, you need to provide this service to the TestBed configuration to be available to the component while running the tests:
// Service is provided in the root injector | |
@Injectable({ | |
providedIn: 'root', | |
}) | |
export class ExampleService {... |
beforeEach(async () => { | |
await TestBed.configureTestingModule({ | |
imports: [FormsModule], | |
declarations: [ ExampleComponent ], | |
providers:[ | |
{ | |
// It is better to provide a mock service in your tests | |
provide: Example, useClass: ExampleServiceMock | |
} | |
] | |
}) | |
.compileComponents(); | |
}); | |
... | |
it('should have access to the provided service', () => { | |
let service: ExampleService = TestBed.inject(ExampleService); | |
expect(service).not.toBeNull() | |
}); |
Notice that we are using a mock service here since it is easier and safer to interact with. After that, you will be able to access that service in your tests by calling the inject
method of the TestBed
.
Dependencies provided in the component injector:
When you have a dependency provided in your component, you can not access it using the TestBed, since it will be available only on the component level of the injection tree. In this case, we need to override the component providers to provide this dependency, and then you can use the component injector to access it.
@Component({ | |
selector: 'app-example-component', | |
templateUrl: './example-component.component.html', | |
styleUrls: ['./example-component.component.scss'], | |
providers: [ExampleService] // service provided in the component injector | |
}) | |
export class ExampleComponent implements OnInit { |
beforeEach(async () => { | |
await TestBed.configureTestingModule({ | |
imports: [FormsModule], | |
declarations: [ ExampleComponent ], | |
}) | |
// overriding the component providers | |
.overrideComponent(ExampleComponent, { | |
set: { | |
providers: [ | |
{provide: ExampleService, useClass: ExampleServiceMock} | |
] | |
} | |
}) | |
.compileComponents(); | |
}); | |
... | |
it('should have access to the component provided service', () => { | |
// Access the service from the component injector | |
let service: ExampleService = fixture.debugElement.injector.get(ExampleService); | |
expect(service).not.toBeNull() | |
}); |
Do you have or need a specific testing scenario that is not covered by this article? Feel free add it in the comments sections and we will add a use case for you :)
Top comments (6)
I have a question about the purpose of tick(); ?
tick is just a simulation of passage of time, for example you can use tick(2000) to simulate a passing of 2 seconds you can check more about it here angular.io/api/core/testing/tick
Did you tried this package: npmjs.com/package/ng-mocks before?
Thank you
And another question about {provide: ExampleService, useClass: ExampleServiceMock} : ExampleServiceMock it's implemneted from scratch ? or we can use mock test library
It is up to you, you can create a new class that matches the real ExampleService and mock the members (properties and methods). you can use a mocking library if you want, but anyway it is up to you, depending on the logic of the service, to decide how the mock service should behave