Angular @ WPS Teil 3: Fokussiert Komponenten testen

Angular @ WPS Teil 3: Fokussiert Komponenten testen

Das ist Teil 3 unserer Reihe Entwicklung von Reale-Welt-Anwendungen ohne Orientierungsverlust

von Richard Voß @richard_voss

Seit (vor-)letztem Jahr entwicklen wir bei WPS immer öfter Anwendungen für unsere Kunden mit Hilfe von Angular 4, 5, 6 und auch 7. Angular ist ein populäres Framework (es wird auch als Plattform bezeichnet) zur Entwicklung von web-basierten Frond-Ends mit Typescript, HTML und CSS. Es ist Open Source Software und wird hauptsächlich durch Google vorangetrieben.

Das Framework macht es wesentlich einfacher, komplexe GUIs zu strukturieren, fördert die Wiederverwendung von Komponenten und – am wertvollsten – erlaubt es tatsächlich, testgetriebene Front-End-Entwicklung zu betreiben, die Spaß macht.

Trotz all dieser Großartigkeit des Frameworks bleiben genug Fehler übrig, die man machen kann. Wir haben einige erfolgreiche Muster aber auch Fallstricke gefunden und gesammelt.

Dieser Artikel gibt einen weiteren Einblick in unsere „Angular Pattern Library“. Beim letzten Mal ging es um die visuelle Schnittstelle von Komponenten, diesmal um die Frage, wie wir Komponenten richtig testen können.

👍 Mock Components

Unit Testing für Angular-Komponenten ist auch Unit Testing.

Komponente FooComponent verwendet den Service FooService und die Komponente HeadlineAndTextComponente, die wiederum den PhoneService verwendet.

 

Ausgehend von diesem Bild: Will man eine Komponente testen, heißt das man will ihre Komponenten-Klasse und das Template testen. (Das Stylesheet ist üblicherweise nicht Gegenstand von Unit Tests, aber das ist ein Thema für sich.) Allerdings gibt es dort ein paar Dinge, die uns beim Unit Test nicht wichtig sind.

Das Angular Tutorial behandelt das Problem nur sehr kurz: Wenn eine Komponente FooComponent eine andere Komponente HeadlineAndTextComponent verwendet, dann muss diese HeadlineAndTextComponent Teil des TestingModule für die Komponente FooComponent sein, sonst kann im Test das Template (von FooComponent) nicht kompiliert werden.

Aber weil HeadlineAndTextComponent seine eigenen Unit Tests hat, wollen wir sie eigentlich im Testaufbau für FooComponent nicht wirklich aufnehmen. Das gleiche gilt für den FooService, dessen eigenes Verhalten auch dediziert getestet werden sollte. Genau so würden wir schließlich auch Abhängigkeiten normaler Klassen in Typescript oder Java ausblenden, damit wir Funktionen nicht doppelt testen. Und selbst wenn man nicht immer alles mocken will, man sollte es können.

FooComponten verwendet nur noch Mocks des FooService und von HeadlineAndTextComponent, der PhoneService entfällt.

Dieses zweite Bild zeigt, dass direkte Abhängigkeiten im Testaufbau vorhanden sein müssen, aber Mocks völlig ausreichen. Der PhoneService wird gänzlich irrelevant, weil er eine transitive Abhängigkeit ist.

Wie kann Ich Komponenten mocken?

Die Lösung aus dem Tutorial erfordert eine Menge Code und die Duplizierung der Schnittstelle der eigentlichen Komponente (Input/Output). An dieser Stelle erscheint ng-mocks als der Held der Stunde. Etwas länger gibt es bereits ng2-mock-component, das weniger mächtig ist, uns aber zu Beginn geholfen hat.

Das folgende Beispiel zeigt, wie mit diesen Werkzeugen das Szenario von oben umgesetzt würde:

describe('FooComponent', () => {
  let fixture: ComponentFixture<FooComponent>;
  let component: FooComponent;

  let fooService: FooService;

  beforeEach(async(() => {
    fooService = mock(FooService);

    when(fooService.getHeadline()).thenReturn('MOCK!');

    TestBed.configureTestingModule({
      declarations: [
        FooComponent, MockComponent(HeadlineAndTextComponent)
      ],
      providers: [
        { provide: FooService, useValue: instance(fooService) }
      ]
    }).compileComponents();
  }));
  beforeEach(() => {
    fixture = TestBed.createComponent(FooComponent);
    component = fixture.debugElement.componentInstance;
    expect(component).toBeTruthy();
    fixture.detectChanges();
  });

  it('passes down the correct headline', () => {
    const subComponent: HeadlineAndTextComponent = fixture.debugElement.query(By.css('headline-and-text')).componentInstance;
    expect(subComponent.headline).toBe('MOCK!');
  });
});

Das ist gar nicht viel Code aber er erreicht eine ganze Menge:

  1. Er garantiert, dass HeadlineAndTextComponent nicht wirklich instanziiert wird, also sind alle transitiven Abhängigkeiten (Services, Komponenten, Pipes, …) tatsächlich weg.
  2. Er erzeugt aber trotzdem für das TestingModule eine Komponente, die aussieht wie die Echte: Selector, Inputs und Outputs passen. Jeder Tippfehler im Template würde den Test scheitern lassen.
  3. Der Test deckt die korrekte Verknüpfung der Variable headline mit der Subkomponente ab, also die tatsächliche Anwendungsfunktionalität wird trotzdem korrekt verifiziert.
  4. Auch verifiziert der Test, dass der Text der Überschrift (MOCK!) tatsächlich aus dem Service stammt, obwohl auch dieser nur simuliert ist.

Den Typescript-Teil (die Klasse) einer Komponente zu testen ist leicht, aber die richtige Verknüpfung von Inputs und Outputs im Template ist schwer zu testen. Mit gemockten Komponenten wird es aber bereits deutlich einfacher. Einfach mal ausprobieren!

Nebenbei: ng-mocks kann das alles auch für Pipes und Direktiven, also gilt all dies auch für sie.

Wann sollte Ich eine Komponte nicht mocken?

Es gibt eine allgemeine Diskussion über das Für und Wider des Mocking und ob es nicht manchmal wesentlich aussagekräftiger ist, wenn in einem Test einige Interaktionen zwischen den echten Komponenten stattfinden. Diese Frage stellt sich nicht nur für Javascript/Typescript oder Angular-Entwicklung.

Wir finden es in den folgenden Fällen sinnvoll und richtig, eine Komponente nicht zu mocken, sondern sie im Testaufbau mit einzubinden:

  • Wenn zwei Komponenten absichtlich eng gekoppelt sind und die innere Komponente außerhalb dieses Kontext nicht verwendet werden soll:
    • Das ist oft der Fall bei Problemen vom Typ Liste und Eintrag, bei denen Verhalten und visuelle Eigenschaften die enge Kopplung erfordern.
    • Diese Komponenten sollten in dem Fall auch außerhalb des Tests zu einem Modul gehören, vielleicht sogar ein Small Module (Dieses Muster thematisieren wir hier später noch einmal.)
    • Die innere Komponente hat in diesem Fall wahrscheinlich keine eigenen Tests.
  • Wenn es sich um eine Komponente von Dritten handelt (aus einer Library wie Angular Material) und die Tests absichern sollen, dass sie korrekt verwendet wird.
    • Unit Tests sind eine hervorragende Methode, um herauszufinden, wie man eine fremde Komponente richtig benutzt – und es dabei auch noch zu dokumentieren,
    • und sie bieten gleichzeitig Regressionssicherheit, wenn man die externe Bibliothek mal aktualisiert.

Und was ist mit Services?

Services sind einfach, denn sie sind nur Objekte, die ein Interface implementieren. Sie zu mocken ist nichts besonderes in Angular. Wir verwenden ts-mockito and das war’s. Im Beispiel oben sieht man das bereits beim FooService.

Services zu mocken ist genau so sinnvoll wie für Komponenten, weil es den Fokus des Unit Test verbessert und so das indirekte durch-den-Tunnel-Testen verhindert.

und weiter?

Bis hier ging es in unserer Reihe um gute Ideen und absolte Empfehlungen, beim nächsten Mal berichte ich von einer unangenehmen Erfahrung und dem abgeleiteten Anti-Muster: 👎 Avoid Component Class Inheritance with Template Duplication.

2019-02-25T10:50:35+02:0025. Februar 2019|