Skip to content

xgirma/angular-tour-of-heroes

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

๐Ÿฑ ๐Ÿถ Testing Angular Tour Of Heroes ๐Ÿท ๐Ÿฎ ๐Ÿœ

GitHub Actions status | xgirma/angular_sandbox

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.

Bootstraping

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.

๐Ÿฑ unit test: app.component.spec.ts

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!');
  });
});

๐Ÿถ e2e test: app.e2e-spec.ts

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.

๐Ÿฑ unit test: result: app.component.spec.ts

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>

๐Ÿถ e2e test: result: app.e2e-spec.ts

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.

๐Ÿฑ unit test: app.component.spec.ts

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!');
-  });
});

๐Ÿถ e2e test: app.e2e-spec.ts

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.

๐Ÿฑ unit test: result: app.component.spec.ts

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

๐Ÿถ e2e test: reult: app.e2e-spec.ts

[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 !!!

Setting Continious Integration

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.

package.json

{
  "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.

.travis.yml

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.

nodejs.yml

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

AppComponent (v0.1)

Our App begins here. First thing we change the title. See details here

๐Ÿฑ unit test: app.component.ts

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';
}

๐Ÿท view: app.component.html

<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.

๐Ÿฑ unit test: app.component.spec.ts

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).

๐Ÿถ e2e test: app.po.ts

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>;
  }
}

๐Ÿถ e2e test: app.e2e-spec.ts

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.

AppComponent(0.2), HeroesComponent(0.1)

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.

๐Ÿฎ component: heroes.component.ts

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() {
  }

}

๐Ÿท view: heroes.component.html

- <p>heroes works!</p>
+ {{hero}}

Then, insert HeroesComponent inside the parent AppComponent.

๐Ÿท view: app.component.html

<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.

๐Ÿฑ unit test: app.component.spec.ts

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.

๐Ÿฑ unit test: app.component.spec.ts

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.

๐Ÿฑ unit test: app.component.spec.ts

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

๐Ÿฑ unit test: result: app.component.spec.ts, heroes.component.spec.ts

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.

๐Ÿฑ unit test: heroes.component.spec.ts

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.

๐Ÿฑ unit test: heroes.component.spec.ts

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.

๐Ÿท view: heroes.component.html

- {{hero}}
+ <p>{{hero}}</p>

Updated test with compiled.querySelector('p') help us to find the element, hence the assertion passes.

๐Ÿฑ unit test: heroes.component.spec.ts

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.

๐Ÿถ e2e test: heroes.po.ts

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>;
  }
}

๐Ÿถ e2e test: heroes.e2e-spec.ts

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

๐Ÿถ e2e test: reult: app.e2e-spec.ts, heroes.e2e-spec.ts

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

AppComponent(0.2), HeroesComponent(0.2)

So far the app only shows the hero name. Here, we want to display additional hero information. See details here

๐Ÿฎ component: heroes.component.ts

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() {
  }
}

๐Ÿท view: heroes.component.html

- <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.

๐Ÿฑ unit test: result: heroes.component.spec.ts

HeroesComponent
    โœ— should have 'Windstorm' as hero
        TypeError: Cannot read property 'textContent' of null
            at <Jasmine>

๐Ÿถ e2e test: result: heroes.e2e-spec.ts

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.

๐Ÿท view: heroes.component.htm

<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.

๐Ÿฑ unit test: heroes.component.spec.ts

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));
  });
});

๐Ÿฑ unit test: result: heroes.component.spec.ts

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

๐Ÿถ e2e test: heroes.po.ts

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>;
  }
}

๐Ÿถ e2e test: heroes.e2e-spec.ts

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}`);
  });
});

๐Ÿถ e2e test: result: heroes.e2e-spec.ts

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.

AppComponent(0.2), HeroesComponent(0.3)

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.

๐Ÿฑ unit test: heroes.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));
  });
});

๐Ÿฑ unit test: app.component.spec.ts

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.

๐Ÿฑ unit test: heroes.component.spec.ts

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.

๐Ÿฑ unit test: result: heroes.component.spec.ts

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.

๐Ÿถ e2e test: heroes.po.ts

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>;
+  }
}

๐Ÿถ e2e test: heroes.e2e-spec.ts

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');
+  });
});

๐Ÿถ e2e test: result: heroes.e2e-spec.ts

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.

AppComponent(0.2), HeroesComponent(0.4)

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

๐Ÿฑ unit test: heroes.component.spec.ts

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;
+  }
}

๐Ÿท view: heroes.component.html

- <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:

  1. the inital state, where we have no selected hero
  2. the state we get details after we select a hero
  3. the state we get when we modify a selected hero

๐Ÿฑ unit test: heroes.component.spec.ts

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.

๐Ÿฑ unit test: result: heroes.component.spec.ts

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

๐Ÿถ e2e test: heroes.po

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>;
  }
}

๐Ÿถ e2e test: heroes.e2e-spec.ts

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');
  });
});

๐Ÿถ e2e test: result: heroes.e2e-spec.ts

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.

AppComponent(0.2), HeroesComponent(0.4), HeroDetailComponent(0.1)

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.

๐Ÿท view: heroes.component.html

<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>

๐Ÿฑ unit test: heroes.component.spec

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.

๐Ÿท view: heroes.component.html

<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>

:caw: component: hero-detail.component.ts

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.

๐Ÿฑ unit test: heroes.component.spec.ts, app.component.spec.ts

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.

๐Ÿฑ unit test: app.component.spec

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.

๐Ÿฑ unit test: error: hero-detail.component.spec.ts

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

๐Ÿฑ unit test: hero-detail.component.spec.ts

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.

๐Ÿฑ unit test : hero-detail.component.spec.ts

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.

๐Ÿฑ unit test: hero-detail.component.spec.ts

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.

AppComponent(0.2), HeroesComponent(0.5), HeroDetailComponent(0.1), HeroService(0.1)

Component shouldn't fetch or save data directly. We will get the data from HeroService. See details here.

๐Ÿœ service: hero.service.ts

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);
  }
}

๐Ÿฎ component: heroes.component.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.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.

๐Ÿท view: heroes.component.html

<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>

๐Ÿฑ unit test: heroes.component.ts

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.

๐Ÿฑ unit test: hero.service.spec.ts

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

๐Ÿท view : app.component.html

<h1 id="title">{{title}}</h1>
<router-outlet></router-outlet>
++ <app-messages></app-messages>

๐Ÿฑ unit test: error : app.component.spec.ts

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 MessagesComponentmakes the unit test to fail. Let us add the MessagesComponent inside the app.component.spec.ts to fix and also add one more test.

๐Ÿฑ unit test: app.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 { 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();
+  });
});

๐Ÿœ service: message.service.ts

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.

๐Ÿฑ unit test: message.service.spec.ts

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);
  });
});

AppComponent(0.4), HeroesComponent(0.6), HeroDetailComponent(0.1), HeroService(0.3)

At this point we will move from using the AppComponent as a shell to introducing a formal router module. See details here.

โšก module: app-routing.module.ts

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 { }

๐Ÿฎ app.component.ts

<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.

๐Ÿฑ unit test: app.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();
  });
});

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.

๐Ÿถ e2e test: heroes.po.ts

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>;
  }
}

AppComponent(0.5)

add a routerLink in the AppComponent.

๐Ÿท view: app.component.html

<h1 id="title">{{title}}</h1>
+ <nav>
+  <a routerLink="/heroes">Heroes</a>
+ </nav>
<router-outlet></router-outlet>
<app-messages></app-messages>

๐Ÿฑ unit test: app.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 { 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');
+  });
});

DashboardComponent(0.1), AppComponent(0.6)

Let us have more than one view. For details see here

๐Ÿท view: dashboard.component.html

<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>

๐Ÿฑ unit test: dashboard.component.spec.ts

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.

๐Ÿท view: app.component.html

<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>

๐Ÿฑ unit test: error: app.component.spec.ts

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.

๐Ÿฑ unit test: app.component.spec.ts

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.

๐Ÿท view: dashboard.component.html

<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>

๐Ÿฑ unit test : error: dashboard.component.spec.ts

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

๐Ÿฑ unit test : dashboard.component.spec.ts

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.

๐Ÿฑ unit test: dashboard.component.spec.ts

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

๐Ÿท view: heroes.component.html

<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

๐Ÿฑ unit test: heroes.component.spec.ts

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.

๐Ÿฎ component: heroes.component.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