[Unit testing] Testing a fetch promise
Code:
import type { paths } from "@octokit/openapi-types";
type OrgRepoResponse =
paths["/repos/{owner}/{repo}"]["get"]["responses"]["200"]["content"]["application/json"];
export type Fetch = typeof fetch;
export class GithubApi {
constructor(
private token: string | undefined,
private fetch: Fetch = fetch,
private delay: (ms: number) => Promise<void> = delay
) {}
async getRepositories(user: string) {
let page = 1;
const repos: OrgRepoResponse[] = [];
while (true) {
const response = await this.fetch(
`https://api.github.com/users/${user}/repos?per_page=30&page=${page}`,
{
headers: {
"User-Agent": "Qwik Workshop",
"X-GitHub-Api-Version": "2022-11-28",
},
}
);
const json = (await response.json()) as OrgRepoResponse[];
repos.push(...json);
if (json.length < 30) {
break;
}
page++;
}
return repos;
}
async getRepository(user: string, repo: string) {
const headers: HeadersInit = {
"User-Agent": "Qwik Workshop",
"X-GitHub-Api-Version": "2022-11-28",
};
if (this.token) {
headers["Authorization"] = "Bearer " + this.token;
}
return Promise.race([
this.delay(4000).then(() => {
return { response: "timeout" };
}),
this.fetch(`https://api.github.com/repos/${user}/${repo}`, {
headers,
}).then((response) => {
return response.json();
}),
]);
}
}
export function delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
Test:
import { describe, test, vi, beforeEach, Mock } from "vitest";
import { Fetch, GithubApi, delay } from "./api";
describe("github-api", () => {
let fetchMock: Mock<Parameters<Fetch>, Promise<Response>>;
let api: GithubApi;
let delayMock: Mock<[number], Promise<void>>;
beforeEach(() => {
delayMock = vi.fn<[number], Promise<void>>(mockPromise);
fetchMock = vi.fn<Parameters<Fetch>, Promise<Response>>(mockPromise);
api = new GithubApi("TOKEN", fetchMock, delayMock);
});
describe("getRepository", () => {
test("should return repository information", async ({ expect }) => {
const responsePromise = api.getRepository("USERNAME", "REPO");
expect(fetchMock).toHaveBeenCalledWith(
"https://api.github.com/repos/USERNAME/REPO",
{
headers: {
"User-Agent": "Qwik Workshop",
"X-GitHub-Api-Version": "2022-11-28",
Authorization: "Bearer TOKEN",
},
}
);
fetchMock.mock.results[0].value.resolve(new Response('"MockResponse"'));
expect(await responsePromise).toEqual("MockResponse");
});
test("should timeout after x seconds with time out response", async ({
expect,
}) => {
const responsePromise = api.getRepository("USERNAME", "REPO");
expect(fetchMock).toHaveBeenCalledWith(
"https://api.github.com/repos/USERNAME/REPO",
{
headers: {
"User-Agent": "Qwik Workshop",
"X-GitHub-Api-Version": "2022-11-28",
Authorization: "Bearer TOKEN",
},
}
);
expect(delayMock).toHaveBeenCalledWith(4000);
delayMock.mock.results[0].value.resolve();
expect(await responsePromise).toEqual({ response: "timeout" });
});
});
describe("getRepositories", () => {
test("should fetch all repositories for a user", async ({ expect }) => {
const responsePromise = api.getRepositories("USERNAME");
expect(fetchMock).toHaveBeenCalledWith(
"https://api.github.com/users/USERNAME/repos?per_page=30&page=1",
expect.any(Object)
);
const repoSet1 = new Array(30).fill(null).map((_, i) => ({ id: i }));
fetchMock.mock.results[0].value.resolve(
new Response(JSON.stringify(repoSet1))
);
await delay(0);
const repoSet2 = [{ id: 30 }];
fetchMock.mock.results[1].value.resolve(
new Response(JSON.stringify(repoSet2))
);
expect(await responsePromise).toEqual([...repoSet1, ...repoSet2]);
});
});
});
function mockPromise<T>(): Promise<T> & {
resolve: typeof resolve;
reject: typeof reject;
} {
let resolve!: (value: T) => void;
let reject!: (error: any) => void;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
}) as any;
promise.resolve = resolve;
promise.reject = reject;
return promise;
}