Angular components, in my view, are everything. As the Angular documentation team puts it –
Components are the most basic UI building block of an Angular app.
https://angular.io/api/core/Component
Components help to manage the lifecycle of a view in Angular. They coordinate the passing of information in an Angular app to a view. At their basic level, components are directives, or we should say a subset of. When we want a component to “come alive” we add it into an ngModule in the declarations field.
In the example below, we’re going to set ourselves up for testing components that have nested components. As an Angular app, again going back to the Angular documentation team’s definition, is a tree of Angular components, we want to be sure we understand how to only test the functionality within a single component at a time. The way we handle this is to create stubs for (injected) components. **in the following example, we’re not actually injecting anything into our system under test (SUT), but our SUT does rely on another component. In this example, that is what I’m referring to by an (injected) component.
Stubs can be thought of as a shell of functionality. With stubs, as with any test double, we want to isolate and test state only and remove the complexity of behavior. This leads to us often hard coding responses, such as return true or false, directly into the stub. The idea, again, is to isolate the system under test (SUT).
Before we get on with it, one more comment. There are many ways to test functionality. We could use other types of test doubles, such as fakes, mocks, and spies. In this article, we are only concerned with isolating the SUT using a stub. Also, deeper testing using stubs will be done at a later time.
So let’s get to it!
In this example, we’re not touching our declarations in the original module.ts file. It’s contents, below, show that we have not in any way included our stub. What we’ll be stubbing is the aptly named TobestubbedComponent.
import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FunctionalityComponent } from './functionality/functionality.component'; import { TobestubbedComponent } from './tobestubbed/tobestubbed.component'; import { ComponentStubRoutingModule } from './componentstub-test-routing.module'; @NgModule({ declarations: [FunctionalityComponent, TobestubbedComponent], // we need to add our routing module here imports: [CommonModule, ComponentStubRoutingModule] }) export class ComponentstubTestModule {}
Here’s our TobestubbedComponent, in all its glory. The example here is very simple to try and demonstrate the idea without too much crud. Testing a larger component with more functionality will take more thought during design.
Our component here only emits a value called sentData when a button in the template is clicked.
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; @Component({ selector: 'app-tobestubbed', templateUrl: './tobestubbed.component.html', styleUrls: ['./tobestubbed.component.css'] }) export class TobestubbedComponent implements OnInit { // This is coming from our parent, the functionality component, which will be under test @Input() inputData: string; // This is going out to our parent, the functionality component, which will be under test // See below where we trigger the emit in sendData() from the button in html @Output() sentData = new EventEmitter<string>(); constructor() { } ngOnInit() { } sendData(data: string) { this.sentData.emit(data); } }
Here’s our very simple template.
<p>Tobestubbed input data: {{ inputData }}</p> <button (click)="sendData('Data sent from child')">Send Data</button>
Now here’s our stub in this case. You’ll notice that it looks nearly identical to the original. This is due to us not having much functionality in the original to begin with. If we had more functionality in our original component, we would strip out a lot of the behavior, and simple return hard coded values. This is to limit the amount of code that might break during testing.
Here, I don’t care about testing the actual button click, I want minimal functionality and complexity, so I “simulate” the button click by calling our sendData method in the ngOnInit method. In this case I’m also not worried about changing the value that’s sent from the stub. That can be easily accomplished, and we’ll tackle that in other posts.
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; import { Observable } from 'rxjs'; @Component({ selector: 'app-tobestubbed', template: '', styles: [''] }) export class ToBeStubbedStubComponent implements OnInit { // This is coming from our parent, the functionality component, which will be under test @Input() inputData: string; // This is going out to our parent, the functionality component, which will be under test // See below where we trigger the emit in sendData() from the button in html @Output() sentData = new EventEmitter<string>(); constructor() {} ngOnInit() { this.sendData('Stub sent data'); } sendData(data: string) { this.sentData.emit(data); } }
There’s not much to say about the SUT, it’s very simple. So I’ll include it here only for reference.
import { Component, OnInit, Input, Output } from '@angular/core'; @Component({ selector: 'app-functionality', templateUrl: './functionality.component.html', styleUrls: ['./functionality.component.css'] }) export class FunctionalityComponent implements OnInit { // This will eventually come from our stub, and is shown in the template inputData: string; outputData: string; constructor() { } ngOnInit() { this.outputData = 'Functionality component output data'; } // This processes the data coming from our stub when triggered onDataReceived(data: string) { this.inputData = data; } }
And its template.
<p>Functionality input data: {{ inputData }}</p> <!-- sentData is the name of the output emitter from our child --> <!-- inputData is the name of the var in the stub that's receiving the ouput from this component , which is stored in outputData --> <app-tobestubbed (sentData)="onDataReceived($event)" [inputData]=outputData></app-tobestubbed>
Again, we’re not testing much as the purpose of this code is to show how to handle testing components with stubbed dependencies. All we do here is import our stub, called ToBeStubbedStubComponent, and then add it to our declarations. Really that’s it! We’ve now removed any functionality in our original dependent component. To prove this, we console.logged the FunctionalityComponent. The result, as expected in our Karma window, was that inputData now says “Stub Sent Data”
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { FunctionalityComponent } from './functionality.component'; // we can use this to get rid of errors before we stub import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ToBeStubbedStubComponent } from '../testing/tobestubbed-stub'; describe('FunctionalityComponent', () => { let component: FunctionalityComponent; let fixture: ComponentFixture<FunctionalityComponent>; beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [ FunctionalityComponent, ToBeStubbedStubComponent ], // we can use this to get rid of errors before we stub // goes along with the NO_ERRORS_SCHEMA import // schemas: [NO_ERRORS_SCHEMA] }) .compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(FunctionalityComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); });
This is the simplest example of testing a stubbed component that I could think of. Again, I wanted to keep behavior to a minimum to show how to get the test rigged up. If you were to run the test without adding the stub to Declarations, you would see our failures around custom elements and our @Input inputData. Then, pulling the emitter from our test stub you would see errors about our @Output sentData not being initialized.
At this point, you should have the basic testing functionality in place to remove any errors related to our stubbed component. You also now have the framework to start that deeper level testing, such as using spys and simulating click events, to get to more meaningful testing.
All of the code shown here, and more Angular tests, can be found in this GitHub repo.