react native - Typescript'te Jest'te varsayılan dışa aktarılan sınıfta yöntemi taklit etme
Bağlam
Özel bir kancayı test etmek istiyorum, bu kanca @react-native-firebase/dynamic-links bağımlılığına sahip. React Native için @testing-library ve kancaları test etmek için yardımcı fonksiyonlarını (@testing-library/react-hooks) kullanıyoruz.
Bu, test etmek istediğim kanca (bu basitleştirilmiş bir örnektir):
import { useEffect } from 'react';
import dynamicLinks from '@react-native-firebase/dynamic-links';
import { navigateFromBackground } from '../deeplink';
// Handles dynamic link when app is loaded from closed state.
export const useDynamicLink = (): void => {
useEffect(() => {
void dynamicLinks()
.getInitialLink()
.then((link) => {
if (link && link.url) {
navigateFromBackground(link.url);
}
});
}, []);
};
Her ayrı testte getInitialLink çağrısının bir şey döndürmesini isterim. getInitialLink'i jest.mock(...) ile taklit edebildim, ancak bu tüm testler için onu taklit ediyor. Sanırım sorun, taklit etmek istediğim yöntemin bir sınıfın yöntemi olması.
import { useDynamicLink } from './useDynamicLink';
import { renderHook, act } from '@testing-library/react-hooks';
import { navigateFromBackground } from '../deeplink';
jest.mock('../deeplink');
// IMPORTANT: You cannot mock constructors with arrow functions. New cannot be
// called on an arrow function.
jest.mock('@react-native-firebase/dynamic-links', () => {
return function () {
return {
getInitialLink: async () => ({
url: 'fake-link',
}),
};
};
});
describe('tryParseDynamicLink', () => {
it('should return null if url is empty', async () => {
// IMPORTANT: act wrapper is needed so that all events are handled before
// state is inspected by the test.
await act(async () => {
renderHook(() => useDynamicLink());
});
expect(navigateFromBackground).toHaveBeenCalledWith('fake-link');
});
});
Denemeler
Yani bu işe yarıyor, ama her test için dönüş değerini değiştirebilmiyorum. Jest, bağımlılıkları taklit etmek için çeşitli yollar sunuyor, ancak bunu çalıştırmayı başaramadım.
jest.MockedClass
Firebase varsayılan olarak bir sınıf ihraç eder, ancak sınıfın kendisi sarılıdır.
declare const defaultExport: ReactNativeFirebase.FirebaseModuleWithStatics<
FirebaseDynamicLinksTypes.Module,
FirebaseDynamicLinksTypes.Statics
>;
Dokümantasyona göre, aşağıda açıklandığı şekilde taklit etmeniz gerekecek.
import dynamicLinks from '@react-native-firebase/dynamic-links';
const dynamicLinksMock = dynamicLinks as jest.MockedClass<typeof dynamicLinks>;
Ancak aşağıdaki hatayı verir:
Type 'FirebaseModuleWithStatics<Module, Statics>' does not satisfy the constraint 'Constructable'.
Type 'FirebaseModuleWithStatics<Module, Statics>' provides no match for the signature 'new (...args: any[]): any'.
Aslında, sarıldığı için bunu bir sınıf olarak tanımıyor.
jest.MockedFunction
Daha sonra bunu bir fonksiyon kullanarak (ve ok fonksiyonları kullanmadan) taklit etmeye karar verdim. Bu yaklaşımla çok daha ilerleyebildim, ancak bu yaklaşımda tüm özellikleri sağlamam gerekiyor. Bir süre bununla denedim, ancak X sayıda özelliği ekledikten sonra vazgeçtim (aşağıdaki kod örneğine bakın). Yani eğer bu gidilecek yolsa, bunun çoğunu otomatik olarak nasıl taklit edebileceğimi bilmek istiyorum.
import { useDynamicLink } from './useDynamicLink';
import { renderHook, act } from '@testing-library/react-hooks';
import { navigateFromBackground } from '../deeplink';
import dynamicLinks from '@react-native-firebase/dynamic-links';
const dynamicLinksMock = dynamicLinks as jest.MockedFunction<
typeof dynamicLinks
>;
jest.mock('../deeplink');
describe('tryParseDynamicLink', () => {
it('should return null if url is empty', async () => {
// eslint-disable-next-line prefer-arrow-callback
dynamicLinksMock.mockImplementationOnce(function () {
return {
buildLink: jest.fn(),
buildShortLink: jest.fn(),
app: {
options: {
appId: 'fake-app-id',
projectId: 'fake-project-id',
},
delete: jest.fn(),
utils: jest.fn(),
analytics: jest.fn(),
name: 'fake-name',
crashlytics: jest.fn(),
dynamicLinks: jest.fn(),
},
onLink: jest.fn(),
resolveLink: jest.fn(),
native: jest.fn(),
emitter: jest.fn(),
getInitialLink: async () => ({
minimumAppVersion: '123',
utmParameters: { 'fake-param': 'fake-value' },
url: 'fake-link',
}),
};
});
await act(async () => {
renderHook(() => useDynamicLink());
});
expect(navigateFromBackground).toHaveBeenCalledWith('fake-link');
});
});
jest.spyOn
Son deneme, bu durumda uygun görünen spyOn kullanmaktı. Ancak sadece belirli işlevleri taklit edeceğinden, testleri çalıştırmayı denediğimde bu bir çalışma zamanı hatası veriyor.
import { useDynamicLink } from './useDynamicLink';
import { renderHook, act } from '@testing-library/react-hooks';
import { navigateFromBackground } from '../deeplink';
import dynamicLinks from '@react-native-firebase/dynamic-links';
jest.mock('../deeplink');
// Ensure automock
jest.mock('@react-native-firebase/dynamic-links');
describe('tryParseDynamicLink', () => {
it('should return null if url is empty', async () => {
jest
.spyOn(dynamicLinks.prototype, 'getInitialLink')
.mockImplementationOnce(async () => 'test');
await act(async () => {
renderHook(() => useDynamicLink());
});
expect(navigateFromBackground).toHaveBeenCalledWith('fake-link');
});
});
Hata :
Cannot spy the getInitialLink property because it is not a function; undefined given instead
Özetle, getInitialLink metodunu nasıl taklit edeceğim konusunda tamamen kayboldum. Eğer biri herhangi bir tavsiye veya ipucu verebilirse çok memnun olurum!
Düzeltme 1:
@user275564'in tavsiyesi üzerine şunları denedim:
jest.spyOn(dynamicLinks, 'dynamicLinks').mockImplementation(() => {
return { getInitialLink: () => Promise.resolve('fake-link') };
});
Maalesef typescript, aşağıdaki hata nedeniyle derlenemiyor:
No overload matches this call.
Overload 1 of 4, '(object: FirebaseModuleWithStatics<Module, Statics>, method: never): SpyInstance<never, never>', gave the following error.
Argument of type 'string' is not assignable to parameter of type 'never'.
Overload 2 of 4, '(object: FirebaseModuleWithStatics<Module, Statics>, method: never): SpyInstance<never, never>', gave the following error.
Argument of type 'string' is not assignable to parameter of type 'never'.
Oradaki nesne üzerinde yalnızca statik özellikleri ortaya koyabiliyorum, bunlar şunlardır:
Bu yüzden bu cevaptaki önerilen dynamicLinks.prototype'i tercih ettim.
Cevap #1
Dinamik bağlantılar (veya diğer Firebase fonksiyonları) kullanarak bir servis oluşturmayı tercih ederim. Taklit etmesi kolaydır.
dynamicLinksService.ts
import dynamicLinks from '@react-native-firebase/dynamic-links';
export const getInitialLink = () => dynamicLinks().getInitialLink();
useDynamicLink.ts
import { useEffect } from 'react';
import { navigateFromBackground } from '../deeplink';
import { getInitialLink } from './dynamicLinkService';
export const useDynamicLink = (): void => {
useEffect(() => {
getInitialLink().then((link) => {
if (link && link.url) {
navigateFromBackground(link.url);
}
});
}, []);
};
useDynamicLink.test.ts
import { renderHook, act } from '@testing-library/react-hooks';
import { navigateFromBackground } from '../deeplink';
import { getInitialLink } from './dynamicLinkService';
import { useDynamicLink } from './useDynamicLink';
jest.mock('../deeplink', () => ({
navigateFromBackground: jest.fn(),
}));
jest.mock('./dynamicLinkService', () => ({
getInitialLink: jest.fn(),
}));
describe('The useDynamicLink', () => {
it('should not navigate when link in empty', async () => {
const getInitialLinkMock = getInitialLink as jest.Mock;
getInitialLinkMock.mockResolvedValue(null);
await act(async () => {
renderHook(() => useDynamicLink());
});
expect(navigateFromBackground).not.toHaveBeenCalled();
});
it('should navigate when link is exist', async () => {
const getInitialLinkMock = getInitialLink as jest.Mock;
getInitialLinkMock.mockResolvedValue({ url: 'www.google.com' });
await act(async () => {
renderHook(() => useDynamicLink());
});
expect(navigateFromBackground).toHaveBeenCalledWith('www.google.com');
});
});
Cevap #2
Senin jest.spyOn üzerinde biraz çalışmaya ihtiyacı var.
Jest.spyOn, taklitler (mocks) ile farklıdır çünkü bulunduğunuz kapsamda kendi taklidini temizler (ve aslında siz açıkça mockImplementation gibi bir şey çağırana kadar gerçekten bir taklit değildir, bu yüzden buna 'spy' denir). Taklitlerinizi sürekli değiştirmek istediğiniz için, spyOn() kullanmalı ve her testte implementasyonu taklit etmelisiniz, böylece her seferinde taklitleri temizleme gereğinden kaynaklanan tekrarları azaltırsınız. Her ikisi de gayet iyi çalışabilir, fakat ben deneme 3 üzerinde çalışırdım.
Öncelikle, dinamik bağlantıların taklidini kaldır, çünkü bunun yerine her belirli testi gözlemleyeceğiz ve uygulamayı orada taklit edeceğiz.
İkincisi, bir dışa aktarılan fonksiyonu doğrudan çağırdığınız için fonksiyonu bu şekilde içe aktarmanız ve gözetlemeniz gerekiyor.
import * as dynamicLinks from '@react-native-firebase/dynamic-links';
const dynamicLinkSpy = jest.spyOn(dynamicLinks, 'dynamicLinks').mockImplentation( ... )
dynamicLinks artık jest'in casusluk yaptığı ve aradığı fonksiyon olan dynamicLinks() olarak dışa aktarılan dosyadır; bu, üretim kodunun çağırdığı şeydir.
Bir başka hata .prototype eklemekten kaynaklanıyor. Üretim kodunun bunu nasıl çağırdığına bakmalısınız, testler bunu taklit etmelidir. Ayrıca bunun için, dynamicLinks üzerinde uygulamayı değiştiriyorsunuz, bu nesne üzerinde çağrılan iç içe fonksiyonlardan aşağı doğru çalışacak dönüş değerini oluşturmanız gerekiyor. Ayrıca, .then() kullandığınız için üretim kodunuz işlevde bir Promise’in çözümlenmesini bekliyor. Şöyle;
const dynamicLinkSpy = jest
.spyOn(dynamicLinks, 'dynamicLinks')
.mockImplementation(()=>{ return {getInitialLink: ()=> Promise.resolve('test')}} );
Şimdi, farklı dönüş değerleriyle oynayabilir ve her zamanki gibi farklı sonuçlar bekleyebilirsiniz. Ayrıca, çağrılıp çağrılmadığını test etmeniz gerektiğini unutmayın. Aşağıdaki gibi:
expect(dynamicLinkSpy).toHaveBeenCalled();
