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.
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.
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.
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
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
.