Exploring the process of the unit- and e2e-testing the Tour of Heroes application. Focusing on the connection between application progression and test development changes.
AppComponent is the application shell. There is not much to test, at this point.
The default unit- and e2e-tests generated by the ng new
command contains a few guiding sanity-tests, as shown below.
import { TestBed, async } from '@angular/core/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
AppComponent
],
}).compileComponents();
}));
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy();
});
it(`should have as title 'angular-tour-of-heroes'`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app.title).toEqual('angular-tour-of-heroes');
});
it('should render title', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector('.content span').textContent)
.toContain('angular-tour-of-heroes app is running!');
});
});
import { AppPage } from './app.po';
import { browser, logging } from 'protractor';
describe('workspace-project App', () => {
let page: AppPage;
beforeEach(() => {
page = new AppPage();
});
it('should display welcome message', () => {
page.navigateTo();
expect(page.getTitleText()).toEqual('angular-tour-of-heroes app is running!');
});
afterEach(async () => {
// Assert that there are no errors emitted from the browser
const logs = await browser.manage().logs().get(logging.Type.BROWSER);
expect(logs).not.toContain(jasmine.objectContaining({
level: logging.Level.SEVERE,
} as logging.Entry));
});
});
Once we clear the content of app.component.html
to build a new app, the tests will start fail, because there is no HTML content.
1) should render title
AppComponent
TypeError: Cannot read property 'textContent' of null
at <Jasmine>
at UserContext.<anonymous> (http://localhost:9876/_karma_webpack_/src/app/app.component.spec.ts:29:51)
at ZoneDelegate.invoke (http://localhost:9876/_karma_webpack_/node_modules/zone.js/dist/zone-evergreen.js:359:1)
at ProxyZoneSpec.push../node_modules/zone.js/dist/zone-testing.js.ProxyZoneSpec.onInvoke (http://localhost:9876/_karma_webpack_/node_modules/zone.js/dist/zone-testing.js:308:1)
at ZoneDelegate.invoke (http://localhost:9876/_karma_webpack_/node_modules/zone.js/dist/zone-evergreen.js:358:1)
at Zone.run (http://localhost:9876/_karma_webpack_/node_modules/zone.js/dist/zone-evergreen.js:124:1)
at runInTestZone (http://localhost:9876/_karma_webpack_/node_modules/zone.js/dist/zone-testing.js:561:1)
at UserContext.<anonymous> (http://localhost:9876/_karma_webpack_/node_modules/zone.js/dist/zone-testing.js:576:1)
at <Jasmine>
workspace-project App
โ should display welcome message
- Failed: No element found using locator: By(css selector, app-root .content span)
at elementArrayFinder.getWebElements.then (/Users/girmae.nigusse/WebstormProjects/angular-tour-of-heroes/node_modules/protractor/built/element.js:814:27)
at ManagedPromise.invokeCallback_ (/Users/girmae.nigusse/WebstormProjects/angular-tour-of-heroes/node_modules/selenium-webdriver/lib/promise.js:1376:14)
at TaskQueue.execute_ (/Users/girmae.nigusse/WebstormProjects/angular-tour-of-heroes/node_modules/selenium-webdriver/lib/promise.js:3084:14)
at TaskQueue.executeNext_ (/Users/girmae.nigusse/WebstormProjects/angular-tour-of-heroes/node_modules/selenium-webdriver/lib/promise.js:3067:27)
at asyncRun (/Users/girmae.nigusse/WebstormProjects/angular-tour-of-heroes/node_modules/selenium-webdriver/lib/promise.js:2927:27)
at /Users/girmae.nigusse/WebstormProjects/angular-tour-of-heroes/node_modules/selenium-webdriver/lib/promise.js:668:7
at process._tickCallback (internal/process/next_tick.js:68:7)Error:
We will remove the falling tests and keep the rest for bootstraping our future tests.
import { TestBed, async } from '@angular/core/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
AppComponent
],
}).compileComponents();
}));
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy();
});
it(`should have as title 'angular-tour-of-heroes'`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app.title).toEqual('angular-tour-of-heroes');
});
- it('should render title', () => {
- const fixture = TestBed.createComponent(AppComponent);
- fixture.detectChanges();
- const compiled = fixture.debugElement.nativeElement;
- expect(compiled.querySelector('.content span').textContent)
- .toContain('angular-tour-of-heroes app is running!');
- });
});
import { AppPage } from './app.po';
import { browser, logging } from 'protractor';
describe('workspace-project App', () => {
let page: AppPage;
beforeEach(() => {
page = new AppPage();
});
- it('should display welcome message', () => {
- page.navigateTo();
- expect(page.getTitleText()).toEqual('angular-tour-of-heroes app is running!');
- });
afterEach(async () => {
// Assert that there are no errors emitted from the browser
const logs = await browser.manage().logs().get(logging.Type.BROWSER);
expect(logs).not.toContain(jasmine.objectContaining({
level: logging.Level.SEVERE,
} as logging.Entry));
});
});
Now the unit-tests should all pass. For e2e-test there will be no test to pass or fail, but the test will run successfully.
17 11 2019 13:50:47.437:INFO [Chrome 78.0.3904 (Mac OS X 10.15.1)]:
Connected on socket BxH0NdUALyuLhlXKAAAA with id 55646172
AppComponent
โ should create the app
โ should have as title 'angular-tour-of-heroes'
Chrome 78.0.3904 (Mac OS X 10.15.1): Executed 2 of 2 SUCCESS (0.053 secs / 0.046 secs)
TOTAL: 2 SUCCESS
[13:48:13] I/launcher - Running 1 instances of WebDriver
[13:48:13] I/direct - Using ChromeDriver directly...
Jasmine started
Executed 0 of 0 specs SUCCESS in 0.004 sec.
[13:48:16] I/launcher - 0 instance(s) of WebDriver still running
[13:48:16] I/launcher - chrome #01 passed
This should tell us we are ready to start testing our application, using the provided unit- and e2e-test configuration !!!
At this point, we can set up our continuous integration (CI) for building and test as we progress the application. CI will be our get keeper not to push code if the code fails to pass linting, unit or e2e-tests.
Install karma-spec-reporter
and angular-cli-ghpages
in your devDependecies
. The first will generate unit-test results in console, and the later will be used to deploy your app in github-pages. karma-spec-reporter
needs to required in your karma.conf.js
and also add as a reporter, see here.
We will deploy the app from our local, using npm run deploy
, hence we don't need to store our github auth-secret in Travis-ci.
{
"name": "angular-tour-of-heroes",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"lint": "ng lint",
"test": "ng test",
"e2e:update-webdriver": "npx webdriver-manager update --gecko=false",
"e2e:local": "ng e2e",
"e2e:travis": "npx protractor --capabilities.chromeOptions.args=--headless e2e/protractor.conf.js",
"start": "ng serve",
"build:hgpages": "ng build --prod --base-href=\"/angular-tour-of-heroes/\"",
"deploy": "npm run build:hgpages && npx ngh --dir=dist/angular-tour-of-heroes"
},
"private": true,
"dependencies": {
"@angular/animations": "8.2.14",
"@angular/common": "8.2.14",
"@angular/compiler": "8.2.14",
"@angular/core": "8.2.14",
"@angular/forms": "8.2.14",
"@angular/platform-browser": "8.2.14",
"@angular/platform-browser-dynamic": "8.2.14",
"@angular/router": "8.2.14",
"rxjs": "6.5.3",
"tslib": "1.10.0",
"zone.js": "0.10.2"
},
"devDependencies": {
"@angular-devkit/build-angular": "0.803.19",
"@angular/cli": "8.3.19",
"@angular/compiler-cli": "8.2.14",
"@angular/language-service": "8.2.14",
"@types/jasmine": "3.4.6",
"@types/jasminewd2": "2.0.8",
"@types/node": "8.10.59",
"angular-cli-ghpages": "0.6.0",
"codelyzer": "5.2.0",
"jasmine-core": "3.5.0",
"jasmine-spec-reporter": "4.2.1",
"karma": "4.4.1",
"karma-chrome-launcher": "2.2.0",
"karma-coverage-istanbul-reporter": "2.1.0",
"karma-jasmine": "2.0.1",
"karma-jasmine-html-reporter": "1.4.2",
"karma-spec-reporter": "0.0.32",
"protractor": "5.4.2",
"ts-node": "7.0.1",
"tslint": "5.20.1",
"typescript": "3.5.3"
}
}
Below is the script you need to run the application in the background, run your unit- and e2e-tests in headless Chrome using Travis-ci.If you want Travis-ci also make the Continous Deplymen (CD) add the deploy
script as documented here and your github token in the build configuration parameters.
language: node_js
node_js:
- "10.16.3"
addons:
chrome: stable
branches:
only:
- master
before_script:
- npm install -g @angular/cli
script:
- ng lint
- npm run start &
- npm test -- --watch false --browsers ChromeHeadless
- npm run e2e:update-webdriver
- npm run e2e:travis
If you would like to use Github Actions as your CD, use the below configuration.
name: Node CI
on: [push]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [10.x, 12.x]
steps:
- uses: actions/checkout@v1
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: npm install, lint, unit test and e2e tests
run: |
npm install -g @angular/cli
npm install
npm run lint
npm run start &
npm test -- --watch false --browsers ChromeHeadless
npm run e2e:update-webdriver
npm run e2e:travis
env:
CI: true
Our App begins here. First thing we change the title. See details here
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
- title = 'angular-tour-of-heroes';
+ title = 'Tour of Heroes';
}
<h1>{{title}}</h1>
As expected, the unit test assertion we have for the title fails. We could simply update the expected value with the new title and test will pass.
import { TestBed, async } from '@angular/core/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
AppComponent
],
}).compileComponents();
}));
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy();
});
it(`should have as title 'Tour of Heroes'`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app.title).toEqual('Tour of Heroes');
});
});
For the e2e-test this is the time to add a test for the title. First we will element seelctor and text 'getter' in the page-object
then add assertion for the new title in our test (spec
).
import { browser, by, element } from 'protractor';
export class AppPage {
navigateTo() {
return browser.get(browser.baseUrl) as Promise<any>;
}
getTitleText() {
- return element(by.css('app-root .content span')).getText() as Promise<string>;
+ return element(by.css('h1')).getText() as Promise<string>;
}
}
import { AppPage } from './app.po';
import { browser, logging } from 'protractor';
- describe('workspace-project App', () => {
+ describe('AppComponent', () => {
let page: AppPage;
beforeEach(() => {
page = new AppPage();
});
+ it('should display title', () => {
+ page.navigateTo();
+ expect(page.getTitleText()).toEqual('Tour of Heroes');
+ });
afterEach(async () => {
// Assert that there are no errors emitted from the browser
const logs = await browser.manage().logs().get(logging.Type.BROWSER);
expect(logs).not.toContain(jasmine.objectContaining({
level: logging.Level.SEVERE,
} as logging.Entry));
});
});
Both your unit- and e2e-tests are passing. Now you could run your app and see the title.
Now we will use HeroesComponent to show hero information and insert that in the application-shell(the AppComponent). See details here
Generate the HeroesComponent; unit and e2e-test are stil passing. Let us build the HeroesComponent
.
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-heroes',
templateUrl: './heroes.component.html',
styleUrls: ['./heroes.component.css']
})
export class HeroesComponent implements OnInit {
+ hero = 'Windstorm';
constructor() { }
ngOnInit() {
}
}
- <p>heroes works!</p>
+ {{hero}}
Then, insert HeroesComponent
inside the parent AppComponent
.
<h1>{{title}}</h1>
+ <app-heroes></app-heroes>
Note, we introduce a change to the AppComponent
that cause all unit-tests for the AppComponent
to fail.
1) should create the app
AppComponent
1. If 'app-heroes' is an Angular component, then verify that it is part of this module.
2. If 'app-heroes' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message. ("<h1>{{title}}</h1>
[ERROR ->]<app-heroes></app-heroes>
'app-heroes' is not a known element:
1. If 'app-heroes' is an Angular component, then verify that it is part of this module.
2. If 'app-heroes' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message. ("<h1>{{title}}</h1>
[ERROR ->]<app-heroes></app-heroes>
One test for the HeroesComponent
pass. At this point, your application works fine. Run it, there will be no application error. Because ng generate component heroes
updates app.module.ts
to add HeroesComponent
in to the application declaration.
We just need to do the same with our tests, but we have to do enter that manually.
import { TestBed, async } from '@angular/core/testing';
import { AppComponent } from './app.component';
+ import { HeroesComponent } from './heroes/heroes.component';
describe('AppComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
AppComponent,
+ HeroesComponent
],
}).compileComponents();
}));
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy();
});
it(`should have as title 'Tour of Heroes'`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app.title).toEqual('Tour of Heroes');
});
});
Either unit- and/or e2e-tests could fail for a number of reasons that is not related to your application feature. Therfore, a test failour does not always correlate with application failour.
Now all tests are passing. Let us add additional unit- and e2e-tests for the HeroesComponent
.
At this point we will cleanup the existing AppComponent
test, to reduce repetion and also add one more test for the addition of the child component (HeroesComponent
).
Compare the above snippet with the below.
import {TestBed, async, ComponentFixture} from '@angular/core/testing';
import { AppComponent } from './app.component';
import { HeroesComponent } from './heroes/heroes.component';
describe('AppComponent', () => {
let component: AppComponent;
let fixture: ComponentFixture<AppComponent>;
let compiled: any;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
AppComponent,
HeroesComponent
],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AppComponent);
component = fixture.componentInstance;
fixture.detectChanges();
compiled = fixture.debugElement.nativeElement;
});
it('should create the app', () => {
expect(component).toBeTruthy();
});
it(`should have as title 'Tour of Heroes'`, () => {
expect(component.title).toEqual('Tour of Heroes');
});
it('should have app-heroes', () => {
expect(compiled.querySelector('app-heroes')).toBeDefined();
});
});
Result of the excution
17 11 2019 18:35:30.532:INFO [Chrome 78.0.3904 (Mac OS X 10.15.1)]:
Connected on socket 5DhdOfd_v_yu29WHAAAA with id 67124381
AppComponent
โ should have app-heroes
โ should create the app
โ should have as title 'Tour of Heroes'
HeroesComponent
โ should create
Chrome 78.0.3904 (Mac OS X 10.15.1): Executed 4 of 4 SUCCESS (0.151 secs / 0.138 secs)
TOTAL: 4 SUCCESS
As shown above, the HeroesComponent
have one default test and it is passing. We need to add one more test to 'hero' bing rendered.
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { HeroesComponent } from './heroes.component';
describe('HeroesComponent', () => {
let component: HeroesComponent;
let fixture: ComponentFixture<HeroesComponent>;
+ let compiled: any;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ HeroesComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(HeroesComponent);
component = fixture.componentInstance;
fixture.detectChanges();
+ compiled = fixture.debugElement.nativeElement;
});
it('should create', () => {
expect(component).toBeTruthy();
});
+ it(`should have 'Windstorm' as hero`, () => {
+ expect(compiled.querySelector('app-heroes').textContent).toEqual(component.hero);
+ });
});
When we run the test, the test will fail, because the querySelector
could not find the element.
1) should have 'Windstorm' as hero
HeroesComponent
TypeError: Cannot read property 'textContent' of null
at <Jasmine>
We need to add a Paragraph HTML tag to the content we want to search for.
Oftten we need to update a working application code to make it testable. Adding IDs, class-names, or HTML tags are common practice.
- {{hero}}
+ <p>{{hero}}</p>
Updated test with compiled.querySelector('p')
help us to find the element, hence the assertion passes.
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { HeroesComponent } from './heroes.component';
describe('HeroesComponent', () => {
let component: HeroesComponent;
let fixture: ComponentFixture<HeroesComponent>;
let compiled: any;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ HeroesComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(HeroesComponent);
component = fixture.componentInstance;
fixture.detectChanges();
compiled = fixture.debugElement.nativeElement;
});
it('should create', () => {
expect(component).toBeTruthy();
});
it(`should have 'Windstorm' as hero`, () => {
expect(compiled.querySelector('p').textContent).toEqual(component.hero);
});
});
Unit test pass, done. Let us add e2e tests to test the hero name.
import { browser, by, element, ExpectedConditions as EC } from 'protractor';
export class AppHeroes {
body = element(by.css('body'));
name = element(by.css('app-heroes > p'));
navigateTo() {
browser.get(browser.baseUrl);
return browser.wait(EC.presenceOf(this.body), 5000) as Promise< void>;
}
getName() {
return this.name.getText() as Promise<any>;
}
}
import { AppHeroes } from './heroes.po';
describe('AppHeroes', () => {
let page: AppHeroes;
beforeAll(() => {
page = new AppHeroes();
page.navigateTo();
});
it(`should have name 'Windstorm'`, () => {
expect(page.getName()).toContain('Windstorm');
});
});
Test excution result
Jasmine started
AppComponent
โ should display title
AppHeroes
โ should have name 'Windstorm'
Executed 2 of 2 specs SUCCESS in 1 sec.
[19:33:03] I/launcher - 0 instance(s) of WebDriver still running
[19:33:03] I/launcher - chrome #01 passed
So far the app only shows the hero name. Here, we want to display additional hero information. See details here
import { Component, OnInit } from '@angular/core';
+ import { Hero } from '../hero';
@Component({
selector: 'app-heroes',
templateUrl: './heroes.component.html',
styleUrls: ['./heroes.component.css']
})
export class HeroesComponent implements OnInit {
- hero = 'Windstorm';
+ hero: Hero = {
+ id: 1,
+ name: 'Windstorm'
+ };
constructor() { }
ngOnInit() {
}
}
- <p>{{hero}}</p>
+ <h2>{{hero.name | uppercase}} Details</h2>
+ <div><span>id: </span>{{hero.id}}</div>
+ <div><span>name: </span>{{hero.name}}</div>
After updating the code, as shown above both unit- and e2e-tests for the HeroesComponent
fails for not finding the original hero content in the view.
HeroesComponent
โ should have 'Windstorm' as hero
TypeError: Cannot read property 'textContent' of null
at <Jasmine>
AppHeroes
โ should have name 'Windstorm'
- Failed: No element found using locator: By(css selector, app-heroes > p)
at elementArray
First let us add IDs for the newer elements just introduced to make them accesssable for testing.
<h2 id="dtl">{{hero.name | uppercase}} Details</h2>
<div id="hro-id"><span>id: </span>{{hero.id}}</div>
<div id="hro-name"><span>name: </span>{{hero.name}}</div>
Now we can write unit- and e2e-tests for testing the added hero title, id, and name.
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { HeroesComponent } from './heroes.component';
describe('HeroesComponent', () => {
let component: HeroesComponent;
let fixture: ComponentFixture<HeroesComponent>;
let compiled: any;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ HeroesComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(HeroesComponent);
component = fixture.componentInstance;
fixture.detectChanges();
compiled = fixture.debugElement.nativeElement;
});
it('should create', () => {
expect(component).toBeTruthy();
});
it(`should have hero title`, () => {
expect(compiled.querySelector('#dtl').textContent)
.toEqual((component.hero.name).toUpperCase() + ' Details');
});
it(`should have hero id`, () => {
expect(compiled.querySelector('#hro-id').textContent)
.toEqual('id: ' + (component.hero.id));
});
it(`should have hero name`, () => {
expect(compiled.querySelector('#hro-name').textContent)
.toEqual('name: ' + (component.hero.name));
});
});
17 11 2019 20:57:28.222:INFO [Chrome 78.0.3904 (Mac OS X 10.15.1)]:
Connected on socket xSYvLvZLsl7yTbN4AAAA with id 50080976
AppComponent
โ should create the app
โ should have as title 'Tour of Heroes'
โ should have app-heroes
HeroesComponent
โ should create
โ should have hero id
โ should have hero title
โ should have hero name
Chrome 78.0.3904 (Mac OS X 10.15.1): Executed 7 of 7 SUCCESS (0.223 secs / 0.181 secs)
TOTAL: 7 SUCCESS
import { browser, by, element, ExpectedConditions as EC } from 'protractor';
export class AppHeroes {
body = element(by.css('body'));
title = element(by.id('dtl'));
id = element(by.id('hro-id'));
name = element(by.id('hro-name'));
navigateTo() {
browser.get(browser.baseUrl);
return browser.wait(EC.presenceOf(this.body), 5000) as Promise< void>;
}
getTitle() {
return this.title.getText() as Promise<any>;
}
getId() {
return this.id.getText() as Promise<any>;
}
getName() {
return this.name.getText() as Promise<any>;
}
}
import { AppHeroes } from './heroes.po';
describe('AppHeroes', () => {
let page: AppHeroes;
const hero = {
id: 1,
name: 'Windstorm'
};
beforeAll(() => {
page = new AppHeroes();
page.navigateTo();
});
it(`should have title`, () => {
expect(page.getTitle()).toContain(`${(hero.name).toUpperCase()} Details`);
});
it(`should have id`, () => {
expect(page.getId()).toContain(`id: ${hero.id}`);
});
it(`should have name`, () => {
expect(page.getName()).toContain(`name: ${hero.name}`);
});
});
Jasmine started
AppComponent
โ should display title
AppHeroes
โ should have title
โ should have id
โ should have name
Executed 4 of 4 specs SUCCESS in 3 secs.
All tests pass.
Two-way data binding. We will add an input box to be able to edit the hero name. See details here
<h2 id="dtl">{{hero.name | uppercase}} Details</h2>
<div id="hro-id"><span>id: </span>{{hero.id}}</div>
- <div id="hro-name"><span>name: </span>{{hero.name}}</div>
+ <div id="hro-name">
+ <label>name:
+ <input [(ngModel)]="hero.name" placeholder="name"/>
+ </label>
+ </div>
After the above change in the view, your application, unit- and e2e-test will fail for a good reason. Check your browser console to see why your application is failing to render the view. Below is an identical error message from the unit test.
Failed: Template parse errors:
Can't bind to 'ngModel' since it isn't a known property of 'input'. ("
<div id="hro-name">
<label>name:
<input [ERROR ->][(ngModel)]="hero.name" placeholder="name"/>
</label>
</div>
"): ng:///DynamicTestModule/HeroesComponent.html@4:11
Add FormsModule
in the app.module.ts
as shown here. We also need to add the same in the heroes.componet.spec.ts
and its parent shell component test app.component.spec.ts
.
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+ import { FormsModule } from '@angular/forms';
import { HeroesComponent } from './heroes.component';
describe('HeroesComponent', () => {
let component: HeroesComponent;
let fixture: ComponentFixture<HeroesComponent>;
let compiled: any;
beforeEach(async(() => {
TestBed.configureTestingModule({
+ imports: [ FormsModule ],
declarations: [ HeroesComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(HeroesComponent);
component = fixture.componentInstance;
fixture.detectChanges();
compiled = fixture.debugElement.nativeElement;
});
it('should create', () => {
expect(component).toBeTruthy();
});
it(`should have hero title`, () => {
expect(compiled.querySelector('#dtl').textContent)
.toEqual((component.hero.name).toUpperCase() + ' Details');
});
it(`should have hero id`, () => {
expect(compiled.querySelector('#hro-id').textContent)
.toEqual('id: ' + (component.hero.id));
});
it(`should have hero name`, () => {
expect(compiled.querySelector('#hro-name').textContent)
.toEqual('name: ' + (component.hero.name));
});
});
import { TestBed, async, ComponentFixture } from '@angular/core/testing';
+ import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { HeroesComponent } from './heroes/heroes.component';
describe('AppComponent', () => {
let component: AppComponent;
let fixture: ComponentFixture<AppComponent>;
let compiled: any;
beforeEach(async(() => {
TestBed.configureTestingModule({
+ imports: [ FormsModule ],
declarations: [
AppComponent,
HeroesComponent
],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AppComponent);
component = fixture.componentInstance;
fixture.detectChanges();
compiled = fixture.debugElement.nativeElement;
});
it('should create the app', () => {
expect(component).toBeTruthy();
});
it(`should have as title 'Tour of Heroes'`, () => {
expect(component.title).toEqual('Tour of Heroes');
});
it('should have app-heroes', () => {
expect(compiled.querySelector('app-heroes')).toBeDefined();
});
});
At this point for both unit- and e2e-tests, only the single test for the name field should fail, since its actual markup is changed. We will update that test.
Unit tests run in random order. It is important that the change made in one test should be tear-down after each test run, when possible. Or a test that could potentially polute other test, shoul be keept in a separate describe block.
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { By } from '@angular/platform-browser';
import { HeroesComponent } from './heroes.component';
describe('HeroesComponent', () => {
let component: HeroesComponent;
let fixture: ComponentFixture<HeroesComponent>;
let compiled: any;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [ FormsModule ],
declarations: [ HeroesComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(HeroesComponent);
component = fixture.componentInstance;
fixture.detectChanges();
compiled = fixture.debugElement.nativeElement;
});
it('should create', () => {
expect(component).toBeTruthy();
});
it(`should have hero title`, () => {
expect(compiled.querySelector('#dtl').textContent)
.toEqual((component.hero.name).toUpperCase() + ' Details');
});
it(`should have hero id`, () => {
expect(compiled.querySelector('#hro-id').textContent)
.toEqual('id: ' + (component.hero.id));
});
});
describe('HeroesComponent: input', () => {
let component: HeroesComponent;
let fixture: ComponentFixture<HeroesComponent>;
let compiled: any;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [ FormsModule ],
declarations: [ HeroesComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(HeroesComponent);
component = fixture.componentInstance;
fixture.detectChanges();
compiled = fixture.debugElement.nativeElement;
});
it(`should have editable hero name`, () => {
const inputBox = fixture.debugElement.query(By.css('input')).nativeElement;
inputBox.value = 'Dr. Nice';
inputBox.dispatchEvent(new Event('input'));
expect(inputBox.value).toBe('Dr. Nice');
fixture.detectChanges();
// two way binding
expect(compiled.querySelector('#dtl').textContent)
.toEqual((component.hero.name).toUpperCase() + ' Details');
});
});
Notice how the above test uses a separate describe block, because the two-way binding test will change the component.hero.name
property.
17 11 2019 21:43:39.565:INFO [Chrome 78.0.3904 (Mac OS X 10.15.1)]:
Connected on socket SPvsaEf-8W7L0e2JAAAA with id 61592508
HeroesComponent: input
โ should have editable hero name
HeroesComponent
โ should create
โ should have hero title
โ should have hero id
AppComponent
โ should have app-heroes
โ should have as title 'Tour of Heroes'
โ should create the app
Chrome 78.0.3904 (Mac OS X 10.15.1): Executed 7 of 7 SUCCESS (0.306 secs / 0.27 secs)
TOTAL: 7 SUCCESS
The change we need to make on the e2e-test is minimal as shown below.
import { browser, by, element, ExpectedConditions as EC } from 'protractor';
export class AppHeroes {
body = element(by.css('body'));
title = element(by.id('dtl'));
id = element(by.id('hro-id'));
- name = element(by.id('hro-name'));
+ name = element(by.css('#hro-name > label > input'));
navigateTo() {
browser.get(browser.baseUrl);
return browser.wait(EC.presenceOf(this.body), 5000) as Promise< void>;
}
getTitle() {
return this.title.getText() as Promise<any>;
}
getId() {
return this.id.getText() as Promise<any>;
}
- getName() {
- return this.name.getText() as Promise<any>;
- }
+ setName(name) {
+ this.name.clear();
+ return this.name.sendKeys(name) as Promise<any>;
+ }
}
import { AppHeroes } from './heroes.po';
describe('AppHeroes', () => {
let page: AppHeroes;
const hero = {
id: 1,
name: 'Windstorm'
};
beforeAll(() => {
page = new AppHeroes();
page.navigateTo();
});
it(`should have title`, () => {
expect(page.getTitle()).toContain(`${(hero.name).toUpperCase()} Details`);
});
it(`should have id`, () => {
expect(page.getId()).toContain(`id: ${hero.id}`);
});
- it(`should have name`, () => {
- expect(page.getName()).toContain(`name: ${hero.name}`);
- });
+ it('should have editable hero name', async () => {
+ await page.setName('Dr. Nice');
+ expect(page.getTitle()).toEqual('DR. NICE Details');
+ });
});
Jasmine started
AppComponent
โ should display title
AppHeroes
โ should have title
โ should have id
โ should have editable hero name
Executed 4 of 4 specs SUCCESS in 2 secs.
So far we have shown a single hero, now let us display a list of heroes. Able to edit only the selected
hero. See details here
import { Component, OnInit } from '@angular/core';
import { Hero } from '../hero';
+ import { HEROES } from '../mock-heroes';
@Component({
selector: 'app-heroes',
templateUrl: './heroes.component.html',
styleUrls: ['./heroes.component.css']
})
export class HeroesComponent implements OnInit {
- hero: Hero = {
- id: 1,
- name: 'Windstorm'
- };
- heroes = HEROES;
+ selectedHero: Hero;
constructor() { }
ngOnInit() {
}
+ onSelect(hero: Hero): void {
+ this.selectedHero = hero;
+ }
}
- <h2 id="dtl">{{hero.name | uppercase}} Details</h2>
- <div id="hro-id"><span>id: </span>{{hero.id}}</div>
- <div id="hro-name">
- <label>name:
- <input [(ngModel)]="hero.name" placeholder="name"/>
- </label>
- </div>
+ <h2>My Heroes</h2>
+ <ul class="heroes">
+ <li *ngFor="let hero of heroes"
+ [class.selected]="hero === selectedHero"
+ (click)="onSelect(hero) ">
+ <span class="badge">{{hero.id}}</span> {{hero.name}}
+ </li>
+ </ul>
+ <div *ngIf="selectedHero" id="details">
+ <h2 id="dtl">{{selectedHero.name | uppercase}} Details</h2>
+ <div id="hro-id"><span>id: </span>{{selectedHero.id}}</div>
+ <div id="hro-name">
+ <label>name:
+ <input [(ngModel)]="selectedHero.name" placeholder="name" name="name"/>
+ </label>
+ </div>
+ </div>
After this change almost all of the test for HeroesComponent
should fail. Tests for AppComponent
should work as before.
Below we will update the unit- and e2e-tests to adapt this change.
We will classify the unit test for the HeroComponent
in to three parts:
- the inital state, where we have no selected hero
- the state we get details after we select a hero
- the state we get when we modify a selected hero
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { By } from '@angular/platform-browser';
import { HeroesComponent } from './heroes.component';
import { HEROES } from '../mock-heroes';
describe('HeroesComponent: init', () => {
let component: HeroesComponent;
let fixture: ComponentFixture<HeroesComponent>;
let compiled: any;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [ FormsModule ],
declarations: [ HeroesComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(HeroesComponent);
component = fixture.componentInstance;
component.heroes = HEROES;
fixture.detectChanges();
compiled = fixture.debugElement.nativeElement;
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should have heroes', () => {
expect(component.heroes).toBeDefined();
});
it('should have a list of heroes', async () => {
HEROES.forEach( (hero, index) => {
expect(compiled.querySelector(`ul > li:nth-child(${index + 1})`).textContent)
.toContain(hero.name);
});
});
it('should not have selected hero', () => {
expect(component.selectedHero).not.toBeDefined();
expect(compiled.querySelector('#details')).toBe(null);
});
});
describe('HeroesComponent: select', () => {
let component: HeroesComponent;
let fixture: ComponentFixture<HeroesComponent>;
let compiled: any;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [ FormsModule ],
declarations: [ HeroesComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(HeroesComponent);
component = fixture.componentInstance;
component.heroes = HEROES;
fixture.detectChanges();
compiled = fixture.debugElement.nativeElement;
});
it('should create', () => {
expect(component).toBeTruthy();
});
it(`should have title ${HEROES[0].name} Details`, async () => {
const myHero = fixture.debugElement.queryAll(By.css('.badge'));
myHero[0].nativeElement.click();
fixture.detectChanges();
await fixture.whenStable();
expect(component.selectedHero).toBeDefined();
expect(compiled.querySelector('#dtl').textContent)
.toEqual(`${(HEROES[0].name).toUpperCase()} Details`);
});
it(`should have id ${HEROES[0].id}`, async () => {
const myHero = fixture.debugElement.queryAll(By.css('.badge'));
myHero[0].nativeElement.click();
fixture.detectChanges();
await fixture.whenStable();
expect(component.selectedHero).toBeDefined();
expect(compiled.querySelector('#hro-id').textContent)
.toEqual(`id: ${HEROES[0].id}`);
});
it(`should have text '${HEROES[0].name}' in the input`, async () => {
const myHero = fixture.debugElement.queryAll(By.css('.badge'));
myHero[0].nativeElement.click();
fixture.detectChanges();
await fixture.whenStable();
expect(component.selectedHero).toBeDefined();
const inputBox = fixture.debugElement.query(By.css('input')).nativeElement;
expect(inputBox.value).toEqual(HEROES[0].name);
});
});
describe('HeroesComponent: input', () => {
let component: HeroesComponent;
let fixture: ComponentFixture<HeroesComponent>;
let compiled: any;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [ FormsModule ],
declarations: [ HeroesComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(HeroesComponent);
component = fixture.componentInstance;
component.heroes = HEROES;
fixture.detectChanges();
compiled = fixture.debugElement.nativeElement;
});
it('input should accept new value', async () => {
const myHero = fixture.debugElement.queryAll(By.css('.badge'));
myHero[0].nativeElement.click();
fixture.detectChanges();
await fixture.whenStable();
const inputBox = fixture.debugElement.query(By.css('input')).nativeElement;
inputBox.value = 'Foo';
inputBox.dispatchEvent(new Event('input'));
expect(inputBox.value).toBe('Foo');
fixture.detectChanges();
expect(component.selectedHero).toBeDefined();
expect(compiled.querySelector('#dtl').textContent)
.toEqual(`${(HEROES[0].name).toUpperCase()} Details`);
expect(compiled.querySelector('li.selected').textContent).toContain('Foo');
});
});
Test results.
17 11 2019 23:24:07.534:INFO [Chrome 78.0.3904 (Mac OS X 10.15.1)]:
Connected on socket 6y91viPShxMH-04mAAAA with id 51344877
HeroesComponent: init
โ should create
โ should have heroes
โ should not have selected hero
HeroesComponent: select
โ should have id 11
โ should have title Dr Nice Details
โ should have text 'Dr Nice' in the input
HeroesComponent: input
โ input should accept new value
AppComponent
โ should have as title 'Tour of Heroes'
โ should have app-heroes
โ should create the app
Chrome 78.0.3904 (Mac OS X 10.15.1): Executed 11 of 11 SUCCESS (0.386 secs / 0.348 secs)
TOTAL: 11 SUCCESS
import { browser, by, element, ExpectedConditions as EC } from 'protractor';
export class AppHeroes {
body = element(by.css('body'));
title = element(by.id('dtl'));
id = element(by.id('hro-id'));
name = element(by.css('#hro-name > label > input'));
selected = element(by.css('li.selected'));
navigateTo() {
browser.get(browser.baseUrl);
return browser.wait(EC.presenceOf(this.body), 5000) as Promise< void>;
}
getTitle() {
return this.title.getText() as Promise<any>;
}
getId() {
return this.id.getText() as Promise<any>;
}
setName(name) {
this.name.clear();
return this.name.sendKeys(name) as Promise<any>;
}
selectHero(index) {
const heroes = element.all(by.css('.badge'));
return heroes.get(index).click() as Promise<void>;
}
getSelected() {
return this.selected.getText() as Promise<string>;
}
}
import { AppHeroes } from './heroes.po';
describe('AppHeroes', () => {
let page: AppHeroes;
beforeAll(() => {
page = new AppHeroes();
page.navigateTo();
page.selectHero(0);
});
it('should have title', () => {
expect(page.getTitle()).toContain('Details');
});
it('should have id', () => {
expect(page.getId()).toContain('id:');
});
it('should have name input', async () => {
await page.setName('Dr. Nice');
expect(page.getTitle()).toEqual('DR. NICE Details');
expect(page.getSelected()).toContain('Dr. Nice');
});
});
Jasmine started
AppComponent
โ should display title
AppHeroes
โ should have title
โ should have id
โ should have name input
Executed 4 of 4 specs SUCCESS in 2 secs.
At this point, we will be splitting the HeroesComponent
into two parts, master (list view) and detail (detail view). See details here
As the applictaion gets more components, we will split this section into two.
First, we will remove the detail section from the HeroesComponent
. Then comment
all faling tests. Update and/or introduce new tests.
Second, we will build the HeroDetailComponent
tests, by re-using
the commented test, update existing tests, and/or introduce new ones.
When a unit of application split into parts, already existing test needs to be splited. Hence, some application integration logics that was being testsed when the unit was one could not be tested using the splited testes. Therefore, the more we split our applications into a smaller unit, the more we loose the ablility of testing our application integration point using unit test.
Note that which of the commented tests could be-reused, and which we have to descard, and which new tests we need introduce to test HeroDetailComponent
. The part that we are going to descard needs to be tested with e2e tests.
<h2>My Heroes</h2>
<ul class="heroes">
<li *ngFor="let hero of heroes"
[class.selected]="hero === selectedHero"
(click)="onSelect(hero) ">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</li>
</ul>
- <div *ngIf="selectedHero" id="details">
- <h2 id="dtl">{{selectedHero.name | uppercase}} Details</h2>
- <div id="hro-id"><span>id: </span>{{selectedHero.id}}</div>
- <div id="hro-name">
- <label>name:
- <input [(ngModel)]="selectedHero.name" placeholder="name" name="name"/>
- </label>
- </div>
- </div>
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { By } from '@angular/platform-browser';
import { HeroesComponent } from './heroes.component';
import { HEROES } from '../mock-heroes';
describe('HeroesComponent: init', () => {
let component: HeroesComponent;
let fixture: ComponentFixture<HeroesComponent>;
let compiled: any;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [ FormsModule ],
declarations: [ HeroesComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(HeroesComponent);
component = fixture.componentInstance;
component.heroes = HEROES;
fixture.detectChanges();
compiled = fixture.debugElement.nativeElement;
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should have heroes', () => {
expect(component.selectedHero).not.toBeDefined();
});
it('should have a list of heroes', async () => {
HEROES.forEach( (hero, index) => {
expect(compiled.querySelector(`ul > li:nth-child(${index + 1})`).textContent)
.toContain(hero.name);
});
});
it('should not have selected hero', () => {
expect(component.heroes).toBeDefined();
expect(compiled.querySelector('#details')).toBe(null);
});
});
// xdescribe('HeroesComponent: select', () => {
// let component: HeroesComponent;
// let fixture: ComponentFixture<HeroesComponent>;
// let compiled: any;
//
// beforeEach(async(() => {
// TestBed.configureTestingModule({
// imports: [ FormsModule ],
// declarations: [ HeroesComponent ]
// })
// .compileComponents();
// }));
//
// beforeEach(() => {
// fixture = TestBed.createComponent(HeroesComponent);
// component = fixture.componentInstance;
// component.heroes = HEROES;
// fixture.detectChanges();
// compiled = fixture.debugElement.nativeElement;
// });
//
// it(`should have title ${HEROES[0].name} Details`, async () => {
// const myHero = fixture.debugElement.queryAll(By.css('.badge'));
// myHero[0].nativeElement.click();
// fixture.detectChanges();
// await fixture.whenStable();
//
// expect(component.selectedHero).toBeDefined();
// expect(compiled.querySelector('#dtl').textContent)
// .toEqual(`${(HEROES[0].name).toUpperCase()} Details`);
// });
//
// it(`should have id ${HEROES[0].id}`, async () => {
// const myHero = fixture.debugElement.queryAll(By.css('.badge'));
// myHero[0].nativeElement.click();
// fixture.detectChanges();
// await fixture.whenStable();
//
// expect(component.selectedHero).toBeDefined();
// expect(compiled.querySelector('#hro-id').textContent)
// .toEqual(`id: ${HEROES[0].id}`);
// });
//
// it(`should have text '${HEROES[0].name}' in the input`, async () => {
// const myHero = fixture.debugElement.queryAll(By.css('.badge'));
// myHero[0].nativeElement.click();
// fixture.detectChanges();
// await fixture.whenStable();
//
// expect(component.selectedHero).toBeDefined();
// const inputBox = fixture.debugElement.query(By.css('input')).nativeElement;
// expect(inputBox.value).toEqual(HEROES[0].name);
// });
// });
// xdescribe('HeroesComponent: input', () => {
// let component: HeroesComponent;
// let fixture: ComponentFixture<HeroesComponent>;
// let compiled: any;
//
// beforeEach(async(() => {
// TestBed.configureTestingModule({
// imports: [ FormsModule ],
// declarations: [ HeroesComponent ]
// })
// .compileComponents();
// }));
//
// beforeEach(() => {
// fixture = TestBed.createComponent(HeroesComponent);
// component = fixture.componentInstance;
// component.heroes = HEROES;
// fixture.detectChanges();
// compiled = fixture.debugElement.nativeElement;
// });
//
// it('input should accept new value', async () => {
// const myHero = fixture.debugElement.queryAll(By.css('.badge'));
// myHero[0].nativeElement.click();
// fixture.detectChanges();
// await fixture.whenStable();
//
// const inputBox = fixture.debugElement.query(By.css('input')).nativeElement;
// inputBox.value = 'Foo';
// inputBox.dispatchEvent(new Event('input'));
// expect(inputBox.value).toBe('Foo');
// fixture.detectChanges();
//
// expect(component.selectedHero).toBeDefined();
// expect(compiled.querySelector('#dtl').textContent)
// .toEqual(`${(HEROES[0].name).toUpperCase()} Details`);
//
// expect(compiled.querySelector('li.selected').textContent).toContain('Foo');
// });
// });
Note: We have commented the above code block to keep it clear for presentation. In practice we can just use the xdiscribe
to ignore the last two describe blockes from test execution.
<h2>My Heroes</h2>
<ul class="heroes">
<li *ngFor="let hero of heroes"
[class.selected]="hero === selectedHero"
(click)="onSelect(hero) ">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</li>
</ul>
<app-hero-detail [hero]="selectedHero"></app-hero-detail>
import { Component, OnInit, Input } from '@angular/core';
import { Hero } from '../hero';
@Component({
selector: 'app-hero-detail',
templateUrl: './hero-detail.component.html',
styleUrls: ['./hero-detail.component.css']
})
export class HeroDetailComponent implements OnInit {
@Input() hero: Hero;
constructor() { }
ngOnInit() {
}
}
After adding the HeroDetailComponent
component, if we run the unit test, tests of AppComponent
and HeroesComponent
will start failing.
Failed: Template parse errors:
Can't bind to 'hero' since it isn't a known property of 'app-hero-detail'.
1. If 'app-hero-detail' is an Angular component and it has 'hero' input, then verify that it is part of this module.
2. If 'app-hero-detail' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message.
3. To allow any property add 'NO_ERRORS_SCHEMA' to the '@NgModule.schemas' of this component. ("
</ul>
<app-hero-detail [ERROR ->][hero]="selectedHero"></app-hero-detail>
Solution, We just need to declare the HeroDetailComponent
in the app.component.spec.ts
and heroes.component.spec.ts
.
Below is an example for the app.component.spec.ts
spec. You do the same for heroes.component.spec.ts
.
import {TestBed, async, ComponentFixture} from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { HeroesComponent } from './heroes/heroes.component';
import { HeroDetailComponent } from './hero-detail/hero-detail.component';
describe('AppComponent', () => {
let component: AppComponent;
let fixture: ComponentFixture<AppComponent>;
let compiled: any;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule,
FormsModule
],
declarations: [
AppComponent,
HeroesComponent,
HeroDetailComponent
],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AppComponent);
component = fixture.componentInstance;
fixture.detectChanges();
compiled = fixture.debugElement.nativeElement;
});
it('should create the app', () => {
expect(component).toBeTruthy();
});
it(`should have as title 'Tour of Heroes'`, () => {
expect(component.title).toEqual('Tour of Heroes');
});
it('should have app-heroes', () => {
expect(compiled.querySelector('app-heroes')).toBeDefined();
});
});
Now we have one last error to fix, hero-detail.component.spec.ts
needs ngModel
.
Failed: Template parse errors:
Can't bind to 'ngModel' since it isn't a known property of 'input'. ("
<div id="hro-name">
<label>name:
<input [ERROR ->][(ngModel)]="hero.name" placeholder="name" name="name"/>
</label>
</div>
"): ng:///DynamicTestModule/HeroDetailComponent.html@6:15
Add the FormsModule
to the HeroDetailComponent
component. Also make sure y
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { HeroDetailComponent } from './hero-detail.component';
describe('HeroDetailComponent', () => {
let component: HeroDetailComponent;
let fixture: ComponentFixture<HeroDetailComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [ FormsModule ],
declarations: [ HeroDetailComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(HeroDetailComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
๐ฑ unit test: result: app.component.spec.ts, heroes.component.spec.ts, hero-detail.component.spec.ts
18 11 2019 21:16:20.990:INFO [Chrome 78.0.3904 (Mac OS X 10.15.1)]:
Connected on socket 1isdMMnOt_ZH72g9AAAA with id 17808333
HeroesComponent: init
โ should create
โ should not have selected hero
โ should have a list of heroes
โ should have heroes
HeroDetailComponent
โ should create
AppComponent
โ should have as title 'Tour of Heroes'
โ should create the app
โ should have app-heroes
TOTAL: 8 SUCCESS
Chrome 78.0.3904 (Mac OS X 10.15.1): Executed 8 of 8 SUCCESS (0.774 secs / 0.692 secs)
TOTAL: 8 SUCCESS
The good thing is the master/detail split did not affect the integration test. The e2e tests are passing as before, so no update or modification is needed.
However, we do not have enough unit test coverage for the HeroDetailComponent
and let us do that.
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { By } from '@angular/platform-browser';
import { HeroDetailComponent } from './hero-detail.component';
describe('HeroDetailComponent', () => {
let component: HeroDetailComponent;
let fixture: ComponentFixture<HeroDetailComponent>;
let compiled: any;
const hero = { id: 20, name: 'Tornado' };
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [ FormsModule ],
declarations: [ HeroDetailComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(HeroDetailComponent);
component = fixture.componentInstance;
component.hero = hero;
fixture.detectChanges();
compiled = fixture.debugElement.nativeElement;
});
it('should create', () => {
expect(component).toBeTruthy();
});
it(`should have title ${hero.name} Details`, () => {
expect(compiled.querySelector('#dtl').textContent)
.toEqual(`${(hero.name).toUpperCase()} Details`);
});
it(`should have id ${hero.id}`, async () => {
expect(compiled.querySelector('#hro-id').textContent)
.toEqual(`id: ${hero.id}`);
});
it(`should have text '${hero.name}' in the input`, async () => {
fixture.detectChanges();
await fixture.whenStable();
const inputBox = fixture.debugElement.query(By.css('input')).nativeElement;
expect(inputBox.value).toEqual(hero.name);
});
it('input should accept new value', async () => {
const inputBox = fixture.debugElement.query(By.css('input')).nativeElement;
inputBox.value = 'Foo';
inputBox.dispatchEvent(new Event('input'));
fixture.detectChanges();
expect(inputBox.value).toBe('Foo');
expect(compiled.querySelector('#dtl').textContent)
.toEqual(`${(hero.name).toUpperCase()} Details`);
});
});
We are not able to click on the list and assert the details of the clicked list displayed correctly, because now the two components are separate. Hence, that part of the commented unit test will be discarded.
18 11 2019 21:54:29.966:INFO [Chrome 78.0.3904 (Mac OS X 10.15.1)]:
Connected on socket W0rmtCeFgvlthm6tAAAA with id 91122346
HeroDetailComponent
โ should have id 20
โ should create
โ should have text 'Tornado' in the input
โ should have title Tornado Details
โ input should accept new value
HeroesComponent: init
โ should have a list of heroes
โ should create
โ should have heroes
โ should not have selected hero
AppComponent
โ should have as title 'Tour of Heroes'
โ should create the app
โ should have app-heroes
TOTAL: 12 SUCCESS
Chrome 78.0.3904 (Mac OS X 10.15.1): Executed 12 of 12 SUCCESS (0.536 secs / 0.489 secs)
TOTAL: 12 SUCCESS
The more smaller units of an application being unit testes, the more the integration points of our application will be missed by the unit test. Hence, we need more integration testes, such as e2e tests.
Component shouldn't fetch or save data directly. We will get the data from HeroService
. See details here.
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { Hero } from './hero';
import { HEROES } from './mock-heroes';
@Injectable({
providedIn: 'root'
})
export class HeroService {
constructor() { }
getHeroes(): Observable<Hero[]> {
return of(HEROES);
}
}
import { Component, OnInit } from '@angular/core';
import { Hero } from '../hero';
+ import { HeroService } from '../hero.service';
@Component({
selector: 'app-heroes',
templateUrl: './heroes.component.html',
styleUrls: ['./heroes.component.css']
})
export class HeroesComponent implements OnInit {
heroes: Hero[];
selectedHero: Hero;
constructor(
private heroService: HeroService
) { }
ngOnInit() {
this.getHeroes();
}
onSelect(hero) {
this.selectedHero = hero;
}
- getHeroes(): void {
- this.heroes = this.heroService.getHeroes();
- }
+ getHeroes(): void {
+ this.heroService.getHeroes().subscribe(
+ heroes => this.heroes = heroes
+ );
+ }
}
After all these changes both unit- and e2e-tests are passing. We will add new tests to the HeroService
.
We will add a class name for the list of heroes.
<h2>My Heroes</h2>
<ul class="heroes">
<li *ngFor="let hero of heroes"
[class.selected]="hero === selectedHero"
(click)="onSelect(hero) "
+ class="hero">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</li>
</ul>
<app-hero-detail [hero]="selectedHero"></app-hero-detail>
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { By } from '@angular/platform-browser';
import { HeroesComponent } from './heroes.component';
import { HeroDetailComponent } from '../hero-detail/hero-detail.component';
import { HeroService } from '../hero.service';
import { HEROES } from '../mock-heroes';
import { defer } from 'rxjs';
// ...
export function fakeAsyncResponse<T>(data: T) {
return defer(() => Promise.resolve(data));
}
const heroServiceStub = {
getHeroes() {
return fakeAsyncResponse([
{ id: 15, name: 'Magneta' },
{ id: 16, name: 'RubberMan' }
]);
}
};
describe('HeroesComponent: data: hero.service', () => {
let fixture: ComponentFixture<HeroesComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [ FormsModule ],
declarations: [
HeroesComponent,
HeroDetailComponent
],
providers: [{
provide: HeroService,
useValue: heroServiceStub
}]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(HeroesComponent);
fixture.detectChanges();
});
it('should have two heroes', async () => {
await fixture.whenStable();
fixture.detectChanges();
const heroes = fixture.debugElement.queryAll(By.css('.hero'));
expect(heroes.length).toEqual(2);
});
});
Below is a unit test for the HeroService
.
import {inject, TestBed} from '@angular/core/testing';
import { HeroService } from './hero.service';
describe('HeroService', () => {
beforeEach(() => TestBed.configureTestingModule({}));
it('should be created', () => {
const service: HeroService = TestBed.get(HeroService);
expect(service).toBeTruthy();
});
it(`get all heroes`, inject([HeroService], (heroService) => {
heroService.getHeroes().subscribe(heroes => {
expect(heroes.length).toEqual(10);
});
}));
});
AppComponent(0.3), HeroesComponent(0.5), HeroService(0.2), MessagesComponent(0.1), MessageService(0.1),
Adding a MessagesComponent
to display message at the bottom. See details here
<h1 id="title">{{title}}</h1>
<router-outlet></router-outlet>
++ <app-messages></app-messages>
Failed: Template parse errors:
'app-messages' is not a known element:
1. If 'app-messages' is an Angular component, then verify that it is part of this module.
2. If 'app-messages' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message. ("<h1 id="title">{{title}}</h1>
<router-outlet></router-outlet>
[ERROR ->]<app-messages></app-messages>
The addition of the MessagesComponent
makes the unit test to fail. Let us add
the MessagesComponent
inside the app.component.spec.ts
to fix and also add one more test.
import {TestBed, async, ComponentFixture} from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
+ import { MessagesComponent } from './messages/messages.component';
describe('AppComponent', () => {
let component: AppComponent;
let fixture: ComponentFixture<AppComponent>;
let compiled: any;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule,
FormsModule
],
declarations: [
AppComponent,
+ MessagesComponent
],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AppComponent);
component = fixture.componentInstance;
fixture.detectChanges();
compiled = fixture.debugElement.nativeElement;
});
it('should create the app', () => {
expect(component).toBeTruthy();
});
it(`should have as title 'Tour of Heroes'`, () => {
expect(component.title).toEqual('Tour of Heroes');
});
it('should have app-heroes', () => {
expect(compiled.querySelector('app-heroes')).toBeDefined();
});
+ it('should have messaging', () => {
+ expect(compiled.querySelector('app-messages')).toBeTruthy();
+ });
});
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class MessageService {
messages: string[] = [];
constructor() { }
add(message: string) {
this.messages.push(message);
}
clear() {
this.messages = [];
}
}
Test for message service.
import { TestBed } from '@angular/core/testing';
import { MessageService } from './message.service';
describe('MessageService', () => {
beforeEach(() => TestBed.configureTestingModule({}));
it('should be created', () => {
const service: MessageService = TestBed.get(MessageService);
expect(service).toBeTruthy();
});
it('should add message', () => {
const service: MessageService = TestBed.get(MessageService);
service.add('Message 1');
service.add('Message 2');
expect(service.messages.length).toEqual(2);
expect(service.messages[0]).toEqual('Message 1');
expect(service.messages[1]).toEqual('Message 2');
});
it('should clear message', () => {
const service: MessageService = TestBed.get(MessageService);
service.add('Message 3');
service.clear();
expect(service.messages.length).toEqual(0);
});
});
At this point we will move from using the AppComponent
as a shell to introducing a
formal router module. See details here.
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
+ import { HeroesComponent } from './heroes/heroes.component';
const routes: Routes = [
+ { path: 'heroes', component: HeroesComponent }
];
@NgModule({
imports: [ RouterModule.forRoot(routes) ],
exports: [ RouterModule ]
})
export class AppRoutingModule { }
<h1 id="title">{{title}}</h1>
+ <router-outlet></router-outlet>
- <app-heroes></app-heroes>
<app-messages></app-messages>
After this change, if we run a unit test, all tests pass. When we run e2e tests, the tests will start failing completely. Because of the route change.
First, let us remove the un-used HeroesComponent
from the AppComponent
test.
import {TestBed, async, ComponentFixture} from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
- import { HeroesComponent } from './heroes/heroes.component';
- import { HeroDetailComponent } from './hero-detail/hero-detail.component';
describe('AppComponent', () => {
let component: AppComponent;
let fixture: ComponentFixture<AppComponent>;
let compiled: any;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule,
FormsModule
],
declarations: [
AppComponent,
- HeroesComponent,
- HeroDetailComponent
],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AppComponent);
component = fixture.componentInstance;
fixture.detectChanges();
compiled = fixture.debugElement.nativeElement;
});
it('should create the app', () => {
expect(component).toBeTruthy();
});
it(`should have as title 'Tour of Heroes'`, () => {
expect(component.title).toEqual('Tour of Heroes');
});
it('should have app-heroes', () => {
expect(compiled.querySelector('app-heroes')).toBeDefined();
});
});
Second, let us fix the routing issue in our e2e tests. Adding a heroes
to the base-url
in the page-object of the heroes page test is enough.
import { browser, by, element, ExpectedConditions as EC } from 'protractor';
export class AppHeroes {
body = element(by.css('body'));
title = element(by.id('dtl'));
id = element(by.id('hro-id'));
name = element(by.css('#hro-name > label > input'));
selected = element(by.css('li.selected'));
navigateTo() {
- browser.get(browser.baseUrl);
+ browser.get(browser.baseUrl + 'heroes');
return browser.wait(EC.presenceOf(this.body), 5000) as Promise< void>;
}
getTitle() {
return this.title.getText() as Promise<string>;
}
getId() {
return this.id.getText() as Promise<string>;
}
setName(name) {
this.name.clear();
return this.name.sendKeys(name) as Promise<any>;
}
selectHero(index) {
const heroes = element.all(by.css('.badge'));
return heroes.get(index).click() as Promise<void>;
}
getSelected() {
return this.selected.getText() as Promise<string>;
}
}
add a routerLink
in the AppComponent
.
<h1 id="title">{{title}}</h1>
+ <nav>
+ <a routerLink="/heroes">Heroes</a>
+ </nav>
<router-outlet></router-outlet>
<app-messages></app-messages>
import {TestBed, async, ComponentFixture} from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { MessagesComponent } from './messages/messages.component';
describe('AppComponent', () => {
let component: AppComponent;
let fixture: ComponentFixture<AppComponent>;
let compiled: any;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule,
FormsModule
],
declarations: [
AppComponent,
MessagesComponent
],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AppComponent);
component = fixture.componentInstance;
fixture.detectChanges();
compiled = fixture.debugElement.nativeElement;
});
it('should create the app', () => {
expect(component).toBeTruthy();
});
it(`should have as title 'Tour of Heroes'`, () => {
expect(component.title).toEqual('Tour of Heroes');
});
it('should have app-heroes', () => {
expect(compiled.querySelector('app-heroes')).toBeDefined();
});
it('should have messaging', () => {
expect(compiled.querySelector('app-messages')).toBeTruthy();
});
+ it(`should have link to '/heroes'`, () => {
+ expect(compiled.querySelector('a').getAttribute('href'))
+ .toEqual('/heroes');
+ });
});
Let us have more than one view. For details see here
<h3>Top Heroes</h3>
<div class="grid grid-pad">
<a *ngFor="let hero of heroes" class="col-1-4">
<div class="module hero">
<h4>{{hero.name}}</h4>
</div>
</a>
</div>
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { DashboardComponent } from './dashboard.component';
import { HEROES } from '../mock-heroes';
import { By } from '@angular/platform-browser';
describe('DashboardComponent', () => {
let component: DashboardComponent;
let fixture: ComponentFixture<DashboardComponent>;
let compiled: any;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ DashboardComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(DashboardComponent);
component = fixture.componentInstance;
component.heroes = HEROES.slice(1, 5);
fixture.detectChanges();
compiled = fixture.debugElement.nativeElement;
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should have heroes', () => {
expect(component.heroes).toBeDefined();
});
it('should have a list of heroes', async () => {
const heroes = fixture.debugElement.queryAll(By.css(`.module.hero > h4`));
component.heroes.forEach( (hero, index) => {
expect(heroes[index].nativeElement.textContent).toContain(hero.name);
});
});
});
Adding a dashboard routerLink
in the application shell breaks the AppComponent
test.
<h1 id="title">{{title}}</h1>
<nav>
+ <a routerLink="/dashboard">Dashboard</a>
<a routerLink="/heroes">Heroes</a>
</nav>
<router-outlet></router-outlet>
<app-messages></app-messages>
Error: Expected '/dashboard' to equal '/heroes'.
at <Jasmine>
at UserContext.<anonymous> (http://localhost:9876/_karma_webpack_/src/app/app.component.spec.ts:51:8)
at ZoneDelegate.invoke (http://localhost:9876/_karma_webpack_/node_modules/zone.js/dist/zone-evergreen.js:365:1)
at ProxyZoneSpec.onInvoke (http://localhost:9876/_karma_webpack_/node_modules/zone.js/dist/zone-testing.js:305:1)
Let us fix this error, by refactoring the test for AppComponet
.
import {TestBed, async, ComponentFixture} from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { FormsModule } from '@angular/forms';
+ import { By } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { MessagesComponent } from './messages/messages.component';
describe('AppComponent', () => {
let component: AppComponent;
let fixture: ComponentFixture<AppComponent>;
let compiled: any;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule,
FormsModule
],
declarations: [
AppComponent,
MessagesComponent
],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AppComponent);
component = fixture.componentInstance;
fixture.detectChanges();
compiled = fixture.debugElement.nativeElement;
});
it('should create the app', () => {
expect(component).toBeTruthy();
});
it(`should have as title 'Tour of Heroes'`, () => {
expect(component.title).toEqual('Tour of Heroes');
});
it('should have app-heroes', () => {
expect(compiled.querySelector('app-heroes')).toBeDefined();
});
it('should have messaging', () => {
expect(compiled.querySelector('app-messages')).toBeTruthy();
});
+ it(`should have link to '/dashboard'`, () => {
+ const links = fixture.debugElement.queryAll(By.css(`a`));
+ expect(links[0].nativeElement.getAttribute('href'))
+ .toEqual('/dashboard');
+ });
it(`should have link to '/heroes'`, () => {
- expect(compiled.querySelector('a').getAttribute('href'))
- .toEqual('/heroes');
+ const links = fixture.debugElement.queryAll(By.css(`a`));
+ expect(links[1].nativeElement.getAttribute('href'))
+ .toEqual('/heroes');
});
});
Adding routerLink
to Dashboard component give the below error.
<h3>Top Heroes</h3>
<div class="grid grid-pad">
- <a *ngFor="let hero of heroes" class="col-1-4">
+ <a *ngFor="let hero of heroes" class="col-1-4" routerLink="/detail/{{hero.id}}">
<div class="module hero">
<h4>{{hero.name}}</h4>
</div>
</a>
</div>
Failed: Template parse errors:
Can't bind to 'routerLink' since it isn't a known property of 'a'. ("<h3>Top Heroes</h3>
<div class="grid grid-pad">
<a *ngFor="let hero of heroes" class="col-1-4" [ERROR ->]routerLink="/detail/{{hero.id}}">
<div class="module hero">
<h4>{{hero.name}}</h4>
"): ng:///DynamicTestModule/DashboardComponent.html@2:49
To fix add RouterTestingModule
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+ import { RouterTestingModule } from '@angular/router/testing';
import { DashboardComponent } from './dashboard.component';
import { HEROES } from '../mock-heroes';
import { By } from '@angular/platform-browser';
describe('DashboardComponent', () => {
let component: DashboardComponent;
let fixture: ComponentFixture<DashboardComponent>;
let compiled: any;
beforeEach(async(() => {
TestBed.configureTestingModule({
+ imports: [
+ RouterTestingModule
+ ],
declarations: [
DashboardComponent
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(DashboardComponent);
component = fixture.componentInstance;
component.heroes = HEROES.slice(1, 5);
fixture.detectChanges();
compiled = fixture.debugElement.nativeElement;
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should have heroes', () => {
expect(component.heroes).toBeDefined();
});
it('should have a list of heroes', async () => {
const heroes = fixture.debugElement.queryAll(By.css(`.module.hero > h4`));
component.heroes.forEach( (hero, index) => {
expect(heroes[index].nativeElement.textContent).toContain(hero.name);
});
});
});
We add a test for the routerLink="/detail/{{hero.id}}"
as shown below.
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { DashboardComponent } from './dashboard.component';
import { HEROES } from '../mock-heroes';
import { By } from '@angular/platform-browser';
describe('DashboardComponent', () => {
let component: DashboardComponent;
let fixture: ComponentFixture<DashboardComponent>;
let compiled: any;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule
],
declarations: [
DashboardComponent
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(DashboardComponent);
component = fixture.componentInstance;
component.heroes = HEROES.slice(1, 5);
fixture.detectChanges();
compiled = fixture.debugElement.nativeElement;
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should have heroes', () => {
expect(component.heroes).toBeDefined();
});
it('should have a list of heroes', async () => {
const heroes = fixture.debugElement.queryAll(By.css(`.module.hero > h4`));
component.heroes.forEach( (hero, index) => {
expect(heroes[index].nativeElement.textContent).toContain(hero.name);
});
});
+ it('should have a link of heroes', async () => {
+ const heroes = fixture.debugElement.queryAll(By.css(`a`));
+ component.heroes.forEach( (hero, index) => {
+ expect(heroes[index].nativeElement.getAttribute('href'))
+ .toContain(`/detail/${hero.id}`);
+ });
+ });
});
Remove HeroesComponent hero links
<ul class="heroes">
- <li *ngFor="let hero of heroes"
- [class.selected]="hero === selectedHero"
- (click)="onSelect(hero)">
+ <a routerLink="/detail/{{hero.id}}">
<span class="badge">{{hero.id}}</span> {{hero.name}}
+ </a>
</li>
</ul>
Failing test after the new addition due to missing
Failed: Template parse errors:
Can't bind to 'routerLink' since it isn't a known property of 'a'. ("<ul class="heroes">
<li *ngFor="let hero of heroes">
<a [ERROR ->]routerLink="/detail/{{hero.id}}">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</a>"): ng:///DynamicTestModule/HeroesComponent.html@2:7
Add the RouterTestingModule
to the heroes.component.spec.ts
.
import { Component, OnInit } from '@angular/core';
import { Hero } from '../hero';
import { HeroService } from '../hero.service';
@Component({
selector: 'app-heroes',
templateUrl: './heroes.component.html',
styleUrls: ['./heroes.component.css']
})
export class HeroesComponent implements OnInit {
heroes: Hero[];
- selectedHero: Hero;
constructor(
private heroService: HeroService
) { }
ngOnInit() {
this.getHeroes();
}
- onSelect(hero) {
- this.selectedHero = hero;
- }
getHeroes(): void {
this.heroService.getHeroes().subscribe(
heroes => this.heroes = heroes
);
}
}
Removing dead code, shown above makes test to fail?
NullInjectorError: StaticInjectorError(DynamicTestModule)[HeroDetailComponent -> ActivatedRoute]:
StaticInjectorError(Platform: core)[HeroDetailComponent -> ActivatedRoute]:
NullInjectorError: No provider for ActivatedRoute!
Solution: RouterTestingModule
Continued here