Кажется, что нет такого проекта, в котором не нужно делать запросы к серверу. И всегда хорошей идеей написать отдельную абстракцию для описания подобной коммуникации. Это добавляет коду не только читаемости и переиспользуемости, но и дает возможность быстро и эффективно написать тест для подобной функциональности.
class NetworkService {
constructor(private readonly fetch: typeof fetch) {}
getUser: (): Promise<IUser> => this.fetch('/api/user').then(d => d.json())
}
function getUsername(api: Pick<NetworkService, 'getUser'>) {
return api.getUser().then(user => user.name);
}
test('getUsername', async () => {
const name = await getUsername({
getUser: () => Promise.resolve({ name: 'test' } as IUser)
});
expect(name).toBe('test');
});
И для среднестатистического проекта такого подхода будет достаточно. Но иногда возникают требования, что нужно переопределить сам fetch
. Например, у нас в проекте, мы разрабатываем extension для браузера и хотим делать запросы через serviceWorker. И возникает требования переимплементировать Fetch API
. Как мы знаем fetch возвращает примитив Response
, для создания которого нужно передать первым аргументом ReadableStream
, а вторым характеристики ответа. Таким образом мы можем использовать следующую конструкцию.
function buildFetchEmulator(
controller: (url: RequestInfo, init: RequestInit | undefined) =>
Promise<[ResponseInit, (add: (msg: object) => void) => Promise<void>]>
) {
const cFetch: typeof fetch = async (url, init) => {
// извлекаем характеристики ответа и контроллер который будет генерировать данные
const [responseInit, ctrl] = await controller(url, init);
// описываем поведение ReadableStream
const stream = new ReadableStream({
async start(controller) {
try {
const enc = new TextEncoder();
await ctrl((msg) => {
// преобразуем обьект в строку
const message = JSON.stringify(msg);
// строку в Uint8Array
const buf = enc.encode(message);
// передаем данные в ReadableStream controller
controller.enqueue(buf);
});
controller.close();
} catch (err) {
controller.error(err);
controller.close();
}
},
});
return new Response(stream, responseInit);
};
return cFetch;
}
test('getUser', () => {
const customFetch = buildFetchEmulator(async (url, init) => {
if (url === '/api/user') {
return [{
headers: new Headers({ 'content-type': contentType }),
status: 200,
statusText: 'OK',
}, async (add) => {
// Здесь мы можем использовать любую логику для транспорта данных
add({ name: 'test' })
}];
}
throw new Error('not implemented');
})
const network = new NetworkService();
const name = await getUsername(network);
expect(name).toBe('test');
});
Здесь хочется еще раз отметить, что для случаев когда нет необходимости эмулировать fetch
и писать кастомные адапторы, первый способ намного лаконичнее и удобнее, но при данном подходе мы можем тестировать даже внешние библиотеки которые используют fetch
напрямую, например как @microsoft/fetch-event-source
.