86 votes

Test unitaire Angular2 avec @Input()

J'ai un composant qui utilise le @Input() sur une variable d'instance et j'essaie d'écrire mon test unitaire pour l'annotation openProductPage() mais je suis un peu perdu sur la façon de configurer mon test unitaire. I pourrait rendre cette variable d'instance publique, mais je ne pense pas que je doive avoir recours à cela.

Comment puis-je configurer mon test Jasmine de manière à ce qu'un produit fictif soit injecté (fourni ?) et que je puisse tester la fonction openProductPage() méthode ?

Mon composant :

import {Component, Input} from "angular2/core";
import {Router} from "angular2/router";

import {Product} from "../models/Product";

@Component({
    selector: "product-thumbnail",
    templateUrl: "app/components/product-thumbnail/product-thumbnail.html"
})

export class ProductThumbnail {
    @Input() private product: Product;

    constructor(private router: Router) {
    }

    public openProductPage() {
        let id: string = this.product.id;
        this.router.navigate([“ProductPage”, {id: id}]);
    }
}

2 votes

J'ai écrit un court blog sur le test des composants avec @Input() qui explique quelques façons de tester l'entrée que vous voulez : medium.com/@AikoPath/

72voto

Vazgen Manukyan Points 588

Ceci est tiré de la documentation officielle https://angular.io/docs/ts/latest/guide/testing.html#!#component-fixe . Vous pouvez donc créer un nouvel objet d'entrée expectedHero et le passer au composant comp.hero = expectedHero

Assurez-vous également d'appeler fixture.detectChanges(); dernier, sinon la propriété ne sera pas liée au composant.

Exemple de travail

// async beforeEach
beforeEach( async(() => {
    TestBed.configureTestingModule({
        declarations: [ DashboardHeroComponent ],
    })
    .compileComponents(); // compile template and css
}));

// synchronous beforeEach
beforeEach(() => {
    fixture = TestBed.createComponent(DashboardHeroComponent);
    comp    = fixture.componentInstance;
    heroEl  = fixture.debugElement.query(By.css('.hero')); // find hero element

    // pretend that it was wired to something that supplied a hero
    expectedHero = new Hero(42, 'Test Name');
    comp.hero = expectedHero;
    fixture.detectChanges(); // trigger initial data binding
});

7 votes

Où est utilisé l'élément héros

0 votes

Aniruddha Das - il sera utilisé si vous vous liez à des propriétés du héros dans le html. J'ai eu exactement le même problème et cette solution est simple à mettre en œuvre, et vous obtenez de créer un objet fantaisie ici même dans le test. Cela devrait être la réponse acceptée.

3 votes

Utiliser before each pour définir les données qui doivent être dynamiques pour chaque test semble être un très mauvais modèle pour écrire des tests qui doivent tester plus d'un cas spécifique.

54voto

Si vous utilisez TestBed.configureTestingModule pour compiler votre composant de test, voici une autre approche. C'est fondamentalement la même chose que la réponse acceptée, mais peut être plus similaire à la façon dont angular-cli génère les spécifications. POUR INFO.

import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { DebugElement } from '@angular/core';

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

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ 
        TestComponentWrapper,
        ProductThumbnail
      ],
      schemas: [CUSTOM_ELEMENTS_SCHEMA]
    })
    .compileComponents();

    fixture = TestBed.createComponent(TestComponentWrapper);
    component = fixture.debugElement.children[0].componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });
});

@Component({
  selector: 'test-component-wrapper',
  template: '<product-thumbnail [product]="product"></product-thumbnail>'
})
class TestComponentWrapper {
  product = new Product()
}

0 votes

J'essaye ce que vous suggérez ci-dessus mais quand je le fais, j'obtiens un "Uncaught ReferenceError : Zone is not defined" . J'utilise un clone virtuel du code que vous avez montré ci-dessus. (avec l'ajout de mes propres inclusions comme ci-dessous) : import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { testContentNavData } from './mok-definitions'; import { ContentNavComponent } from '../app/content-nav/content-nav.component'; import {} from 'jasmine';

0 votes

Cela ressemble à une erreur de Zone.js, donc c'est difficile à dire. Utilisez-vous Angular CLI ? Vous pouvez peut-être fournir un lien vers l'erreur complète enregistrée dans votre console.

0 votes

J'ai suivi votre approche mais mon composant testé a le template '<p [outerHTML]="customFieldFormatted"></p>' et il ne passe jamais les tests. Tout fonctionne bien, le composant est rendu correctement mais le html n'est pas ajouté. Si je change pour <p>{{ customFieldFormatted }}</p> tout fonctionne bien. Je ne sais pas pourquoi [outerHTML] ne fonctionne pas. Avez-vous une idée ? Merci

30voto

thierry templier Points 998

Vous devez définir le product sur l'instance du composant après qu'il ait été chargé dans votre test.

À titre d'exemple, voici un composant simple dans une entrée que vous pouvez utiliser comme base pour votre cas d'utilisation :

@Component({
  selector: 'dropdown',
  directives: [NgClass],
  template: `
    <div [ngClass]="{open: open}">
    </div>
  `,
})
export class DropdownComponent {
  @Input('open') open: boolean = false;

  ngOnChanges() {
    console.log(this.open);
  }
}

Et le test correspondant :

it('should open', injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => {
  return tcb.createAsync(DropdownComponent)
  .then(fixture => {
    let el = fixture.nativeElement;
    let comp: DropdownComponent = fixture.componentInstance;

    expect(el.className).toEqual('');

    // Update the input
    comp.open = true; // <-----------

    // Apply
    fixture.detectChanges(); // <-----------

    var div = fixture.nativeElement.querySelector('div');
    // Test elements that depend on the input
    expect(div.className).toEqual('open');
  });
}));

Voir ce plunkr à titre d'exemple : https://plnkr.co/edit/YAVD4s?p=preview .

3 votes

Dans l'exemple de l'OP, le @Input la propriété en cours de définition est privée. Sauf erreur de ma part, cette approche ne fonctionnera pas dans ce cas, car tsc va vomir sur la référence à un champ privé.

2 votes

Merci de nous le signaler ! Je n'avais pas remarqué que le champ était privé. J'ai repensé à votre commentaire et à l'aspect "privé". Je me demande si c'est une bonne chose d'avoir le champ private sur ce champ puisqu'il n'est pas réellement "privé"... Je veux dire qu'il sera mis à jour depuis l'extérieur de la classe par Angular2. Je serais intéressé d'avoir votre avis ;-)

2 votes

Vous posez une question intéressante, mais je pense que la vraie question à se poser est de savoir si c'est une bonne chose d'avoir private dans Typescript, puisqu'elle n'est pas "réellement privée", c'est-à-dire qu'elle ne peut pas être appliquée au moment de l'exécution, mais seulement au moment de la compilation. Personnellement, je l'apprécie, mais je comprends aussi les arguments contre. Mais en fin de compte, Microsoft a choisi de l'intégrer dans TS, et Angular a choisi TS comme langage principal, et je ne pense pas que l'on puisse dire que c'est une mauvaise idée d'utiliser une fonctionnalité majeure d'un langage principal.

18voto

drewmoore Points 5171

Je fais habituellement quelque chose comme :

describe('ProductThumbnail', ()=> {
  it('should work',
    injectAsync([ TestComponentBuilder ], (tcb: TestComponentBuilder) => {
      return tcb.createAsync(TestCmpWrapper).then(rootCmp => {
        let cmpInstance: ProductThumbnail =  
               <ProductThumbnail>rootCmp.debugElement.children[ 0 ].componentInstance;

        expect(cmpInstance.openProductPage()).toBe(/* whatever */)
      });
  }));
}

@Component({
 selector  : 'test-cmp',
 template  : '<product-thumbnail [product]="mockProduct"></product-thumbnail>',
 directives: [ ProductThumbnail ]
})
class TestCmpWrapper { 
    mockProduct = new Product(); //mock your input 
}

Notez que product et tout autre champ de l ProductThumbnail classe peut être privé avec cette approche (c'est la raison principale pour laquelle je la préfère à celle de Thierry, malgré le fait qu'elle soit un peu plus verbeuse).

0 votes

Avez-vous encore besoin d'injecter TestComponentBuilder ? voir : medium.com/@AikoPath/

0 votes

Pour les développeurs qui recherchent l'approche du "pur banc d'essai", il y a quelques réponses dans ce billet : stackoverflow.com/a/36655501/301603 y stackoverflow.com/a/43755910/301603 Cette réponse particulière n'est pas fausse, mais il s'agit plus d'un "hack" que d'une véritable approche de test unitaire.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X