docs(testing): simplify and accelerate path to Angular component testing (#2879)

Rob Wormald recognized that we had no plunker for a simple component test. Inspired improved learning path for testing including:
* Add plunkers for both inline and external template versions of  the simplest `BannerComponent`
* Added the banner-specs for that purpose and also a quickstart-specs in the setup folder
* Adjusted prose in testing and setup-systemjs-anatomy to call these out
* Moved testing of external template spec earlier in the guide because it is likely to be needed right away.
* Add comments on the optional "testing" folder and corrects var names to match
* Leaves Jasmine Spec Runner output visible when tests finish
This commit is contained in:
Ward Bell 2016-12-05 11:46:53 -08:00 committed by GitHub
parent caec707ffa
commit 695df67929
28 changed files with 788 additions and 497 deletions

View File

@ -104,6 +104,7 @@ var _exampleBoilerplateFiles = [
var _exampleDartWebBoilerPlateFiles = ['a2docs.css', 'styles.css']; var _exampleDartWebBoilerPlateFiles = ['a2docs.css', 'styles.css'];
var _exampleUnitTestingBoilerplateFiles = [ var _exampleUnitTestingBoilerplateFiles = [
'browser-test-shim.js',
'karma-test-shim.js', 'karma-test-shim.js',
'karma.conf.js' 'karma.conf.js'
]; ];
@ -587,10 +588,14 @@ function deleteExampleBoilerPlate() {
gutil.log('Deleting example boilerplate files'); gutil.log('Deleting example boilerplate files');
var examplePaths = getExamplePaths(EXAMPLES_PATH); var examplePaths = getExamplePaths(EXAMPLES_PATH);
var dartExampleWebPaths = getDartExampleWebPaths(EXAMPLES_PATH); var dartExampleWebPaths = getDartExampleWebPaths(EXAMPLES_PATH);
var unittestPaths = getUnitTestingPaths(EXAMPLES_PATH);
return deleteFiles(_exampleBoilerplateFiles, examplePaths) return deleteFiles(_exampleBoilerplateFiles, examplePaths)
.then(function() { .then(function() {
return deleteFiles(_exampleDartWebBoilerPlateFiles, dartExampleWebPaths); return deleteFiles(_exampleDartWebBoilerPlateFiles, dartExampleWebPaths);
})
.then(function() {
return deleteFiles(_exampleUnitTestingBoilerplateFiles, unittestPaths);
}); });
} }

View File

@ -45,6 +45,7 @@ button:disabled {
nav a { nav a {
padding: 5px 10px; padding: 5px 10px;
text-decoration: none; text-decoration: none;
margin-right: 10px;
margin-top: 10px; margin-top: 10px;
display: inline-block; display: inline-block;
background-color: #eee; background-color: #eee;

View File

@ -63,7 +63,6 @@
"karma": "^1.3.0", "karma": "^1.3.0",
"karma-chrome-launcher": "^2.0.0", "karma-chrome-launcher": "^2.0.0",
"karma-cli": "^1.0.1", "karma-cli": "^1.0.1",
"karma-htmlfile-reporter": "^0.3.4",
"karma-jasmine": "^1.0.2", "karma-jasmine": "^1.0.2",
"karma-jasmine-html-reporter": "^0.2.2", "karma-jasmine-html-reporter": "^0.2.2",
"karma-phantomjs-launcher": "^1.0.2", "karma-phantomjs-launcher": "^1.0.2",

View File

@ -1,18 +1,16 @@
/* tslint:disable:no-unused-variable */
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core'; import { DebugElement } from '@angular/core';
//////// SPECS /////////////
describe('AppComponent', function () { describe('AppComponent', function () {
let de: DebugElement; let de: DebugElement;
let comp: AppComponent; let comp: AppComponent;
let fixture: ComponentFixture<AppComponent>; let fixture: ComponentFixture<AppComponent>;
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [ AppComponent ] declarations: [ AppComponent ]
}) })
.compileComponents(); .compileComponents();

View File

@ -0,0 +1,3 @@
{
"unittesting": true
}

View File

@ -0,0 +1,40 @@
<!-- Run application specs in a browser -->
<!-- #docregion -->
<!DOCTYPE html>
<html>
<head>
<base href="/">
<title>1st Specs</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css">
<link rel="stylesheet" href="node_modules/jasmine-core/lib/jasmine-core/jasmine.css">
</head>
<body>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="node_modules/jasmine-core/lib/jasmine-core/jasmine.js"></script>
<script src="node_modules/jasmine-core/lib/jasmine-core/jasmine-html.js"></script>
<script src="node_modules/jasmine-core/lib/jasmine-core/boot.js"></script>
<script src="node_modules/reflect-metadata/Reflect.js"></script>
<script src="node_modules/zone.js/dist/zone.js"></script>
<script src="node_modules/zone.js/dist/long-stack-trace-zone.js"></script>
<script src="node_modules/zone.js/dist/proxy.js"></script>
<script src="node_modules/zone.js/dist/sync-test.js"></script>
<script src="node_modules/zone.js/dist/jasmine-patch.js"></script>
<script src="node_modules/zone.js/dist/async-test.js"></script>
<script src="node_modules/zone.js/dist/fake-async-test.js"></script>
<!-- #docregion files -->
<script>
var __spec_files__ = [
'app/app.component.spec'
];
</script>
<!-- #enddocregion files-->
<script src="browser-test-shim.js"></script>
</body>
</html>

View File

@ -0,0 +1,15 @@
{
"description": "Quickstart AppComponent Testing",
"files":[
"browser-test-shim.js",
"systemjs.config.extras.js",
"app/app.component.ts",
"app/app.component.spec.ts",
"quickstart-specs.html"
],
"main": "quickstart-specs.html",
"open": "app/app.component.spec.ts",
"tags": ["quickstart", "setup", "testing"],
"includeSystemConfig": true
}

View File

@ -0,0 +1,11 @@
/**
* Add barrels and stuff
* Adjust as necessary for your application needs.
*/
// (function (global) {
// System.config({
// packages: {
// // add packages here
// }
// });
// })(this);

View File

@ -8,6 +8,7 @@
"1st-specs.html" "1st-specs.html"
], ],
"main": "1st-specs.html", "main": "1st-specs.html",
"open": "app/1st.spec.ts",
"tags": ["testing"], "tags": ["testing"],
"includeSystemConfig": true "includeSystemConfig": true
} }

View File

@ -34,6 +34,8 @@
'app/app.component.spec', 'app/app.component.spec',
'app/app.component.router.spec', 'app/app.component.router.spec',
'app/banner.component.spec', 'app/banner.component.spec',
'app/banner.component.detect-changes.spec',
'app/banner-inline.component.spec',
'app/dashboard/dashboard.component.spec', 'app/dashboard/dashboard.component.spec',
'app/dashboard/dashboard.component.no-testbed.spec', 'app/dashboard/dashboard.component.no-testbed.spec',
'app/dashboard/dashboard-hero.component.spec', 'app/dashboard/dashboard-hero.component.spec',

View File

@ -0,0 +1,55 @@
// #docplaster
// #docregion imports
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { BannerComponent } from './banner-inline.component';
// #enddocregion imports
// #docregion setup
describe('BannerComponent (inline template)', () => {
let comp: BannerComponent;
let fixture: ComponentFixture<BannerComponent>;
let de: DebugElement;
let el: HTMLElement;
// #docregion before-each
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ BannerComponent ], // declare the test component
});
fixture = TestBed.createComponent(BannerComponent);
comp = fixture.componentInstance; // BannerComponent test instance
// query for the title <h1> by CSS element selector
de = fixture.debugElement.query(By.css('h1'));
el = de.nativeElement;
});
// #enddocregion before-each
// #enddocregion setup
// #docregion test-w-o-detect-changes
it('no title in the DOM until manually call `detectChanges`', () => {
expect(el.textContent).toEqual('');
});
// #enddocregion test-w-o-detect-changes
// #docregion tests
it('should display original title', () => {
fixture.detectChanges();
expect(el.textContent).toContain(comp.title);
});
it('should display a different test title', () => {
comp.title = 'Test Title';
fixture.detectChanges();
expect(el.textContent).toContain('Test Title');
});
// #enddocregion tests
// #docregion setup
});
// #enddocregion setup

View File

@ -0,0 +1,11 @@
// #docregion
import { Component } from '@angular/core';
@Component({
selector: 'app-banner',
template: '<h1>{{title}}</h1>'
})
export class BannerComponent {
title = 'Test Tour of Heroes';
}

View File

@ -0,0 +1 @@
h1 { color: green; font-size: 350%}

View File

@ -0,0 +1,59 @@
// #docplaster
// #docregion
// #docregion import-async
import { async } from '@angular/core/testing';
// #enddocregion import-async
// #docregion import-ComponentFixtureAutoDetect
import { ComponentFixtureAutoDetect } from '@angular/core/testing';
// #enddocregion import-ComponentFixtureAutoDetect
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { BannerComponent } from './banner.component';
describe('BannerComponent (AutoChangeDetect)', () => {
let comp: BannerComponent;
let fixture: ComponentFixture<BannerComponent>;
let de: DebugElement;
let el: HTMLElement;
beforeEach(async(() => {
// #docregion auto-detect
TestBed.configureTestingModule({
declarations: [ BannerComponent ],
providers: [
{ provide: ComponentFixtureAutoDetect, useValue: true }
]
})
// #enddocregion auto-detect
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(BannerComponent);
comp = fixture.componentInstance;
de = fixture.debugElement.query(By.css('h1'));
el = de.nativeElement;
});
// #docregion auto-detect-tests
it('should display original title', () => {
// Hooray! No `fixture.detectChanges()` needed
expect(el.textContent).toContain(comp.title);
});
it('should still see original title after comp.title change', () => {
const oldTitle = comp.title;
comp.title = 'Test Title';
// Displayed title is old because Angular didn't hear the change :(
expect(el.textContent).toContain(oldTitle);
});
it('should display updated title after detectChanges', () => {
comp.title = 'Test Title';
fixture.detectChanges(); // detect changes explicitly
expect(el.textContent).toContain(comp.title);
});
// #enddocregion auto-detect-tests
});

View File

@ -0,0 +1 @@
<h1>{{title}}</h1>

View File

@ -1,25 +1,30 @@
// #docplaster // #docplaster
// #docregion import { async, ComponentFixture, TestBed } from '@angular/core/testing';
// #docregion imports
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core'; import { DebugElement } from '@angular/core';
import { BannerComponent } from './banner.component'; import { BannerComponent } from './banner.component';
// #enddocregion imports
// #docregion setup describe('BannerComponent (templateUrl)', () => {
let comp: BannerComponent;
let fixture: ComponentFixture<BannerComponent>;
let de: DebugElement;
let el: HTMLElement;
describe('BannerComponent', () => { let comp: BannerComponent;
beforeEach(() => { let fixture: ComponentFixture<BannerComponent>;
let de: DebugElement;
let el: HTMLElement;
// #docregion async-before-each
// async beforeEach
beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [ BannerComponent ], // declare the test component declarations: [ BannerComponent ], // declare the test component
}); })
.compileComponents(); // compile template and css
}));
// #enddocregion async-before-each
// #docregion sync-before-each
// synchronous beforeEach
beforeEach(() => {
fixture = TestBed.createComponent(BannerComponent); fixture = TestBed.createComponent(BannerComponent);
comp = fixture.componentInstance; // BannerComponent test instance comp = fixture.componentInstance; // BannerComponent test instance
@ -27,10 +32,13 @@ describe('BannerComponent', () => {
// query for the title <h1> by CSS element selector // query for the title <h1> by CSS element selector
de = fixture.debugElement.query(By.css('h1')); de = fixture.debugElement.query(By.css('h1'));
el = de.nativeElement; el = de.nativeElement;
}); });
// #enddocregion setup // #enddocregion sync-before-each
// #docregion tests
it('no title in the DOM until manually call `detectChanges`', () => {
expect(el.textContent).toEqual('');
});
it('should display original title', () => { it('should display original title', () => {
fixture.detectChanges(); fixture.detectChanges();
expect(el.textContent).toContain(comp.title); expect(el.textContent).toContain(comp.title);
@ -41,90 +49,5 @@ describe('BannerComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
expect(el.textContent).toContain('Test Title'); expect(el.textContent).toContain('Test Title');
}); });
// #enddocregion tests
// #docregion test-w-o-detect-changes
it('no title in the DOM until manually call `detectChanges`', () => {
expect(el.textContent).toEqual('');
});
// #enddocregion test-w-o-detect-changes
// #docregion setup
});
// #enddocregion setup
///////// With AutoChangeDetect /////
import { ComponentFixtureAutoDetect } from '@angular/core/testing';
describe('BannerComponent with AutoChangeDetect', () => {
beforeEach(() => {
// #docregion auto-detect
fixture = TestBed.configureTestingModule({
declarations: [ BannerComponent ],
providers: [
{ provide: ComponentFixtureAutoDetect, useValue: true }
]
})
// #enddocregion auto-detect
.createComponent(BannerComponent);
comp = fixture.componentInstance; // BannerComponent test instance
// query for the title <h1> by CSS element selector
de = fixture.debugElement.query(By.css('h1'));
el = de.nativeElement;
});
// #docregion auto-detect-tests
it('should display original title', () => {
// Hooray! No `fixture.detectChanges()` needed
expect(el.textContent).toContain(comp.title);
});
it('should still see original title after comp.title change', () => {
const oldTitle = comp.title;
comp.title = 'Test Title';
// Displayed title is old because Angular didn't hear the change :(
expect(el.textContent).toContain(oldTitle);
});
it('should display updated title after detectChanges', () => {
comp.title = 'Test Title';
fixture.detectChanges(); // detect changes explicitly
expect(el.textContent).toContain(comp.title);
});
// #enddocregion auto-detect-tests
});
describe('BannerComponent (simpified)', () => {
// #docregion simple-example-before-each
beforeEach(() => {
// refine the test module by declaring the test component
TestBed.configureTestingModule({
declarations: [ BannerComponent ],
});
// create component and test fixture
fixture = TestBed.createComponent(BannerComponent);
// get test component from the fixture
comp = fixture.componentInstance;
});
// #enddocregion simple-example-before-each
// #docregion simple-example-it
it('should display original title', () => {
// trigger change detection to update the view
fixture.detectChanges();
// query for the title <h1> by CSS element selector
de = fixture.debugElement.query(By.css('h1'));
// confirm the element's content
expect(de.nativeElement.textContent).toContain(comp.title);
});
// #enddocregion simple-example-it
}); });

View File

@ -1,9 +1,11 @@
// #docregion // #docregion
import { Component } from '@angular/core'; import { Component } from '@angular/core';
@Component({ @Component({
moduleId: module.id,
selector: 'app-banner', selector: 'app-banner',
template: '<h1>{{title}}</h1>' templateUrl: 'banner.component.html',
styleUrls: ['banner.component.css']
}) })
export class BannerComponent { export class BannerComponent {
title = 'Test Tour of Heroes'; title = 'Test Tour of Heroes';

View File

@ -0,0 +1,39 @@
<!-- Run application specs in a browser -->
<!-- #docregion -->
<!DOCTYPE html>
<html>
<head>
<base href="/">
<title>Banner Component (inline template) Specs</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="node_modules/jasmine-core/lib/jasmine-core/jasmine.css">
</head>
<body>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="node_modules/jasmine-core/lib/jasmine-core/jasmine.js"></script>
<script src="node_modules/jasmine-core/lib/jasmine-core/jasmine-html.js"></script>
<script src="node_modules/jasmine-core/lib/jasmine-core/boot.js"></script>
<script src="node_modules/reflect-metadata/Reflect.js"></script>
<script src="node_modules/zone.js/dist/zone.js"></script>
<script src="node_modules/zone.js/dist/long-stack-trace-zone.js"></script>
<script src="node_modules/zone.js/dist/proxy.js"></script>
<script src="node_modules/zone.js/dist/sync-test.js"></script>
<script src="node_modules/zone.js/dist/jasmine-patch.js"></script>
<script src="node_modules/zone.js/dist/async-test.js"></script>
<script src="node_modules/zone.js/dist/fake-async-test.js"></script>
<!-- #docregion files -->
<script>
var __spec_files__ = [
'app/banner-inline.component.spec'
];
</script>
<!-- #enddocregion files-->
<script src="browser-test-shim.js"></script>
</body>
</html>

View File

@ -0,0 +1,15 @@
{
"description": "Testing - banner-inline.component.specs",
"files":[
"browser-test-shim.js",
"systemjs.config.extras.js",
"app/banner-inline.component.ts",
"app/banner-inline.component.spec.ts",
"banner-inline-specs.html"
],
"main": "banner-inline-specs.html",
"open": "app/banner-inline.component.spec.ts",
"tags": ["testing"],
"includeSystemConfig": true
}

View File

@ -0,0 +1,39 @@
<!-- Run application specs in a browser -->
<!-- #docregion -->
<!DOCTYPE html>
<html>
<head>
<base href="/">
<title>Banner Component Specs</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="node_modules/jasmine-core/lib/jasmine-core/jasmine.css">
</head>
<body>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="node_modules/jasmine-core/lib/jasmine-core/jasmine.js"></script>
<script src="node_modules/jasmine-core/lib/jasmine-core/jasmine-html.js"></script>
<script src="node_modules/jasmine-core/lib/jasmine-core/boot.js"></script>
<script src="node_modules/reflect-metadata/Reflect.js"></script>
<script src="node_modules/zone.js/dist/zone.js"></script>
<script src="node_modules/zone.js/dist/long-stack-trace-zone.js"></script>
<script src="node_modules/zone.js/dist/proxy.js"></script>
<script src="node_modules/zone.js/dist/sync-test.js"></script>
<script src="node_modules/zone.js/dist/jasmine-patch.js"></script>
<script src="node_modules/zone.js/dist/async-test.js"></script>
<script src="node_modules/zone.js/dist/fake-async-test.js"></script>
<!-- #docregion files -->
<script>
var __spec_files__ = [
'app/banner.component.spec'
];
</script>
<!-- #enddocregion files-->
<script src="browser-test-shim.js"></script>
</body>
</html>

View File

@ -0,0 +1,17 @@
{
"description": "Testing - banner.component.specs",
"files":[
"browser-test-shim.js",
"systemjs.config.extras.js",
"app/banner.component.css",
"app/banner.component.html",
"app/banner.component.ts",
"app/banner.component.spec.ts",
"banner-specs.html"
],
"main": "banner-specs.html",
"open": "app/banner.component.spec.ts",
"tags": ["testing"],
"includeSystemConfig": true
}

View File

@ -43,7 +43,7 @@ function importSystemJsExtras(){
return System.import('systemjs.config.extras.js') return System.import('systemjs.config.extras.js')
.catch(function(reason) { .catch(function(reason) {
console.log( console.log(
'Warning: System.import could not load the optional "systemjs.config.extras.js". Did you omit it by accident? Continuing without it.' 'Note: System.import could not load "systemjs.config.extras.js" where you might have added more configuration. It is an optional file so we will continue without it.'
); );
console.log(reason); console.log(reason);
}); });

View File

@ -36,7 +36,7 @@ var allSpecFiles = Object.keys(window.__karma__.files)
System.config({ System.config({
baseURL: 'base', baseURL: 'base',
// Extend usual application package list with test folder // Extend usual application package list with testing folder
packages: { 'testing': { main: 'index.js', defaultExtension: 'js' } }, packages: { 'testing': { main: 'index.js', defaultExtension: 'js' } },
// Assume npm: is set in `paths` in systemjs.config // Assume npm: is set in `paths` in systemjs.config

View File

@ -5,6 +5,7 @@ module.exports = function(config) {
var appSrcBase = 'app/'; // app source TS files var appSrcBase = 'app/'; // app source TS files
var appAssets = 'base/app/'; // component assets fetched by Angular's compiler var appAssets = 'base/app/'; // component assets fetched by Angular's compiler
// Testing helpers (optional) are conventionally in a folder called `testing`
var testingBase = 'testing/'; // transpiled test JS and map files var testingBase = 'testing/'; // transpiled test JS and map files
var testingSrcBase = 'testing/'; // test source TS files var testingSrcBase = 'testing/'; // test source TS files

View File

@ -5,6 +5,10 @@ block includes
The Angular documentation is a living document with continuous improvements. The Angular documentation is a living document with continuous improvements.
This log calls attention to recent significant changes. This log calls attention to recent significant changes.
## Testing: added component test plunkers (2016-12-02)
Added two plunkers that each test _one simple component_ so you can write a component test plunker of your own: <live-example name="setup" plnkr="quickstart-specs">one</live-example> for the QuickStart seed's `AppComponent` and <live-example name="testing" plnkr="banner-specs">another</live-example> for the Testing guide's `BannerComponent`.
Linked to these plunkers in [Testing](testing.html#live-examples) and [Setup anatomy](setup-systemjs-anatomy) guides.
## Internationalization: pluralization and _select_ (2016-11-30) ## Internationalization: pluralization and _select_ (2016-11-30)
The [Internationalization (i18n)](i18n.html) guide explains how to handle pluralization and The [Internationalization (i18n)](i18n.html) guide explains how to handle pluralization and
translation of alternative texts with `select`. translation of alternative texts with `select`.

View File

@ -22,11 +22,15 @@ table(width="100%")
td <code>app/...</code> td <code>app/...</code>
td td
:marked :marked
Your Angular application files. Your Angular application files go here.
Ships with the "Hello Angular" sample's Ships with the "Hello Angular" sample's
`AppComponent`, `AppModule`, a component unit test (`app.component.spec.ts`), and `AppComponent`, `AppModule`, a component unit test (`app.component.spec.ts`), and
the bootstrap file, `main.ts`. the bootstrap file, `main.ts`.
Try the <live-example name="setup">sample application</live-example>
and the <live-example name="setup" plnkr="quickstart-specs">unit test</live-example>
as _live examples_.
tr tr
td <code>e2e/...</code> td <code>e2e/...</code>
td td

File diff suppressed because it is too large Load Diff

View File

@ -210,7 +210,9 @@ class PlunkerBuilder {
systemJsConfigPath = '/_boilerplate/systemjs.config.web.build.js'; systemJsConfigPath = '/_boilerplate/systemjs.config.web.build.js';
} }
this.systemjsConfig = fs.readFileSync(this.basePath + systemJsConfigPath, 'utf-8'); this.systemjsConfig = fs.readFileSync(this.basePath + systemJsConfigPath, 'utf-8');
this.systemjsConfig += this.copyrights.jsCss;
// Copyright already added to web versions of systemjs.config
// this.systemjsConfig += this.copyrights.jsCss;
} }
_htmlToElement(document, html) { _htmlToElement(document, html) {