test: remove stdout and std from test by mocking logger module (#161)
- Same as #143 - Resolves https://github.com/stats-organization/github-stats-extended/pull/143#discussion_r3105155487 - Closes #141
This commit is contained in:
@@ -1,9 +0,0 @@
|
||||
// @ts-check
|
||||
/**
|
||||
* Return console instance based on the environment.
|
||||
*
|
||||
* @type {Console | {log: () => void, error: () => void}}
|
||||
*/
|
||||
const logger = console;
|
||||
|
||||
export { logger };
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Return console instance based on the environment.
|
||||
*/
|
||||
const logger: {
|
||||
log: (...args: Array<unknown>) => void;
|
||||
error: (...args: Array<unknown>) => void;
|
||||
} = {
|
||||
log: console.log,
|
||||
error: console.error,
|
||||
};
|
||||
|
||||
export { logger };
|
||||
@@ -1,11 +1,16 @@
|
||||
import axios from "axios";
|
||||
import MockAdapter from "axios-mock-adapter";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { calculateRank } from "../src/calculateRank.js";
|
||||
import { loadConfigFromEnv } from "../src/common/config.js";
|
||||
import { fetchStats } from "../src/fetchers/stats.js";
|
||||
|
||||
vi.mock(import("../src/common/log.js"), async () => {
|
||||
const { createLoggerMock } = await import("./utils.js");
|
||||
return createLoggerMock();
|
||||
});
|
||||
|
||||
// Test parameters.
|
||||
const data_stats = {
|
||||
data: {
|
||||
@@ -534,7 +539,6 @@ describe("Test fetchStats", () => {
|
||||
});
|
||||
|
||||
it("should return correct data when user don't have any pull requests", async () => {
|
||||
mock.reset();
|
||||
mock
|
||||
.onPost("https://api.github.com/graphql")
|
||||
.reply(200, data_without_pull_requests);
|
||||
|
||||
@@ -1,15 +1,25 @@
|
||||
import axios from "axios";
|
||||
import MockAdapter from "axios-mock-adapter";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { fetchTopLanguages } from "../src/fetchers/top-languages.js";
|
||||
|
||||
import { approxNumber } from "./utils.js";
|
||||
|
||||
vi.mock(import("../src/common/log.js"), async () => {
|
||||
const { createLoggerMock } = await import("./utils.js");
|
||||
return createLoggerMock();
|
||||
});
|
||||
|
||||
const { logger } = await import("../src/index.js");
|
||||
|
||||
const loggerErrorSpy = vi.mocked(logger.error);
|
||||
|
||||
const mock = new MockAdapter(axios);
|
||||
|
||||
afterEach(() => {
|
||||
mock.reset();
|
||||
loggerErrorSpy.mockClear();
|
||||
});
|
||||
|
||||
const data_langs = {
|
||||
@@ -149,6 +159,8 @@ describe("FetchTopLanguages", () => {
|
||||
await expect(fetchTopLanguages("anuraghazra")).rejects.toThrow(
|
||||
"Could not resolve to a User with the login of 'noname'.",
|
||||
);
|
||||
|
||||
expect(loggerErrorSpy).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("should throw other errors with their message", async () => {
|
||||
@@ -159,6 +171,8 @@ describe("FetchTopLanguages", () => {
|
||||
await expect(fetchTopLanguages("anuraghazra")).rejects.toThrow(
|
||||
"Some test GraphQL error",
|
||||
);
|
||||
|
||||
expect(loggerErrorSpy).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("should throw error with specific message when error does not contain message property", async () => {
|
||||
@@ -169,5 +183,7 @@ describe("FetchTopLanguages", () => {
|
||||
await expect(fetchTopLanguages("anuraghazra")).rejects.toThrow(
|
||||
"Something went wrong while trying to retrieve the language data using the GraphQL API.",
|
||||
);
|
||||
|
||||
expect(loggerErrorSpy).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,55 +2,45 @@
|
||||
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { logger } from "../src/common/log.js";
|
||||
import { retryer } from "../src/common/retryer.js";
|
||||
|
||||
const fetcher = vi.fn((variables, token) => {
|
||||
logger.log(variables, token);
|
||||
return new Promise((res) => res({ data: "ok" }));
|
||||
vi.mock(import("../src/common/log.js"), async () => {
|
||||
const { createLoggerMock } = await import("./utils.js");
|
||||
return createLoggerMock();
|
||||
});
|
||||
|
||||
const fetcherFail = vi.fn(() => {
|
||||
return new Promise((res) =>
|
||||
res({ data: { errors: [{ type: "RATE_LIMITED" }] } }),
|
||||
);
|
||||
const fetcher = vi.fn().mockResolvedValue({ data: "ok" });
|
||||
|
||||
const fetcherFail = vi.fn().mockResolvedValue({
|
||||
data: { errors: [{ type: "RATE_LIMITED" }] },
|
||||
});
|
||||
|
||||
const fetcherFailOnSecondTry = vi.fn((_vars, _token, retries) => {
|
||||
return new Promise((res) => {
|
||||
// faking rate limit
|
||||
// @ts-ignore
|
||||
if (retries < 1) {
|
||||
return res({ data: { errors: [{ type: "RATE_LIMITED" }] } });
|
||||
}
|
||||
return res({ data: "ok" });
|
||||
});
|
||||
if (retries < 1) {
|
||||
return Promise.resolve({ data: { errors: [{ type: "RATE_LIMITED" }] } });
|
||||
}
|
||||
return Promise.resolve({ data: "ok" });
|
||||
});
|
||||
|
||||
const fetcherFailWithMessageBasedRateLimitErr = vi.fn(
|
||||
(_vars, _token, retries) => {
|
||||
return new Promise((res) => {
|
||||
// faking rate limit
|
||||
// @ts-ignore
|
||||
if (retries < 1) {
|
||||
return res({
|
||||
data: {
|
||||
errors: [
|
||||
{
|
||||
type: "ASDF",
|
||||
message: "API rate limit already exceeded for user ID 11111111",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
return res({ data: "ok" });
|
||||
});
|
||||
if (retries < 1) {
|
||||
return Promise.resolve({
|
||||
data: {
|
||||
errors: [
|
||||
{
|
||||
type: "ASDF",
|
||||
message: "API rate limit already exceeded for user ID 11111111",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
return Promise.resolve({ data: "ok" });
|
||||
},
|
||||
);
|
||||
|
||||
const customFetcher = vi.fn((variables, token) => {
|
||||
logger.log(variables, token);
|
||||
return Promise.resolve({ data: { token } });
|
||||
});
|
||||
|
||||
@@ -77,20 +67,21 @@ describe("Test Retryer", () => {
|
||||
});
|
||||
|
||||
it("retryer should throw specific error if maximum retries reached", async () => {
|
||||
try {
|
||||
await retryer(fetcherFail, {});
|
||||
} catch (err) {
|
||||
expect(fetcherFail).toHaveBeenCalledTimes(2);
|
||||
// @ts-ignore
|
||||
expect(err.message).toBe("Downtime due to GitHub API rate limiting");
|
||||
}
|
||||
await expect(retryer(fetcherFail, {})).rejects.toThrow(
|
||||
"Downtime due to GitHub API rate limiting",
|
||||
);
|
||||
|
||||
expect(fetcherFail).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("retryer should use injected PATs when provided", async () => {
|
||||
const res = await retryer(customFetcher, {}, "user-pat-token");
|
||||
|
||||
expect(customFetcher).toHaveBeenCalledTimes(1);
|
||||
expect(customFetcher).toHaveBeenCalledWith({}, "user-pat-token", 0);
|
||||
expect(customFetcher).toHaveBeenCalledExactlyOnceWith(
|
||||
{},
|
||||
"user-pat-token",
|
||||
0,
|
||||
);
|
||||
expect(res).toStrictEqual({ data: { token: "user-pat-token" } });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* Creates an asymmetric matcher for approximate numeric equality.
|
||||
*
|
||||
* This helper is intended for use in test frameworks (e.g., Jest) where
|
||||
* values need to be compared within a configurable decimal precision
|
||||
* instead of strict equality.
|
||||
*
|
||||
* The comparison succeeds when:
|
||||
*
|
||||
* |actual - expected| < 10^(-precision)
|
||||
*
|
||||
* For example, with `precision = 3`, values must be within `0.001`.
|
||||
*
|
||||
* @param {number} expected The expected numeric value to compare against.
|
||||
*
|
||||
* @param {number} [precision=10]
|
||||
* The number of decimal places of tolerance. Higher values mean stricter
|
||||
* comparison. Internally converted to epsilon = 10^-precision.
|
||||
*
|
||||
* @returns {{
|
||||
* asymmetricMatch(actual: unknown): boolean,
|
||||
* toAsymmetricMatcher(): string
|
||||
* }} An object implementing Jest-style asymmetric matcher methods.
|
||||
*
|
||||
*/
|
||||
export function approxNumber(expected, precision = 10) {
|
||||
return {
|
||||
asymmetricMatch(actual) {
|
||||
if (typeof actual !== "number" || typeof expected !== "number") {
|
||||
return false;
|
||||
}
|
||||
const epsilon = Math.pow(10, -precision);
|
||||
return Math.abs(actual - expected) < epsilon;
|
||||
},
|
||||
toAsymmetricMatcher() {
|
||||
return `≈ ${expected} (precision ${precision})`;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { vi } from "vitest";
|
||||
|
||||
import type * as loggerModule from "../src/common/log.js";
|
||||
|
||||
/**
|
||||
* Creates an asymmetric matcher for approximate numeric equality.
|
||||
*
|
||||
* This helper is intended for use in test frameworks (e.g., Jest) where
|
||||
* values need to be compared within a configurable decimal precision
|
||||
* instead of strict equality.
|
||||
*
|
||||
* The comparison succeeds when:
|
||||
*
|
||||
* |actual - expected| < 10^(-precision)
|
||||
*
|
||||
* For example, with `precision = 3`, values must be within `0.001`.
|
||||
*
|
||||
* @param expected The expected numeric value to compare against.
|
||||
*
|
||||
* @param
|
||||
* The number of decimal places of tolerance. Higher values mean stricter
|
||||
* comparison. Internally converted to epsilon = 10^-precision.
|
||||
*
|
||||
* @returns An object implementing Jest-style asymmetric matcher methods.
|
||||
*
|
||||
*/
|
||||
export function approxNumber(
|
||||
expected: number,
|
||||
precision = 10,
|
||||
): {
|
||||
asymmetricMatch(actual: unknown): boolean;
|
||||
toAsymmetricMatcher(): string;
|
||||
} {
|
||||
return {
|
||||
asymmetricMatch(actual) {
|
||||
if (typeof actual !== "number" || typeof expected !== "number") {
|
||||
return false;
|
||||
}
|
||||
const epsilon = Math.pow(10, -precision);
|
||||
return Math.abs(actual - expected) < epsilon;
|
||||
},
|
||||
toAsymmetricMatcher() {
|
||||
return `≈ ${expected} (precision ${precision})`;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to create logger module mocks to use in unit tests.
|
||||
* If you need to perform assertions on logger use `vi.mocked`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { logger } from "../src/common/log.js";
|
||||
*
|
||||
* vi.mock(import("../src/common/log.js"), async () => {
|
||||
* const { createLoggerMock } = await import("./utils.js");
|
||||
* return createLoggerMock();
|
||||
* });
|
||||
*
|
||||
* const logSpy = vi.mocked(logger.log);
|
||||
* ```
|
||||
*
|
||||
* @returns mocked logger module
|
||||
*/
|
||||
export function createLoggerMock(): typeof loggerModule {
|
||||
return {
|
||||
logger: {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user