Angular testlerinde gözlemlenebilir akışları nasıl sahteleyebiliriz
Angular testlerinde sahte bir gözlemlenebilir oluşturulabilir
MockProvider
,
MockInstance
veya
ngMocks.defaultMock
kullanılarak.
Problem
Örneğin, TodoService.list$()
varsa,
bu Observable>
türünde bir değer döndürüyorsa,
ve bir bileşen varsa,
bu bileşen OnInit
içinde subscribe
metodu ile listeyi alıyorsa:
class TodoComponent implements OnInit {
public list: Observable<Array<Todo>>;
constructor(protected service: TodoService) {}
ngOnInit(): void {
// :::warning Asla böyle yapmayın.
// Bu yalnızca gösterim amaçları için.
this.service.list$().subscribe(list => (this.list = list));
}
}
Bileşeni test etmek istiyorsak, bağımlılıklarını sahte nesneleri ile değiştirmek isteriz.
Bizim durumumuzda bu TodoService
dir.
TestBed.configureTestingModule({
declarations: [TodoComponent],
providers: [MockProvider(TodoService)],
});
Eğer bir fixture oluşturursak, undefined
'in özelliklerini okuma hatası ile karşılaşırız. Bu, TodoService.list$
'nin sahte nesnesinin bir spy döndürmesinden kaynaklanır. Eğer otomatik spy
yapılandırılmışsa veya undefined
ise, her ikisinin de subscribe
özelliği yoktur.
Çözüm
Bunu çözmek için, yöntemin bir gözlemlenebilir akış döndürmesini sağlamalıyız.
Bunun için, sahte nesneyi MockProvider
içine ikinci parametre olarak geçerek genişletebiliriz.
TestBed.configureTestingModule({
declarations: [TodoComponent],
providers: [
MockProvider(TodoService, {
list$: () => EMPTY,
}),
],
});
Artık bileşenin başlangıcı hata vermiyor.
Kalıcı düzeltme
Eğer bunu tüm testler için global olarak yapmak istiyorsak, ngMocks.defaultMock
kullanabiliriz.
ngMocks.defaultMock(TodoService, () => ({
list$: () => EMPTY,
}));
Böylece her seferinde testlerin bir TodoService
sahte nesnesine ihtiyacı olduğunda, list$()
EMPTY
döndürür.
Subject
'i sahteleme
EMPTY
gözlemlenebilirleri düzgün bir şekilde bastırmak için çok temel bir sahte nesnedir.
Ancak, Subject
ve onun özel türlerini, yani BehaviorSubject
, ReplaySubject
ve AsyncSubject
'u sahtelemek istiyorsanız,
o zaman EMPTY
, gerekli yöntemlerin eksikliği nedeniyle bunu karşılayamaz:
.next()
, .error()
ve .complete()
.
Örnek olarak, şöyle bir servise sahip olalım:
class TodoService {
subject: Subject<boolean>;
behavior: BehaviorSubject<boolean>;
replay: ReplaySubject<boolean>;
async: AsyncSubject<boolean>;
}
Ve tüm bu özelliklerin testlerimizde EMPTY
olmasını istiyoruz:
ngMocks.defaultMock(TodoService, () => ({
subject: EMPTY,
behavior: EMPTY,
replay: EMPTY,
async: EMPTY,
}));
Ancak bu, kutudan çıktığı gibi çalışmaz ve bir tür hatası verir.
TS2769: No overload matches this call.
The last overload gave the following error.
Argument of type 'typeof TodoService' is not assignable to parameter of type
'AnyDeclaration<{
subject: Observable<never>;
behavior: Observable<never>;
replay: Observable<never>;
async: Observable<never>;
}>[]'.
Bu mantıklıdır, çünkü Observable
gerçekte Subject
vb. değildir.
Bunu düzeltmek için, Subject
, BehaviorSubject
, ReplaySubject
ve AsyncSubject
'un bir sahte nesnesine sahip olmamız gerekir,
ancak bu, EMPTY
gibi davranmalıdır: yalnızca abone olunduğunda tamamlanmalıdır.
ng-mocks
içinde MockService
bulunmaktadır; bu, bir sınıf alabilir ve onun sahte bir örneğini sağlar.
Daha da önemlisi, ikinci parametresi, sahte nesneyi özelleştirmenizi sağlar.
Böylece, Subject
'ı sahteleyebilir ve EMPTY
mantığını şu şekilde uygulayabiliriz:
ngMocks.defaultMock(TodoService, () => ({
subject: MockService(Subject, EMPTY),
behavior: MockService(BehaviorSubject, EMPTY),
replay: MockService(ReplaySubject, EMPTY),
async: MockService(AsyncSubject, EMPTY),
}));
Artık, tüm özellikler abone olunduğunda tamamlanır ve gerekli türleri karşılar.
BehaviorSubject
'ın ilk yayılımını sahteleme
Yukarıda belirtilenlerin devamında, gözlemlenebilirleri bastırmanın yanı sıra,
BehaviorSubject
'ın ilk yayılımını da sahteleme ihtiyacı olabilir.
Bu durumda, EMPTY
yerine istenen değeri ile birlikte of()
kullanılmalıdır:
ngMocks.defaultMock(TodoService, () => ({
behavior: MockService(BehaviorSubject, of(false)),
}));
Kazandık! Artık TodoService.behavior
abone olduğunda false
yayıyor ve aboneliği tamamlıyor.
Gözlemlenebilir akışları özelleştirme
Yine de, genellikle yalnızca EMPTY
gözlemlenebilir akışı döndürmek istemeyiz,
aynı zamanda aramalarını simüle edecek sahte bir konuyu da sağlamamız gerekir.
Olası bir çözüm, it
bağlamında MockInstance
kullanmaktır:
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [TodoComponent],
providers: [MockProvider(TodoService)],
});
});
it('test', () => {
const fakeList$ = new Subject(); // <- konu oluştur.
const list$ = jasmine.createSpy().and.returnValue(fakeList$);
MockInstance(TodoService, () => ({
list$,
}));
const fixture = TestBed.createComponent(TodoComponent);
fixture.detectChanges();
// Burada bazı doğrulamalar yapabiliriz.
expect(list$).toHaveBeenCalledTimes(1);
// Yayılımları simüle edelim.
fakeList$.next([]);
});
MockBuilder
için bir çözüm de oldukça benzer.
let todoServiceList$: Subject<any>; // <- bir bağlam değişkeni.
beforeEach(() => {
todoServiceList$ = new Subject(); // <- konu oluştur.
return MockBuilder(TodoComponent, ItsModule)
// TodoService, ItsModule içindeki modülde sağlanıyor
.mock(TodoService, {
list$: () => todoServiceList$,
});
});
it('test', () => {
const fixture = MockRender(TodoComponent);
todoServiceList$.next([]);
// bazı doğrulamalar.
});
Bunların hepsi MockInstance
ile de uygulanabilir,
ancak bu konu dışına çıkmaktadır.
Gelişmiş örnek
Angular testlerinde gözlemlenebilirleri sahteleme üzerine gelişmiş bir örnek. Lütfen koddaki yorumlara dikkat edin.
describe('MockObservable', () => {
// Bileşeni test etmek için, onu ilk parametre olarak veriyoruz
// MockBuilder'a. Sahte bağımlılıklarını oluşturmak için
// modülünü ikinci parametre olarak veriyoruz.
// MockBuilder'ın promise'ını döndürmeyi unutmayın.
beforeEach(() => MockBuilder(TargetComponent, TargetModule));
// Şimdi, servisin sahte nesnesini özelleştirmemiz gerekiyor.
// value$ akışa erişim noktamızdır.
const value$: Subject<number[]> = new Subject();
beforeAll(() => {
// MockInstance, sahte nesne örneklerini geçersiz kılmamıza yardımcı olur.
MockInstance(TargetService, instance =>
ngMocks.stub(instance, {
value$, // bu bir salt okunur özellik olsa bile geçersiz kılabiliriz.
}),
);
});
// Testlerden sonra temizleme.
afterAll(() => {
value$.complete();
MockInstance(TargetService);
});
it('içerilen bir konunun yayılımına dinler', () => {
// Bileşeni render edelim.
const fixture = MockRender(TargetComponent);
// Henüz hiçbir şey yaymadık, şablonu kontrol edelim.
expect(fixture.nativeElement.innerHTML).not.toContain('1');
expect(fixture.nativeElement.innerHTML).not.toContain('2');
expect(fixture.nativeElement.innerHTML).not.toContain('3');
// Bir yayılım simüle edelim.
value$.next([1, 2, 3]);
fixture.detectChanges();
// Şablon, yayılmış sayıları içermelidir.
expect(fixture.nativeElement.innerHTML).toContain('1');
expect(fixture.nativeElement.innerHTML).toContain('2');
expect(fixture.nativeElement.innerHTML).toContain('3');
// Bir yayılım daha simüle edelim.
value$.next([]);
fixture.detectChanges();
// Sayılar kaybolmalıdır.
expect(fixture.nativeElement.innerHTML).not.toContain('1');
expect(fixture.nativeElement.innerHTML).not.toContain('2');
expect(fixture.nativeElement.innerHTML).not.toContain('3');
// Kardeş yöntemin de bir sahte nesne ile değiştirildiğini kontrol ediyoruz.
expect(
fixture.point.injector.get(TargetService).getValue$,
).toBeDefined();
expect(
fixture.point.injector.get(TargetService).getValue$(),
).toBeUndefined();
});
});