web-dev-qa-db-fra.com

Angular 2: Comment se moquer de ChangeDetectorRef pendant les tests unitaires

Je viens de commencer avec les tests unitaires et j'ai pu me moquer de mes propres services et de certains Angular et Ionic également, mais peu importe ce que Je ChangeDetectorRef reste le même.

Je veux dire quel genre de sorcellerie est-ce?

beforeEach(async(() => 
    TestBed.configureTestingModule({
      declarations: [MyComponent],
      providers: [
        Form, DomController, ToastController, AlertController,
        PopoverController,

        {provide: Platform, useClass: PlatformMock},
        {
          provide: NavParams,
          useValue: new NavParams({data: new PageData().Data})
        },
        {provide: ChangeDetectorRef, useClass: ChangeDetectorRefMock}

      ],
      imports: [
        FormsModule,
        ReactiveFormsModule,
        IonicModule
      ],
    })
    .overrideComponent(MyComponent, {
      set: {
        providers: [
          {provide: ChangeDetectorRef, useClass: ChangeDetectorRefMock},
        ],
        viewProviders: [
          {provide: ChangeDetectorRef, useClass: ChangeDetectorRefMock},
        ]
      }
    })
    .compileComponents()
    .then(() => {
      let fixture = TestBed.createComponent(MyComponent);
      let cmp = fixture.debugElement.componentInstance;

      let cdRef = fixture.debugElement.injector.get(ChangeDetectorRef);

      console.log(cdRef); // logs ChangeDetectorRefMock
      console.log(cmp.cdRef); // logs ChangeDetectorRef , why ??
    })
  ));

 it('fails no matter what', async(() => {
    spyOn(cdRef, 'markForCheck');
    spyOn(cmp.cdRef, 'markForCheck');

    cmp.ngOnInit();

    expect(cdRef.markForCheck).toHaveBeenCalled();  // fail, why ??
    expect(cmp.cdRef.markForCheck).toHaveBeenCalled(); // success

    console.log(cdRef); // logs ChangeDetectorRefMock
    console.log(cmp.cdRef); // logs ChangeDetectorRef , why ??
  }));

@Component({
  ...
})
export class MyComponent {
 constructor(private cdRef: ChangeDetectorRef){}

 ngOnInit() {
   // do something
   this.cdRef.markForCheck();
 }
}

J'ai tout essayé, async, fakeAsync, injector([ChangeDetectorRef], () => {}).

Rien ne fonctionne.

15
Ankit Singh

Au cas où quelqu'un se heurterait à cela, c'est une façon qui a bien fonctionné pour moi:

Lorsque vous injectez l'instance ChangeDetectorRef dans votre constructeur:

 constructor(private cdRef: ChangeDetectorRef) { }

Vous avez cet cdRef comme l'un des attributs privés du composant, ce qui signifie que vous pouvez espionner le composant, bloquer cet attribut et le faire retourner ce que vous voulez. Vous pouvez également affirmer ses appels et ses paramètres, si nécessaire.

Dans votre fichier de spécifications, appelez votre TestBed sans fournir le ChangeDetectorRef car il ne fournira pas ce que vous lui donnez. Définissez le même composant avant chaque bloc, afin qu'il soit réinitialisé entre les spécifications comme cela se fait dans les documents ici :

component = fixture.componentInstance;

Puis dans les tests, espionnez directement l'attribut

describe('someMethod()', () => {
  it('calls detect changes', () => {
    const spy = spyOn((component as any).cdRef, 'detectChanges');
    component.someMethod();

    expect(spy).toHaveBeenCalled();
  });
});

Avec l'espion, vous pouvez utiliser .and.returnValue() et lui faire retourner tout ce dont vous avez besoin.

Notez que (component as any) Est utilisé car cdRef est un attribut privé. Mais privé n'existe pas dans le javascript compilé, il est donc accessible.

C'est à vous de décider si vous souhaitez accéder aux attributs privés lors de l'exécution de cette manière pour vos tests. Personnellement, je n'ai aucun problème avec cela, je le fais selon mes spécifications pour obtenir plus de couverture.

11
Juan

Un point qui doit probablement être souligné, c'est que vous voulez essentiellement tester votre propre code, pas tester unitaire le détecteur de changement lui-même (qui a été testé par l'équipe Angular). à mon avis, c'est un bon indicateur que vous devez extraire l'appel du détecteur de changement vers une méthode privée locale (privée car c'est quelque chose que vous ne voulez pas tester unitaire), par exemple.

private detectChanges(): void {
    this.cdRef.detectChanges();
}

Ensuite, dans votre test unitaire, vous souhaiterez vérifier que votre code a réellement appelé cette fonction, et donc appelé la méthode du ChangeDetectorRef. Par exemple:

it('should call the change detector',
    () => {
        const spyCDR = spyOn((cmp as any), 'detectChanges' as any);
        cmp.ngOnInit();
        expect(spyCDR).toHaveBeenCalled();
    }
);

J'avais exactement la même situation, et cela m'a été suggéré comme meilleure pratique générale pour les tests unitaires par un développeur senior qui m'a dit que les tests unitaires vous obligent en fait par ce modèle à mieux structurer votre code. Avec la restructuration proposée, vous vous assurez que votre code est flexible à changer, par ex. si Angular change la façon dont ils nous fournissent la détection de changement, alors vous n'aurez qu'à adapter la méthode detectChanges.

1
ArthurT

Je ne sais pas si c'est une nouvelle chose ou non, mais changeDetectorRef est accessible via un appareil.

Voir les documents: https://angular.io/guide/testing#componentfixture-properties

Nous avons rencontré le même problème avec la moquerie du détecteur de changement et cela a fini par être la solution

1
FDIM

Pour les tests unitaires, si vous vous moquez de ChangeDetectorRef juste pour satisfaire l'injection de dépendances pour un composant à créer, vous pouvez passer n'importe quelle valeur.

Pour mon cas, je l'ai fait:

TestBed.configureTestingModule({
  providers: [
    FormBuilder,
    MyComponent,
    { provide: ChangeDetectorRef, useValue: {} }
  ]
}).compileComponents()
injector = getTestBed()
myComponent = injector.get(MyComponent)

Il va créer myComponent avec succès. Assurez-vous simplement que le chemin d'exécution du test n'a pas besoin de ChangeDetectorRef. Si c'est le cas, remplacez useValue: {} avec un objet factice approprié.

Dans mon cas, j'avais juste besoin de tester des trucs de création de formulaire en utilisant FormBuilder.

0
kctang