Inhaltsverzeichnis


Nichts verpassen?

angular ‱ testing ‱ advanced | Gerard Sans ‱ | 29 Minuten

In diesem Artikel wollen wir die gĂ€ngigsten Unit-Tests fĂŒr Angular Anwendungen, wie zum Beispiel Komponenten, Services, Http und Pipes abdecken. Wir werden aber auch weniger bekannte Bereiche, wie Directives, Routes und Testen von Observables behandeln. Als Referenz und Basis fĂŒr deine eigenen Tests stellen wir dir kleine Beispiele vor. Weiterhin haben wir eine Testing-Checkliste, um bei der Erstellung deiner eigenen Tests ein einfachen Leitfaden zu bieten.

Testen wurde von ReleaseCandidate(RC) von Angular Version 2 bis zur ersten Final ziemlich stark verbessert. Durch den Umstieg auf NgModule haben sich weiterhin viele Sachen im Bereich testen vereinfacht. Das CoreTeam von Angular hat hart gearbeitet, um Boilerplate-Code zu reduzieren und neben Jasmine auch andere Test-Frameworks wie Mocha zu unterstĂŒtzen.

In diesem Artikel werden wir folgendes abdecken:

  • EinfĂŒhrung in Jasmine: suites, specs, exprectations und matchers


  • Testen in Angular: setup, dependency injection und testing checklist

  • Testing-Utilities: TestBed, inject, fixtures, async, fakeAsync/tick und jasmine.done().

  • Testbeispiele: Components, Services, Http+MockBackend, Directives, Pipes, Routes, Observables und EventEmitter.

Alle 26 Test-Specs zum selber ausprobieren und Àndern findest du in diesem Plunker.

„Wir haben in diesem Beispielen Jasmine verwendet, es ist aber problemlos Möglich auch andere Test-Frameworks wie Mocha zu verwenden.

EinfĂŒhrung in Jasmine

Jasmine ist ein Open-Source-Test-Framwork von Pivotal Labs. Die verwendete DSL(domain specific language) verfolgt die GrundsĂ€tze des behaviour-driven-developments, somit ist es sehr leicht hiermit natĂŒrlich-sprachliche Tests zu definieren.

Generelle Konzepte der Jasmine-DSL

Suites – describe(String, function) - Eine Suite definiert ein logischen Abschnitt von Spezifikationen. Sie wird mit einem Titel und einer Funktion aufgerufen, welche Spezifikationen oder auch weitere Suits enthalten kann.

Specs – it(string, function) Eine Spec wird mit it eingeleitet. Auch hier definieren wir ein Titel ĂŒber den ersten Parameter. Der zweite Parameter erwartet eine Funktion welche dann die tatsĂ€chlichen technischen Erwartungen(Expectations) enthĂ€lt.

Expectations expect(actual).toBe(expected) – Eine Expectation(Erwartung) ist die technische Implementierung deines Tests, die letztendlich einfach nach true oder false auswertet.

Matchers – sind vordefinierte Helfer fĂŒr gemeinsame Expectations. z.B.: toBe(expected), toEqual(expected). Hier findest du die komplette Liste.

“Beachte, dass toEqual() einen tiefen Objekt-Match macht, hier wird also der Inhalt von zwei Objekten verglichen. Die Funktion .toBe() prĂŒft auf Referenzgleichheit. Also: Ist actual und expected das gleiche Objekt

“Wenn ihr nur eine Submenge eurer Test ausfĂŒhren wollt könnt ihr innerhalb euerer Tests auf diese Tests einen Fokus setzen. Die Aufrufe heißen dann fdescribe() bzw. fit() - das f steht fĂŒr Fokus! Eine deutlich bessere Alternative als bei großen Testsuites sehr lange zu warten oder gar Tests auszukommentieren. Allerdings sollten diese Funktionen nur bei euch lokal und temporĂ€r genutzt werden und nicht in euer Repository wandern, da ihr sonst andere Personen im Team oder euerer CI die Möglichkeit nehmt alle Tests auszufĂŒhren und Chaos erzeugt.”

Setup und Teardown

Manchmal haben wir eine Summe von Tests, welche gleichen oder Àhnliche Vorbedingungen haben z.B. ein bestimmten Zustand unser Directive oder eines Services.

Jasmine bietet dir vier Handler an, um unseren Setup- und Teardown-Code fĂŒr Tests zu definieren:

  • beforeEach, afterEach einmal pro Spec (it)
  • beforeAll, afterAll einmal pro Test-Suite (describe)

Testen Einrichtung

FĂŒr die Einrichtung von Jasmine gibt es verschiedene Optionen. In echten Entwicklungs-Umgebungen solltet ihr eure Test-Run-Konfiguration ĂŒber Test-Runner wie Karma managen. In unseren Beispiel nutzen wir dies nicht, da wir uns auf die eigentlich Angular-Tests konzentieren wollen und nicht auf Tools wie Karma und Co. Darum laufen unsere Tests hier ĂŒber eine SpecRunner.html. Schauen wir uns diese Datei also einmal an.

„Dieses Setup ist nur als Referenz anzusehen und wird nur mit Plunker funktionieren“

<!-- Jasmine dependencies -->
<link href="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.4.1/jasmine.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.4.1/jasmine.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.4.1/jasmine-html.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.4.1/boot.js"></script>

<!-- Angular dependencies -->
<script src="https://unpkg.com/zone.js/dist/zone.js"></script>
<script src="https://unpkg.com/zone.js/dist/long-stack-trace-zone.js"></script>
<script src="https://unpkg.com/reflect-metadata@0.1.3/Reflect.js"></script>
<script src="https://unpkg.com/systemjs@0.19.31/dist/system.js"></script>

<!-- Angular testing dependencies -->
<script src="https://unpkg.com/zone.js/dist/proxy.js?main=browser"></script>
<script src="https://unpkg.com/zone.js/dist/sync-test.js?main=browser"></script>
<script src="https://unpkg.com/zone.js/dist/jasmine-patch.js?main=browser"></script>
<script src="https://unpkg.com/zone.js@0.6.25/dist/async-test.js"></script>
<script src="https://unpkg.com/zone.js/dist/fake-async-test.js?main=browser"></script>

<script src="config.js"></script>
<script>
  //load all dependencies at the same time
  Promise.all([
    //required to test on browser
    System.import('src/setup.spec'),
    //specs
    System.import('src/languagesService.spec'),
    ...
  ]).then(function(modules) {
    //manually trigger Jasmine test-runner
    window.onload();
  }).catch(console.error.bind(console));
</script>

Informationen zu unserem Plunker-Test-Setup:

  • Laden von Jasmine-AbhĂ€ngigkeiten gefolgt von Angular-AbhĂ€ngigkeiten.
  • Wir verwenden ein System.js und TypeScript Setup.
  • Wir laden ĂŒber System.import unsere Tests
  • Mit Promise.all() lösen wir den Jasmine-Test-Runner manuell aus, indem wir onload aufrufen.

Dies sollte uns aber an dieser Stelle nicht weiter interessieren und nur als kurze ErklÀrung dienen.

KurzerklÀrung: Testen in Angular

Testen mit Dependency Injection (DI)

TestBed hilft uns Ă€hnlich wie @NgModule die AbhĂ€ngigkeiten fĂŒr unsere Tests einzurichten. Wir rufen TestBed.configureTestingModule mit unserer Konfiguration auf. Diese Informationen werden dann verwendet, um alle AbhĂ€ngigkeiten fĂŒr unseren Test aufzulöse.

Unten sehen wir ein Beispiel:

@NgModule({
  declarations: [ ComponentToTest ]
  providers: [ MyService ]
})
class AppModule { }
TestBed.configureTestingModule({
  declarations: [ ComponentToTest ],
  providers: [ MyService ]
});
//get instance from TestBed (root injector)
let service = TestBed.get(MyService);

inject erlaubt es uns AbhÀngigkeiten auf der TestBed Ebene zu bekommen.

it('should return ...', inject([MyService], service => {
  service.foo();
}));

Component injector ermöglicht es uns eine AbhÀngigkeit auf der Komponentenebene zu erhalten.

@Component({
  providers: [ MyService ]
})
class ComponentToTest { }
let fixture = TestBed.createComponent(ComponentToTest);
let service = fixture.debugElement.injector.get(MyService);

FĂŒr AbhĂ€ngigkeiten, die auf der Komponenten-Ebene definiert sind, mĂŒssen wir den Component injector wie oben gezeigt benutzen.

Lass uns mal sehen, wie wir TestBed mit der LanguagesService Komponente verwenden wĂŒrden:

describe('Service: LanguagesService', () => {
  beforeEach(() => TestBed.configureTestingModule({
    providers: [ LanguagesService ]
  }));

  it('should return available languages', inject([LanguagesService], service => {
    expect(service.get()).toContain('en');
  }));
})

Zuerst laden wir die fĂŒr unsere Tests benötigten AbhĂ€ngigkeiten mit TestBed.configureTestingModule. Dann benutzen wir inject auf auf unsere Spezifikation, um automatisch jede AbhĂ€ngigkeit zu instanziieren.

Wir können jetzt die Injektion umgestalten, damit wir das nicht fĂŒr jede Spezifikation wiederholen brauchen.

describe('Service: LanguagesService', () => {
  let service;

  beforeEach(() => TestBed.configureTestingModule({
    providers: [ LanguagesService ]
  }));

  beforeEach(inject([LanguagesService], s => {
    service = s;
  }));

  it('should return available languages', () => {
    expect(service.get()).toContain('en');
  });
});

Lass uns mal zwei Beispiele fĂŒr die Instanziierung einer Komponente anschauen. Das erste Beispiel ist synchron und erstellt eine Vorrichtung mit einer Instanz von MyTestComponent.

// synchronous
  beforeEach(() => {
    fixture = TestBed.createComponent(MyTestComponent);
  });

Das zweite Beispiel ist asynchron, da es mit externen Vorlagen und css XHR-Anrufe erfordert.

// asynchronous
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ MyTestComponent ],
    }).compileComponents(); // compile external templates and css
  }));

Wir können async verwenden, wenn AbhĂ€ngigkeiten eine asynchrone Handhabung beinhalten. Dies wird intern eine Zone erstellen und jede asynchrone Verarbeitung „behandeln“.

„AbhĂ€ngig von deinem Build-Setup können deine Vorlagen und CSS alle innenliegend (inlined) sein, so dass sie den synchronen Ansatz sicher verwenden können.“

Testen Checkliste

  • Welche Art von Test? Isolated, shallow oder integration Test.

  • Kann ich Mocks, Stubs oder Spies benutzen? AbhĂ€ngigkeiten sollten durch eigene Tests abgedeckt werden. Mit ihnen kannst du deine Tests boosten, ohne an Wirksamkeit zu verlieren.

  • Sync oder Async? Macht dein Test asynchrone Aufrufe? Benutzt XHR, Promises, Obervables, etc. Benutzt die Komponente TemplateUrl oder styleURls oder inline? Stelle sicher, dass du die entsprechenden APIs verwendest.

Code-Beispiele fĂŒr Testen in Angular

1. Eine Komponente testen

Nehmen wir eine einfache Komponente, die eine BegrĂŒĂŸungsnachricht mit einer @Input() - Eigenschaft rendert.

// Usage:    <greeter name="Joe"></greeter>
// Renders:  <h1>Hello Joe!</h1>
@Component({
  selector: 'greeter',
  template: `<h1>Hello {{name}}!</h1>`
})
export class Greeter {
  @Input() name;
}

Um diese Komponente zu testen, verwenden wir eine gemeinsames Setup mit TestBed.

„Benutze TestBed, um die entsprechenden AbhĂ€ngigkeiten zu laden, damit sie wĂ€hrend deiner Tests verfĂŒgbar sind.“

Es ist eine gĂ€ngige Praxis beforeEach zu verwenden, um unsere Tests umzusetzen. Wenn wir das tun, dann vermeiden wir, dass wir fĂŒr jeden Tests einen Code wiederholen mĂŒssen. Dies vereinfacht auch unsere Spezifikation.

describe('Component: Greeter', () => {
  let fixture, greeter, element, de;

  //setup
  beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [ Greeter ]
    });

    fixture = TestBed.createComponent(Greeter);
    greeter = fixture.componentInstance;  // to access properties and methods
    element = fixture.nativeElement;      // to access DOM element
    de = fixture.debugElement;            // test helper
  });

  //specs
  it('should render `Hello World!`', async(() => {
    greeter.name = 'World';
    //trigger change detection
    fixture.detectChanges();
    fixture.whenStable().then(() => {
      expect(element.querySelector('h1').innerText).toBe('Hello World!');
      expect(de.query(By.css('h1')).nativeElement.innerText).toBe('Hello World!');
    });
  }));
})

Wir haben TestBed.create.Component verwendet, um eine Instanz unserer Greeter-Komponente zu erstellen. Die Komponenteninstanz wird innerhalb einer fixture zugÀnglich sein. Das ist die Haupt-API:

abstract class ComponentFixture {
  debugElement;       // test helper
  componentInstance;  // to access properties and methods
  nativeElement;      // to access DOM element
  detectChanges();    // trigger component change detection
}

Wir haben die name-Eigenschaft benutzt, um einen Wert einzurichten, die Änderungserkennung mit detectChanges einzurichten und das erwartet Ergebnis zu ĂŒberprĂŒfen, wenn alle asynchronen Anrufe mit whenStable beendet wurden. Um auf den gerenderten Text zuzugreifen, verwendeten wir zwei verschiedene APIs, welche durch einen CSS-Selektor (Zeilen 22-23) gefiltert wurden.

„Weitere Abfragen fĂŒr debugElement sind: Query(By.all())query(by.directive(MyDirective))“

2. Einen Service testen

LanguageService hat nur eine Methode, welche ein Array von verfĂŒgbaren Sprachen fĂŒr die Anwendung zurĂŒckgibt.

//a simple service
export class LanguagesService {
  get() {
    return ['en', 'es', 'fr'];
  }
}

Ähnlich wie bei unserem vorherigen Beispiel instanziieren wir den Service vor. Wie wir schon gesagt haben, ist das eine gute Praxis, auch wenn wir nur eine Spezifikation haben. Bei dieser Gelegenheit prĂŒfen wir jede einzelne Sprache und die Gesamtzahl.

describe('Service: LanguagesService', () => {
  let service;

  beforeEach(() => TestBed.configureTestingModule({
    providers: [ LanguagesService ]
  }));

  beforeEach(inject([LanguagesService], s => {
    service = s;
  }));

  it('should return available languages', () => {
    let languages = service.get();
    expect(languages).toContain('en');
    expect(languages).toContain('es');
    expect(languages).toContain('fr');
    expect(languages.length).toEqual(3);
  });
});

3. Testen mit Http

Wir wollen normalerweise keine HTTP-Anrufe wĂ€hrend unserer Tests machen, aber wir werden es jetzt als Referenz trotzdem zeigen. Wir haben unseren Erstdienst LanguageService fĂŒr LanguageServiceHttp.

export class LanguagesServiceHttp {
  constructor(private http:Http) { }

  get(){
    return this.http.get('api/languages.json')
      .map(response => response.json());
  }
}

In diesem Fall verwendet es http.get(), um eine JSON-Datei zu lesen. Danach haben wir Observable.map verwendet, um die Antwort in das Endergebins mit json() umzuwandeln.

Unser Test sieht dem vorherigen ziemlich Àhnlich. Der Hauptunterschied besteht in der Verwendung eines asynchronen Tests, wie wir es mit der Komponente aufgrund des Abonnements gemacht haben.

describe('Service: LanguagesServiceHttp', () => {
  let service;

  //setup
  beforeEach(() => TestBed.configureTestingModule({
    imports: [ HttpModule ],
    providers: [ LanguagesServiceHttp ]
  }));

  beforeEach(inject([LanguagesServiceHttp], s => {
    service = s;
  }));

  //specs
  it('should return available languages', async(() => {
    service.get().subscribe(x => {
      expect(x).toContain('en');
      expect(x).toContain('es');
      expect(x).toContain('fr');
      expect(x.length).toEqual(3);
    });
  }));
})

„Beachte, dass fakeAsync nicht verwendet werden kann, wenn es XHR-Anrufe gibt. Wegen dem Design.“

Http.get() gibt ein Observavble zurĂŒck, welches wir abonnieren können. Wir werden Observables spĂ€ter noch ausfĂŒhrlicher abdecken.

4. Testen mit MockBackend

Ein Ansatz, der sinnvoller erscheint, ist das Ersetzen von HTTP-Anrufen durch einen MockBackend. Um dies zu tun, können wir die Bereitstellung (Zeile 10) verwenden. Dies wid uns ermöglichen, unsere Antworten zu mocken und damit zu vermeiden, das echte Backend zu „hitten“ und damit unsere Testergebnisse zu steigern.

describe('MockBackend: LanguagesServiceHttp', () => {
  let mockbackend, service;

  //setup
  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [ HttpModule ],
      providers: [
        LanguagesServiceHttp,
        { provide: XHRBackend, useClass: MockBackend }
      ]
    })
  });

  beforeEach(inject([LanguagesServiceHttp, XHRBackend], (_service, _mockbackend) => {
    service = _service;
    mockbackend = _mockbackend;
  }));

  //specs
  it('should return mocked response (sync)', () => {
    let response = ["ru", "es"];
    mockbackend.connections.subscribe(connection => {
      connection.mockRespond(new Response(new ResponseOptions({
        body: JSON.stringify(response)
      }));
    });
    service.get().subscribe(languages => {
      expect(languages).toContain('ru');
      expect(languages).toContain('es');
      expect(languages.length).toBe(2);
    });
  });
})

Auf unserem Test bauen wir unsere gemockte Antwort (Zeilen 23-25), also wenn wir endlich den Anruf zu unserem Service machen, bekommt man die erwarteten Ergebnisse.

„Beachte: Wir mĂŒssen async nicht benutzen, weil MockBackend sich synchron verhĂ€lt. Danke an Pascal Precht, der das erwĂ€hnt hat.“

5. Eine Directive testen

Directives in Angular sind eine spezifische Art von Komponente mit in der Regel keiner begleitenden Ansicht. Wir verwenden eine AttributDirective, logClicks, die protokoliieren, wie viele Klicks wir auf dem host element machen, damit du deine Idee erfassen kannst.

// Example: <div log-clicks></div>
@Directive({
  selector: "[log-clicks]"
})
export class logClicks {
  counter = 0;
  @Output() changes = new EventEmitter();

  @HostListener('click', ['$event.target'])
  clicked(target) {
    console.log(`Click on [${target}]: ${++this.counter}`);
    //we use emit as next is marked as deprecated
    this.changes.emit(this.counter);
  }
}

Um diese Directive zu testen, haben wir beschlossen, eine Container-Komponente zu erstellen. Wir werden sie aufstellen, damit sie als unser Host wirken kann, welcher die von unserer Directive emittierten Ereignisse wiedergibt.

@Component({
  selector: 'container',
  template: `<div log-clicks (changes)="changed($event)"></div>`,
  directives: [logClicks]
})
export class Container {
  @Output() changes = new EventEmitter();

  changed(value){
    this.changes.emit(value);
  }
}

describe('Directive: logClicks', () => {
  let fixture;
  let container;
  let element;

  //setup
  beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [ Container, logClicks ]
    });

    fixture = TestBed.createComponent(Container);
    container = fixture.componentInstance; // to access properties and methods
    element = fixture.nativeElement;       // to access DOM element
  });

  //specs
  it('should increment counter', fakeAsync(() => {
    let div = element.querySelector('div');
    //set up subscriber
    container.changes.subscribe(x => {
      expect(x).toBe(1);
    });
    //trigger click on container
    div.click();
    //execute all pending asynchronous calls
    tick();
  }));
})

Wir haben beforeEach verwendet, um die Logik fĂŒr die Erstellung der Komponente aus den Tests zu trennen. Dieser Teil kann nun fĂŒr alle Spezifikationen verwendet werden.

Um den click auf den Container auszulösen, haben wir die DOM API (empfohlen) verwendet. Wir könnten auch fixture.debugElement.triggerEventHandler(‘click’) verwenden.

In diesem Test haben wir fakeAsync und tick verwendet. Mit fakeAsync wird jegliche asynchrone Verarbeitung pausiert, bis wir tick aufrufen. Dies gibt uns eine grĂ¶ĂŸere Kontrolle und vermeidet es außerdem auf verschachtelte Blöcke von Versprechen oder Beobachtungen zurĂŒckzugreifen.

„Mit fakeAsync/tick bekommen wir eine bessere Kontrolle ĂŒber den asynchronen Code, obwohl er nicht mit XHR verwendet werden kann.“

6. Ein Pipe testen

Pipes sind Funktionen, die Eingabedaten in ein vom Benutzer lesbares Format umwandeln können. Wir schreiben eine benutzerdefinierte Pipe capitalise mit dem Standard String.toUpperCase(). Das ist der Einfachheit halber, da Angular eine eigene UpperCasePipe-Implementierung hat.

import {Pipe, PipeTransform} from '@angular/core';
@Pipe({
  name: 'capitalise'
})
export class CapitalisePipe implements PipeTransform {
  transform(value: string): string {
    if (typeof value !== 'string') {
      throw new Error('Requires a String as input');
    }
    return value.toUpperCase();
  }
}

Pipes sind nur einfache Klassen, die injiziert werden können, damit wir unsere Spezifikationen sehr einfach mit inject einrichten können.

describe('Pipe: CapitalisePipe', () => {
  let pipe;

  //setup
  beforeEach(() => TestBed.configureTestingModule({
    providers: [ CapitalisePipe ]
  }));

  beforeEach(inject([CapitalisePipe], p => {
    pipe = p;
  }));

  //specs
  it('should work with empty string', () => {
    expect(pipe.transform('')).toEqual('');
  });

  it('should capitalise', () => {
    expect(pipe.transform('wow')).toEqual('WOW');
  });

  it('should throw with invalid values', () => {
    //must use arrow function for expect to capture exception
    expect(()=>pipe.transform(undefined)).toThrow();
    expect(()=>pipe.transform()).toThrow();
    expect(()=>pipe.transform()).toThrowError('Requires a String as input');
  });
})

Um unsere Pipes zu testen, haben wir die ĂŒblichen FĂ€lle ĂŒberprĂŒft: Sie sollten mit leeren Strings arbeiten, sie sollten kapitalisieren und schließlich sollten sie „werfen“ wenn sie nicht mit einem String benutzt werden.

„Beachte: Wir benutzen eine Pfeilfunktion, um Ausnahmen in expect zu erfassen. “

7. Routes testen

Routen werden manchmal ausgelassen, aber werden fĂŒr gewöhnlich als eine gute Praxis fĂŒr doppelte BuchfĂŒhrung gesehen. In unserem Beispiel verwenden wir eine einfache Routenkonfiguration mit nur wenigen Routes und einer anderen Route, die nach Hause fĂŒhrt.

@Component({
  selector: 'my-app',
  template: `<router-outlet></router-outlet>`
})
class TestComponent { }

@Component({
  selector: 'home',
  template: `<h1>Home</h1>`
})
export class Home { }

export const routes: Routes = [
  { path: '', redirectTo: 'home', pathMatch: 'full' },
  { path: 'home', component: Home },
  { path: '**', redirectTo: 'home' }
];

@NgModule({
  imports: [
    BrowserModule, RouterModule.forRoot(routes),
  ],
  declarations: [TestComponent, Home],
  bootstrap: [TestComponent],
  exports: [TestComponent]
})
export class AppModule {}

Die erste Route-Definition fĂ€ngt die ursprĂŒngliche Route ein, wenn keine zur Umleitung nach Hause geliefert wurde (Zeile 14). Die zweite instanziiert die Home-Komponente (Zeile 15); die letzte fĂ€ngt alle ĂŒbrigen Routes ein und leitet sie auch nach Hause (Zeile 16) um. Unsere Tests werden diese Routes nutzen, um unsere Erwartungen zu prĂŒfen.

describe('Router tests', () => {
  //setup
  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [
        RouterTestingModule.withRoutes(routes),
        AppModule
      ]
    });
  });

  //specs
  it('can navigate to home (async)', async(() => {
    let fixture = TestBed.createComponent(TestComponent);
    TestBed.get(Router)
      .navigate(['/home'])
        .then(() => {
          expect(location.pathname.endsWith('/home')).toBe(true);
        }).catch(e => console.log(e));
  }));

  it('can navigate to home (fakeAsync/tick)', fakeAsync(() => {
    let fixture = TestBed.createComponent(TestComponent);
    TestBed.get(Router).navigate(['/home']);
    fixture.detectChanges();
    //execute all pending asynchronous calls
    tick();
    expect(location.pathname.endsWith('/home')).toBe(true);
  }));

  it('can navigate to home (done)', done => {
    let fixture = TestBed.createComponent(TestComponent);
    TestBed.get(Router)
      .navigate(['/home'])
        .then(() => {
          expect(location.pathname.endsWith('/home')).toBe(true);
          done();
        }).catch(e => console.log(e));
  });
});

Wir haben RouterTestingModule.withRoutes(routes) importiert, um die Routerinstanz mit den Routes fĂŒr unsere Tests zu initialisieren (Zeile 6). Im obigen Code haben wir getestet, dass wir mit async, asyncFake/tick und done nach Hause navigieren können.

„FakeAsync/tick sind ideal fĂŒr komplexe asynchrone Test ohne XHR“

8. Observables testen

Observables sind optimal, um asynchrone Aufgaben zu meistern. Sie werden an wenigen Stellen in Angular, wie Http, Form controls, validations oder hinter EventEmitter verwendet. Wir werden das Observable unten verwenden, um zu zeigen, wie wir ihr Verhalten testen können.

describe('Observable: basic observable', () => {
  var basic$;

  //setup
  beforeEach(() => {
    basic$ = new Observable(observer => {
      //pushing values
      observer.next(1);
      observer.next(2);
      observer.next(3);
      //complete stream
      observer.complete();
    });
  })

  //specs
  it('should create the expected sequence (async)', async(() => {
    let expected = [1,2,3],
      index = 0;
    basic$
      .subscribe({
        next: x => expect(x).toEqual(expected[index++]),
        error: e => console.log(e)
      });
  }));
});

Wir haben eine Observable geschaffen, welche 1,2,3 aussendet und abschließt. Um zu testen, richten wir die nĂ€chsten ein, error und complete die RĂŒckrufe auf Abonnieren. WĂ€hrend der nĂ€chste RĂŒckrufe einige Male aufgerufen wird, mĂŒssen wir unsere Erwartungen dynamisch setzen.

9. EventEmitters testen

EventEmitters werden in Angular verwendet, um Ereignisse zwischen Komponenten zu kommunizieren. Wir haben eine Gegenkomponente namens Counter, die es uns erlaubt, einen Anfangswert von Null zu erhöhen oder vermindern. Jedes Mal, wenn wir das tun, wird der neue Wert mit einem EventEmitter als changes offengelegt.

@Component({
  selector: 'counter',
  template: `
    <div>
      <h1>{{counter}}</h1>
      <button (click)="change(1)">+1</button>
      <button (click)="change(-1)">-1</button>
    </div>`
})
export class Counter {
  @Output() changes = new EventEmitter();

  constructor(){
    this.counter = 0;
  }

  change(increment) {
    this.counter += increment;
    //we use emit as next is marked as deprecated
    this.changes.emit(this.counter);
  }
}

Dieses Setup wird dem von Observables sehr Àhnlich sein.

describe('EventEmitter: Counter', () => {
  let counter;

  //setup
  beforeEach(() => TestBed.configureTestingModule({
    providers: [ Counter ]
  }));

  beforeEach(inject([Counter], c => {
    counter = c;
  }))

  //specs
  it('should increment +1 (async)', async(() => {
    counter.changes.subscribe(x => {
      expect(x).toBe(1);
    });
    counter.change(1);
  }));

  it('should decrement -1 (async)', async(() => {
    counter.changes.subscribe(x => {
      expect(x).toBe(-1);
    });
    counter.change(-1);
  }));
})

In diesem Fall ĂŒberprĂŒfen wir, dass wir mit dem Abonnement auf dem EventEmitter increment oder decrement benutzen, da es ein Observable aussetzt. Wir lösen die verschiedenen Werte aus, indem wir die Änderungsmethode anrufen und unsere Erwartungen im nĂ€chsten RĂŒckruf ĂŒberprĂŒfen.

Alle Tests, die in diesem Beitrag enthalten waren, und noch mehr findest auf Plunker.

Das ist alles! Danke fĂŒrs Lesen! Hast du fragen? Wenn ja dann benachrichtige mich auf @gerardsans.

Willst du mehr?

Wenn du weitere Beispiele benötigst, wende dich bitte an mich bei gerard_dot_sans_at_gmail_dot_com oder schau mal bei Angular Unit Tests in GitHub vorbei!