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:
Marco Pasqualetti
2026-04-19 09:28:46 +02:00
committed by GitHub
parent d476f8cbe1
commit fc4ee7507b
21 changed files with 450 additions and 262 deletions
-9
View File
@@ -1,9 +0,0 @@
// @ts-check
/**
* Return console instance based on the environment.
*
* @type {Console | {log: () => void, error: () => void}}
*/
const logger = console;
export { logger };
+12
View File
@@ -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 };
+6 -2
View File
@@ -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);
+17 -1
View File
@@ -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();
});
});
+34 -43
View File
@@ -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" } });
});
});
-41
View File
@@ -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})`;
},
};
}
+73
View File
@@ -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(),
},
};
}