Testing Angular Container Components | Shallow & Deep
Typical Angular application renders and interacts with set of Container (Smart / Stateful) components, containing one or more re-usable Presentation (Dumb / Stateless) components.
Services contain Business Logic. Pipes and Store Selectors contain Re-usable Data Transformers / Functions.
Store pattern works well for managing State / Model. Ngrx is one such implementation for state management with reducers and effects handling state mutations and selectors handling retrieval.
Container components are responsible for wiring up, Services, data to/from Store, pass in @Input()
data to Presentation components for rendering and process @Output()
event(s) emitted by them by dispatching actions.
Services, Pipes , Store (Reducers & Selectors) are usually straight forward to unit test as they don’t involve any DOM rendering or async event-handling and mostly take pure functions approach for easy testability. (with the exception of Effects and Impure-Pipes).
If in tests, our components are instantiated as simple class instance without TestBed, we wouldn’t have Zone to test its bindings to template. We cannot test DOM rendering and event-handling.
Since a well designed component usually has all the business / state manipulation & selection logic abstracted away in services, store, store-reducers, store-selectors / pipes respectively, traditional unit tests wouldn’t have much to test, as component’s main responsibility then is to serve its template.
Testing of Container Components must be done at 2 levels, Shallow Test and Deep Test. First test any direct rendering / event-handling done by Container component, then its wiring to its Providers (ex store) and contained Presentation components respectively.
Consider an example <Details-Container/>
is a Container component, interacts with store to select and manage state slice details
, (ex. using ngrx store, state selectors and/or actions). It contains a Presentation component <Contact-Presentation/>
, which receives details.contact
data as @Input() contact
property for rendering, and emits @Output() onAdd.emit(contact.id)
event when user clicks on a Add to my Contacts <button/>
.
import { Component } from '@angular/core';
import { select, Store, Action } from '@ngrx/store';
@Component({
'selector': 'Details-Container',
'template': `<Details-Container *ngIf='details$ | async as details'>
<h3>{{details.title}}</h3>
<p>{{details.description}}</p>
<Location-Presentation [location]='details.address'/>
<Contact-Presentation
[contact]='details.contact'
(onAdd)='addContact($event)'
/>
</DetailsPage-Container>`
})
export class DetailsContainerComponent {
details$: Observable<any>;
constructor(private store: Store<any>) {
this.details$ = store.pipe(select(getDetailsSelector));
}
addContact(contactId: number) {
this.store.dispatch({
type: 'add_my_contact',
payload: contactId
} : Action)
}
}import { Component, Input, Output EventEmitter } from '@angular/core';
@Component({
'selector': 'Contact-Presentation',
'template': `<span>
<strong> {{contact.firstName}} |</strong>
<a href='mailto'> {{contact.email}} </a>
<button (click)='this.onAdd.emit(contact.id)'>
Add to My Contacts
</button>
</span>`
})
export class DetailsContainerComponent {
@Input() contact: {firstName: string, email?: string, id: number};
@Output() ontAdd = new EventEmitter<number>();
}
Testing Angular Components
Shallow Test
Our Objective in Shallow Test(s) is to assert Container Component render stuff it is responsible for rendering properly.
Its usual that we write our Container component before we figure out which presentation components to re-use or create to abstract away the DOM handling.
Therefore for TDD its important for us to write unit tests for the container component ignoring all inner components at first if any to focus on the testing the containing component in isolation.
The trick here is to configure TestBed
with only declarations: [DetailsContainerComponent]
Avoid the the contained presentation components with schemas: [NO_ERRORS_SCHEMA, CUSTOM_ELEMENTS_SCHEMA]
<Details-Container/>
instantiates properly.<Details-Container/>
, selectsdetails
data from store (it could beActivatedRoute
, some direct service, if you aren’t using store / Ngrx)<Details-Container/>
rendersdetails.title
anddetails.description
, re-renders with changes on change detection
import { TestBed, async } from '@angular/core/testing';
import { DetailsContainerComponent } from './details.component';
import { CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA } from '@angular/core';
import { BehaviourSubject } from 'rxjs';
import { select, Store } from '@ngrx/store';
describe('DetailsContainerComponent', () => {
let details: any;
let details$: BehaviourSubject<any>;
let store;
let fixture: ComponentFixture<DetailsContainerComponent>;
let comp: ContainerComponent;
beforeEach(async(() => {
details = {
title: 'This is a test title',
description: 'This is a test description`
contact: {
id: 1,
name: 'Simar',
email: 'simar@simar.simar'
}
};
details$ = new BehaviourSubject<any>(details);
TestBed.configureTestingModule({
declarations: [
DetailsContainerComponent
],
providers: [
provide: Store,
useValue: {
pipe: jasmine.createSpy('pipe').and.return(details$)},
dispatch: jasmine.createSpy('dispatch')
}
],
schemas: [CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA]
}).compileComponents();
fixture = TestBed.createComponent(DetailsContainerComponent);
comp = fixture.debugElement.componentInstance;
}));
it('should create the comp', async(() => {
expect(comp).toBeTruthy();
}));
it('should select details from store', async(() => {
expect(store.pipe).toHaveBeenCallWith(select(getContainerDetails));
expect(comp.details$).subscribe((d)=> expect(d).toBe(details)); // without jasmine marbles
expect(comp.details$).toBeObsevable('(a|)', {a: details}); // with jasmine marbles
}));
it('should render title in a h3 tag', async(() => {
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector('h3').textContent).toContain(details.title);
const title = 'Changed Title';
details$.next({...details, title});
fixture.detectChanges();
expect(compiled.querySelector('h3').textContent).toContain(changedTitle);
}));
it('should render description in a p tag', async(() => {
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;
const description = 'Changed Description'
details.next({...details, description});
fixture.detectChanges();
expect(compiled.querySelector('p').textContent).toContain(description);
}));
});
Deep Test
Our Objective in Deep Test(s) is to assert Interactions of Container Component with presentation Components, store, and any services if any.
<Container-Details\>
passes the correct@Input() contact
=details.location
to<Contact-Presentation/>
<Container-Details\>
can receive(onAdd)
event from<Contact-Presentation/>
& react by dispatchingadd-my-contact
action toStore
import { TestBed, async } from '@angular/core/testing';
import { DetailsContainerComponent } from './details.component';
import { ContactPresetationComponent } from './contact.component';
import { Store } from '@ngrx/store';
import { BehaviourSubject } from 'rxjs';
import { select, Store } from '@ngrx/store';
describe('DetailsContainerComponent', () => {
let details : any;
let details$: BehaviourSubject<any>;
let store;
let fixture: ComponentFixture<DetailsContainerComponent>;
let comp: ContainerComponent;
beforeEach(async(() => {
details = {
title: 'This is a test title',
description: 'This is a test description`
contact: {
id: 1,
name: 'Simar',
email: 'simar@simar.simar'
}
};
details$ = new BehaviourSubject<any>(details);
TestBed.configureTestingModule({
declarations: [
DetailsContainerComponent
],
providers: [
provide: Store,
useValue: {
pipe: jasmine.createSpy('pipe').and.return(details$)},
dispatch: jasmine.createSpy('dispatch')
}
]
}).compileComponents();
fixture = TestBed.createComponent(DetailsContainerComponent);
comp = fixture.debugElement.componentInstance;
}));
it('should pass contact.details as inpiut to <Contact-Presentation/>', async(() => {
fixture.detectChanges();
const de = fixture.debugElement;
const contactEl = de.query(By.css('Contact-Presentation'));
const contactComp = contactEl.componentInstance as ContactPresetationComponent;
expect(contactComp).toBeTruhty();
expect(contactComp.contact).toEqual(details.contact);
const contact = { firstName: 'Paul', email: 'paul@paul.com' }
details.next({...details, contact});
fixture.detectChanges();
expect(contactComp.contact).toEqual(contact);
}));
it('should listen to onAdd() event from <Contact-Presentation/>', async(() => {
fixture.detectChanges();
const de = fixture.debugElement;
const contactEl = de.query(By.css('Contact-Presentation'));
const contactComp = contactEl.componentInstance as ContactPresetationComponent;
expect(contactComp).toBeTruhty();
spyOn(comp,'addContact').and.callThrough();
const contactId = 10;
contactComp.onAdd.emit(contactId);
expect(comp.addContact).toHaveBeenCalledWith(contactId);
expect(store.dispatch).toHaveBeenCalledWith({
type: 'add_my_contact',
payload: 10
} : Action);
}));
});
How deep should we test?
We can always test the rendering and event handling of inner components in deep tests for container components. However, it is necessary only if we are not writing any shallow tests for those inner components. Assuming we write shallow tests for all components, unit tests for store reducers, selectors and effects, writing deep tests for container components to just test the interactions with its providers and inner components is reasonable.
Let us consider we don’t have a shallow test for <Contact-Presentation>
, and we don't need writing one as it is not going to be used anywhere other than <Details-Container>
We could re-write the last two tests as follows.
it('should pass contact.details as inpiut to <Contact-Presentation/>', async(() => {
fixture.detectChanges();
const contactComp = fixture.debugElement.query(By.css('Contact-Presentation'));
expect(contactComp).toBeTruhty();
const contactEl = contactComp.nativeElement;
// check rendering on contact element
expect(contactEl.querySelector('strong').textContent).toContain(details.contact.firstName);
expect(contactEl.querySelector('a').textContent).toContain(details.contact.email);
const contact = { firstName: 'Paul', email: 'paul@paul.com' }
details.next({...details, contact});
fixture.detectChanges();
expect(contactEl.querySelector('strong').textContent).toContain(contact.firstName);
expect(contactEl.querySelector('a').textContent).toContain(contact.email);
}));
it('should listen to onAdd() event from <Contact-Presentation/>', async(() => {
fixture.detectChanges();
const contactComp = fixture.debugElement.query(By.css('Contact-Presentation'));
expect(contactComp).toBeTruhty();
spyOn(comp,'addContact').and.callThrough();
const contactId = 10;
// click event triggerred from contact elemnt button
contactComp.nativeElement.querySelector('button').click();
expect(comp.addContact).toHaveBeenCalledWith(contactId);
expect(store.dispatch).toHaveBeenCalledWith({
type: 'add_my_contact',
payload: 10
} : Action);
}));
Its more maintainable to test DOM rendering and event handling of <Contact-Presentation/>
in its own spec and not have the same tests in the <Details-Container/>
Presentation components are re-usable and may be used by many other components. It is a maintenance overhead when the same component for same cases gets tested in many different places.
If you are concerned about how the whole component tree renders, E2E (End to End Testing) tests are a better solution than complicating our deep tests for containers.
Check out Protractor , a popular framework / tool for Angular end to end testing E2E .
Shallow and Deep Tests fall under Unit Testing which are much easier to write, run faster and are self-contained which makes them more maintainable than E2E Tests
E2E Tests are great for high-level validation of the entire system. But they can’t give you the comprehensive test coverage that you would expect from Unit Tests.
Originally published at www.simars.io.