Ana içeriğe geç

MockBuilder - Angular testlerinde mock oluşturmaya en basit yol

MockBuilder mock oluşturmanın en basit yoludur.
Testlerin gerektirdiği şekilde mock'ları manipüle etmek için zengin bir fonksiyon seti sunar, ancak minimum overhead ile.

Genellikle test edilmesi gereken basit bir şeyimiz vardır, ama zaman zaman, basitlik korkunç bağımlılıklar tarafından ortadan kaldırılır.
Burada iyi olan şey, genellikle bağımlılıkların test ettiğimiz şeyin bulunduğu modülde beyan edilmiş veya içe aktarılmış olmasıdır.
Bu nedenle, MockBuilder yardımıyla, test modülünü oldukça kolay bir şekilde tanımlayabiliriz.
Burada modüldeki her şey mock'larıyla değiştirilir, test edilen şey hariç:

beforeEach(() => {
return MockBuilder(TheThing, ItsModule);
});

// Bir bileşeni test etmek için
beforeEach(() => {
return MockBuilder(TheComponent, ItsModule);
});

// Bir direktifi test etmek için
beforeEach(() => {
return MockBuilder(TheDirective, ItsModule);
});

// Bir pipe'ı test etmek için
beforeEach(() => {
return MockBuilder(ThePipe, ItsModule);
});

// Bağımsız deklarasyonları test etmek için
beforeEach(() => {
return MockBuilder(TheStandaloneDeclaration).mock(OneOfItsImports);
});

MockBuilder, Angular bağımlılıklarını mock'larına dönüştürmek için basit bir araç sağlar, bunu izole alanlarda yapar, ve şu alanları destekleyen zengin bir araç setine sahiptir:

  • kök sağlayıcılar için mock'ların tespiti ve oluşturulması
  • her derinlikte modüllerin ve deklarasyonların değiştirilmesi
  • her derinlikte modüllerin, deklarasyonların ve sağlayıcıların hariç tutulması
  • bağımsız deklarasyonların yüzeysel test edilmesi

Basit örnek

MockBuilder yardımıyla Angular testlerinde mock oluşturmanın kolaylığını gösteren bir kod örneği.
Lütfen, kodun içindeki yorumlara dikkat edin.

describe('MockBuilder:simple', () => {
// MockBuilder'ın promisesini döndürmeyi unutmayın.
beforeEach(() => MockBuilder(MyComponent, MyModule));
// Bununla aynı
// beforeEach(() => TestBed.configureTestingModule({{
// imports: [MockModule(MyModule)],
// }).compileComponents());
// ama MyComponent, test amaçları için mock nesnesi ile değiştirilmemiştir.

it('bütün bağımlılıkları göz ardı ederek içeriği render etmelidir', () => {
const fixture = MockRender(MyComponent);
expect(fixture).toBeDefined();
expect(fixture.nativeElement.innerHTML).toContain(
'<div>My Content</div>',
);
});
});

Esnek mod

ipucu

TestBed'i istediğiniz şekilde oluşturmak için esnek modu kullanabilirsiniz.

Diyelim ki TargetComponent'i test etmek istiyorsunuz ve 3 bağımlılığı var:

  • CurrencyPipe bir mock olmalı
  • TimeService bir mock olmalı
  • ReactiveFormModule olduğu gibi kalmalı

Bu durumda, MockBuilder şu şekilde çağrılabilir:

beforeEach(() => {
return MockBuilder()
// TestBed'de olduğu gibi tanımlanır.
.keep(TargetComponent)

// TestBed'de bir mock olarak tanımlanır.
.mock(CurrencyPipe)

// TestBed'de bir mock olarak sağlanır.
.mock(TimeService)

// TestBed'de olduğu gibi içe aktarılır.
.keep(ReactiveFormModule);
});

Bu yaklaşım iyi, ancak sorun, bağımlılıkların açıkça sağlanmış olmasıdır.
Eğer birisi TargetComponent'in tanımlandığı modülden ReactiveFormModule'u kaldırmışsa, test başarısız olmayacak, oysa uygulama olacak.

tehlike

İşte burada katı mod öne çıkar.

Katı mod

Katı mod, MockBuilder'a 2 parametre geçerseniz etkinleştirilir:

  • ilk parametre, test için sağlanması ve olduğu gibi korunması gereken şeydir
  • ikinci parametre, test için sağlanması ve sahte olması gereken şeydir
  • zincir çağrıları yalnızca bu deklarasyonları özelleştirir

Esnek mod örneğini dikkate alırsak, katı modu etkinleştirmek için kod şöyle görünmelidir:

beforeEach(() => {
// TargetComponent, TargetModule'dan olduğu gibi dışarı aktarılır
// TargetModule'ın tüm içe aktarımlarının ve deklarasyonlarının mock'lanması gerekir
return MockBuilder(TargetComponent, TargetModule)

// ReactiveFormModule'un, TargetModule'da olduğu gibi kalmasını işaretler
// ve TargetModule veya içe aktarımlarının bunu içermemesi durumunda bir hata fırlatır.
.keep(ReactiveFormModule);
});

TargetComponent'in tüm bağımlılıkları TargetModule'dadır ve bunlardan herhangi biri silinirse, testler başarısız olacaktır.

Ayrıca, eğer birisi TargetModule'dan ReactiveFormModule'u silerse, testler de başarısız olacaktır, çünkü MockBuilder, korunması gereken ReactiveFormModule'un eksik olduğuna dair bir hata fırlatacaktır.

Peki, birden fazla modül gerekiyorsa? Örneğin, tembel modüller için.
Tembel olarak yüklenen modüller durumunda, TestBed'de birden fazla modül içe aktarmanız gerekir.
Genellikle, kök deklarasyonları sağlayan bir AppModule ve belirli bir yola ait olan bir LazyModule vardır.
Bunu yapmak için, MockBuilder'ın parametreleri olarak dizileri geçin:

beforeEach(() => {
return MockBuilder(
// Birden fazla şeyi korumak ve dışa aktarmak istiyorsanız bir dizi de olabilir
TargetComponent,

[
// TargetModule'u TestBed'de mock'layacak ve içe aktaracak
TargetModule,
// AppModule'ü TestBed'de mock'layacak ve içe aktaracak
AppModule,
],
)

// CurrencyPipe'ı olduğu gibi tutacak,
// ve ne TargetModule ne de AppModule'un bunu tanımlayıp içe aktarmaması durumunda hata verecek.
.keep(CurrencyPipe);
});

Katı mod önerilen yaklaşımdır.

Zincir fonksiyonları

.keep()

Bir modülü, bileşeni, direktifi, pipe'ı veya sağlayıcıyı olduğu gibi tutmak istiyorsak .keep kullanmalıyız.

beforeEach(() => {
return MockBuilder(MyComponent)
.keep(SomeModule)
.keep(SomeModule.forSome())
.keep(SomeModule.forAnother())
.keep(SomeComponent)
.keep(SomeDirective)
.keep(SomePipe)
.keep(SomeService)
.keep(SomeInjectionToken);
});

.mock()

Herhangi bir şeyi (hatta bir korunmuş modülün bir parçasını) bir mock nesnesine dönüştürmek istiyorsak .mock kullanmalıyız.

beforeEach(() => {
return MockBuilder(MyComponent)
.mock(SomeModule)
.mock(SomeModule.forSome())
.mock(SomeModule.forAnother())
.mock(SomeComponent)
.mock(SomeDirective)
.mock(SomePipe)
.mock(SomeService)
.mock(SomeInjectionToken);
});

Pipe'lar için, 2. parametre olarak handler'larını belirtebiliriz.

beforeEach(() => {
return MockBuilder(MyComponent)
.mock(SomePipe, value => 'My Custom Content');
});

Hizmetler ve token'lar için isteğe bağlı olarak stub'larını sağlayabiliriz.
Lütfen unutmayın ki hizmetin mock nesnesi sağlanan değer ile genişletilecektir.

beforeEach(() => {
return MockBuilder(MyComponent)
.mock(SomeService3, anything1)
.mock(SOME_TOKEN, anything2);
});

.exclude()

Bir şeyi (hatta bir korunmuş modülün bir parçasını) hariç tutmak istiyorsak .exclude kullanmalıyız.

beforeEach(() => {
return MockBuilder(MyComponent, MyModule)
.exclude(SomeModule)
.exclude(SomeComponent)
.exclude(SomeDirective)
.exclude(SomePipe)
.exclude(SomeDependency)
.exclude(SomeInjectionToken);
});

.replace()

Bir şeyi başka bir şeyle değiştirmek istiyorsak .replace kullanmalıyız.
Değiştirilen şey, kaynak ile aynı dekoratör ile işaretlenmelidir.
Bir sağlayıcı/hizmeti değiştirmek imkansız değildir, bunun için .provide veya .mock kullanmalıyız.

beforeEach(() => {
return MockBuilder(MyComponent, MyModule)
.replace(SomeModule, SomeOtherModule)
.replace(SomeComponent, SomeOtherComponent)
.replace(SomeDirective, SomeOtherDirective)
.replace(SomePipe, SomeOtherPipe);
});

HttpClientTestingModule durumunda, .replace kullanabiliriz.

beforeEach(() => {
return MockBuilder(MyComponent, MyModule)
.replace(HttpClientModule, HttpClientTestingModule);
});

RouterTestingModule durumunda, her iki modül için de .keep ve NG_MOCKS_ROOT_PROVIDERS kullanmamız ve .withRoutes içine boş bir dizi geçmemiz gerekiyor.
NG_MOCKS_ROOT_PROVIDERS gerekli, çünkü RouterModule'un da korunması gereken birçok kök bağımlılığı vardır.

beforeEach(() => {
return MockBuilder(MyComponent)
.keep(RouterModule)
.keep(RouterTestingModule.withRoutes([]))
.keep(NG_MOCKS_ROOT_PROVIDERS);
});

.provide()

Sağlayıcıları/hizmetleri eklemek veya değiştirmek istiyorsak .provide kullanmalıyız.
Aynı arayüze sahiptir.

beforeEach(() => {
return MockBuilder(MyComponent, MyModule)
.provide(MyService)
.provide([SomeService1, SomeService2])
.provide({ provide: SomeComponent3, useValue: anything1 })
.provide({ provide: SOME_TOKEN, useFactory: () => anything2 });
});

Config

Mock nesnelerinin varsayılan davranışını özelleştirebilirsiniz.
Ayrıca, tekrarları önlemek için ngMocks.defaultConfig() aracılığıyla global olarak da yapılabilir.

precise bayrağı

Varsayılan olarak, .mock(Service, mock) kullanıldığında, MockService(Service, mock) aracılığıyla bir mock nesnesi oluşturur.
Bazı durumlarda, uzantı yerine tam olarak iletilen mock nesnesini kullanmak isteyebiliriz.
Bu davranış için precise bayrağını true yapmamız gerekmektedir. Token'lar her zaman kesindir.

declare class MyService {
p1: boolean;
getP1(): boolean;
}
const mock = {
p1: true,
};

beforeEach(() => {
return (
MockBuilder(MyComponent, MyModule)
// instance !== mock ama instance.p1 === mock.p1
// instance.getP1() undefined döner
.mock(MyService, mock)
// instance === mock, bu yüzden instance.p1 === mock.p1
// ve instance.getP1 mevcut değildir.
.mock(MyService, mock, {
precise: true,
})
);
});

export bayrağı

Eğer test etmek istediğimiz bir bileşen, direktif veya pipe maalesef dışa aktarılmadıysa, onu export bayrağı ile işaretlememiz gerekir.
Ne kadar derin olursa olsun. MyModule düzeyine dışa aktarılacaktır.

beforeEach(() => {
return MockBuilder(MyComponent, MyModule)
.keep(SomeDeclaration1, {
export: true,
})
.mock(SomeDeclaration2, {
export: true,
});
});

exportAll bayrağı

Eğer dışa aktarılmamış bir modülün tüm deklarasyonlarını kullanmak istiyorsak, modülü exportAll bayrağı ile işaretlememiz gerekir.
O zaman tüm ithalatları ve deklarasyonları dışa aktarılacaktır.
Modül iç içe geçmişse, exportAll dışında export bayrağını da ekleyin.

beforeEach(() => {
return MockBuilder(MyComponent)
.keep(MyModule, {
exportAll: true,
})
.mock(MyNestedModule, {
exportAll: true,
export: true,
});
});

dependency bayrağı

Varsayılan olarak, tüm tanımlar, başka bir tanımın bağımlılığı değilse MyModule'a eklenir.
Modüller MyModule'a ithalat olarak eklenir.
Bileşenler, Direktifler, Pipe'lar MyModule'a deklarasyon olarak eklenir.
Token'lar ve Hizmetler MyModule'a sağlayıcılar olarak eklenir.
Eğer bir şeyin MyModule'a eklenmesini istemiyorsak, onu dependency bayrağı ile işaretlemeliyiz.

beforeEach(() => {
return (
MockBuilder(MyComponent)
.mock(MyModule)
.keep(SomeModuleComponentDirectivePipeProvider1, {
dependency: true,
})
.mock(SomeModuleComponentDirectivePipe, {
dependency: true,
})
// Sadece yapılandırmayı belirtmek istiyorsak,
// aynı tanımı bir mock örneği olarak geçin.
.mock(SomeProvider, SomeProvider, {
dependency: true,
})
// Veya yapılandırma ile birlikte bir mock örneği sağlayın.
.mock(SomeProvider, mockInstance, {
dependency: true,
})
.replace(SomeModuleComponentDirectivePipeProvider1, anything1, {
dependency: true,
})
);
});

shallow bayrağı

Shallow bayrağı, korunan bağımsız deklarasyonlarla çalışır.
MockBuilder'a deklarasyonun tüm ithalatlarını mock'lamasını belirtir, oysa deklarasyon kendisi mock'lanmayacaktır.

beforeEach(() => {
return MockBuilder()
.keep(StandaloneComponent, {
shallow: true, // StandaloneComponent'in tüm ithalatları mock'lar.
});
});

Eğer bir bağımsız deklarasyon, MockBuilder'ın ilk parametresi olarak geçirilmişse, o zaman shallow bayrağı otomatik olarak ayarlanacaktır.
Bu, onların yüzeysel test edilmesini sağlar.

beforeEach(() => {
// StandaloneComponent'in OneOfItsDependenciesPipe dışında tüm ithalatları mock'lar.
return MockBuilder(StandaloneComponent).keep(OneOfItsDependenciesPipe);
});

render bayrağı

Bir yapısal direktifi varsayılan olarak render etmek istediğimizde, bunu, yapılandırmasında render bayrağını ekleyerek yapabiliriz.

beforeEach(() => {
return MockBuilder(MyComponent, MyModule).mock(MyDirective, {
render: true,
});
});

Direktifin kendine ait bir bağlamı ve değişkenleri varsa.
Bu durumda render'ı true yapmak yerine bağlamı ayarlayabiliriz.

beforeEach(() => {
return MockBuilder(MyComponent, MyModule).mock(MyDirective, {
render: {
$implicit: something1,
variables: { something2: something3 },
},
});
});

Bir bileşende ContentChild kullanıyorsak ve varsayılan olarak render etmek istiyorsak, bunun için id'sini kullanmalıyız, mock direktifine benzer şekilde.

beforeEach(() => {
return MockBuilder(MyComponent, MyModule).mock(MyComponent, {
render: {
blockId: true,
blockWithContext: {
$implicit: something1,
variables: { something2: something3 },
},
},
});
});

onRoot bayrağı

Bu, dahili bir bayraktır ve açıkça kullanılmamalıdır.
Bir modül veya deklarasyonun TestBedModule'da doğrudan tanımlanıp tanımlanmayacağını belirtir, bu, içe aktarıldığı veya iç içe geçmiş modüllerde tanımlandığı durumlarda bile.

Tokenlar

NG_MOCKS_GUARDS tokenı

NG_MOCKS_GUARDS, testte tüm rotalardan koruyucuları kaldırmak için kullanılır.
Belirli bir koruyucuyu test etmek istiyorsanız yararlıdır.
Bunu yapmak için, NG_MOCKS_GUARDS.exclude etmeli ve koruyucuyu .keep etmelisiniz.

beforeEach(() => {
return MockBuilder(
[RouterModule, RouterTestingModule.withRoutes([])],
ModuleWithRoutes,
)
.exclude(NG_MOCKS_GUARDS) // <- tüm koruyucuları kaldırır
.keep(GuardUnderTest) // <- ama GuardUnderTest'ı korur
;
});

NG_MOCKS_RESOLVERS tokenı

NG_MOCKS_RESOLVERS, testte tüm rotalardan tüm resolver'ları kaldırmak için kullanılır.
Belirli bir resolver'ı test etmek istiyorsanız yararlıdır.
Bunu yapmak için, NG_MOCKS_RESOLVERS.exclude etmeli ve resolver'ı .keep etmelisiniz.

beforeEach(() => {
return MockBuilder(
[RouterModule, RouterTestingModule.withRoutes([])],
ModuleWithRoutes,
)
.exclude(NG_MOCKS_RESOLVERS) // <- tüm resolver'ları kaldırır
.keep(ResolverUnderTest) // <- ama ResolverUnderTest'ı korur
;
});

NG_MOCKS_INTERCEPTORS tokenı

Genellikle bir interceptörü test etmek istediğimizde, diğer interceptor'ların etkilerinden kaçınmak isteriz.
Angular testinde tüm interceptor'ları kaldırmak için, NG_MOCKS_INTERCEPTORS tokenını hariç tutmalıyız, ondan sonra tüm interceptor'lar hariç tutulacaktır, aksi takdirde açıklanmış olanlar hariç.

beforeEach(() => {
return MockBuilder(MyInterceptor, MyModule)
.exclude(NG_MOCKS_INTERCEPTORS);
});

NG_MOCKS_ROOT_PROVIDERS tokenı

Angular uygulamalarında sağlananların dışında kök hizmetler ve tokenlar vardır.
Bir testte bu sağlayıcıların sahte olmalarını veya korunmalarını istemek mümkün olabilir.

Angular testinde tüm kök sağlayıcıları sahte ile değiştirmek istiyorsak, NG_MOCKS_ROOT_PROVIDERS tokenını .mock içine geçmeliyiz.

beforeEach(() => {
return MockBuilder(MyComponentWithRootServices, MyModuleWithRootTokens)
.mock(NG_MOCKS_ROOT_PROVIDERS);
});

Buna karşılık, tüm kök sağlayıcıları mock deklarasyonları için korumak isteyebiliriz.
Bunun için NG_MOCKS_ROOT_PROVIDERS tokenını korumalıyız.

beforeEach(() => {
return MockBuilder(MyComponentWithRootServices, MyModuleWithRootTokens)
.keep(NG_MOCKS_ROOT_PROVIDERS);
});

Eğer NG_MOCKS_ROOT_PROVIDERS'ı hiçbir yere geçmezsek, o zaman yalnızca korunan modüllerin kök sağlayıcıları olduğu gibi kalacak.
Diğer tüm kök sağlayıcılar, mock modüllerin korunmayan deklarasyonları için bile kendilerine sahte ile değiştirilir.

Fabrika fonksiyonu

Başka angular için test çerçeveleri kullanıyor olabilirsiniz,
örneğin @ngneat/spectator veya @testing-library/angular.

Bu, fabrika fonksiyonu için bir kullanım durumudur.

Fabrika fonksiyonu, başka bir yerde geçirilebilecek önceden yapılandırılmış bir TestBed deklarasyonu almanızı sağlar.

const ngModule = MockBuilder(MyComponent, MyModule)
.build();

Yukarıdaki kod MyModule'daki her şey için (ithalatlar, deklarasyonlar, sağlayıcılar ve dışa aktarımlar) mock'lar oluşturur, ancak MyComponent'i, test amaçları için olduğu gibi tutar.
Aslında, şu şekilde yapar:

const ngModule = MockBuilder()
.keep(MyComponent, { export: true })
.mock(MyModule, { exportAll: true })
.build();

Ayrıca, tüm deklarasyonlar için mock'lar oluşturmak istiyorsak ilk parametreyi null veya undefined ile bastırabiliriz.

const ngModule = MockBuilder(null, MyModule)
.build();

Bu da şu anlama gelir:

const ngModule = MockBuilder()
.mock(MyModule, { exportAll: true })
.build();

Eğer daha fazla özelleştirme planlamıyorsanız, ngModule'ın sonucunu .build() çağırmadan direkt olarak dönebilirsiniz.

// MockBuilder'ın promisesini döndürmeyi unutmayın.
beforeEach(() => MockBuilder(MyComponent, MyModule));

Bu da şunları yapar:

beforeEach(() => {
const ngModule = MockBuilder()
.keep(MyComponent, { export: true })
.mock(MyModule, { exportAll: true })
.build();
TestBed.configureTestingModule(ngModule);
return TestBed.compileComponents();
});

description: Bu içerik, MockBuilder'a özelleştirilmiş yöntemler eklemek ve bu yöntemleri nasıl kullanacağınız hakkında rehberlik sağlar. Ayrıca MockBuilder ile şemalar ekleme ve bilinmesi gerekenler üzerinde durur.

keywords: [MockBuilder, özelleştirilmiş yöntem, Angular, test uyarlama, testBed]

MockBuilder Genişletme

MockBuilder'a kendi yöntemlerinizi eklemek istiyorsanız, bunu MockBuilder.extend(method, callback) ile yapabilirsiniz.

Varsayalım ki, bir dize kabul eden customMock adında bir yöntem eklemek istiyoruz ve dize bir serviste döndürme değeri olarak kullanılacak.

Sonuçta bu şekilde kullanılmalıdır:

MockBuilder(/* ... */)
.mock(/* ... */)
.customMock('değer') // <-- MockBuilder'a eklenti
.keep(/* ... */);

İlk adım, customMock'u MockBuilder türünün bir parçası olarak tanımlamaktır:

declare module 'ng-mocks' {
interface IMockBuilderExtended {
// parametreler istediğiniz gibi olabilir
customMock(value: string): this; // bu değeri döndürmesi gerekir
}
}

customMock'un parametreleri, özel geri çağırma işlevinize iletmek istediğiniz herhangi bir şey olabilir, bizim durumumuzda bir dizedir. Ancak, döndürme türünün this olması gerektiğini lütfen unutmayın.

Doğru parametreler türü

Doğru bir tür demeti almak için Parameters adlı yerleşik bir türü kullanabilirsiniz: Parameters<IMockBuilderExtended['customMock']>, basitçe customMock'u özel yönteminizin adıyla değiştirin.

// Yerleşik `Parameters` türü, tür güvenliği için kullanılabilir. 
MockBuilder.extend(
'customMock', // <-- özel yöntemimizin adı

(builder, parameters: Parameters<IMockBuilderExtended['customMock']>) => { // Değeri çıkarma.
const value = parameters[0];

// Oluşturucu üzerinde özel mantığı çağırma.
// Bu durumda, TargetService.echo() değeri döndürmelidir.
builder.mock(TargetService, {
echo: () => value,
});
},
);

Kar, şimdi MockBuilder().customMock('mock') çağırırsanız, testinde TargetService.echo() çağrısı 'mock' döndürecektir.

Özel bir yöntemi silerken

Özel bir yöntemi silmek isterseniz, ikinci parametre olmadan MockBuilder.extend()'i çağırmanız yeterlidir:

MockBuilder.extend('customMock');
MockBulder().customMock(''); // artık bir hata fırlatır

Şemalar Eklemek

MockBuilder, oluşturulan testBed'i özelleştirmeye olanak tanıyan beforeCompileComponents adında bir yöntem sağlar. Örneğin, schemas: CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA eklemek için.

beforeEach(() => {
return MockBuilder(MyComponent, MyModule)
.beforeCompileComponents(testBed => {
testBed.configureTestingModule({
schemas: [
NO_ERRORS_SCHEMA,
],
});
});
});

Bilinmesi Gerekenler

Her zaman kararımızı değiştirebiliriz. Aynı nesnedeki son işlem kazanan olur. SomeModule kendi sahte nesnesi ile değiştirilecektir.

beforeEach(() => {
return MockBuilder(MyComponent, MyModule)
.keep(SomeModule)
.mock(SomeModule)
.keep(SomeModule)
.mock(SomeModule);
});