* chore(tooling): replace ESLint + Prettier with Biome - Add biome.json migrated from the ESLint flat config (preset: none, drop-in rules) with tab indentation, lineWidth 100, and the evolution-types submodule excluded. - Enable unsafeParameterDecoratorsEnabled for DI/worker param decorators. - Switch lint/lint:fix scripts and lint-staged to Biome; drop ESLint and Prettier devDependencies and their config files. - Update .editorconfig to tabs and refresh CONTRIBUTING/testing docs. No source reformatting in this commit. * style: reformat codebase with Biome (spaces to tabs) Mass-apply `biome format --write` across src/ and config files. Indentation converted from 2 spaces to tabs, lineWidth 100, plus Biome's default trailing commas. No logic changes — build and the full test suite (704 tests) pass unchanged. This is a formatting-only commit; see .git-blame-ignore-revs. * chore: ignore the Biome reformat commit in git blame
5.3 KiB
Testing conventions
How we write tests in EDOpro-server-ts: where they live, how we build test data, how we mock, and how we format them. The goal is one consistent pattern so any test reads like the one next to it.
Status: Active standard. All tests are co-located under
src/— the legacytests/folder has been fully migrated and removed. New tests must follow this doc.
Quick path — writing a new test
- Co-locate it. Put
Thing.test.tsnext toThing.tsinsidesrc/. Never add tests to the roottests/folder. - Build domain objects with a Mother. Use (or create) an Object Mother for shared domain entities. Use a local
make*factory only for stubs specific to that one suite. - Mock infra with the shared doubles.
LoggerMock,SocketMock,MessageRepositoryMockfor ubiquitous interfaces;jest.mock()for module singletons;mock<T>()(jest-mock-extended) for one-off interface mocks. - Reset singletons in
afterEach(e.g.WindbotModule.resetForTests(),JoinStrategyRegistry.reset()). - Format: tab indentation — Biome applies it (
npm run format), and your editor via.editorconfig.
Where tests live
| Rule | Detail |
|---|---|
| Co-location | src/<feature>/Thing.test.ts sits next to src/<feature>/Thing.ts. The test travels with the code it tests. |
| Shared test support | Mothers and mocks live in src/test-support/ (mothers/, mocks/), importable from any co-located test. |
Legacy tests/ |
Being phased out. Do not add to it. When you touch a module, migrate its legacy test to co-location. |
Why co-location: a mirrored tests/ tree drifts from src/ (it already did). The test next to the source is discovered together, refactored together, and never goes stale.
Building test data
Decide by what you are building:
| You are building… | Use | Why |
|---|---|---|
| A shared domain entity (Client, Player, Room, YGOProRoom, UserProfile, Game) | Object Mother | One canonical builder. Faker defaults expose hidden coupling; override only the field under test. |
| A local stub used by one suite (a fake repo, provider, socket) | Inline make* factory |
Lightweight, self-contained, fresh per test. No need to share. |
Object Mother (domain entities)
A static create(params?: Partial<Props>) returning a real domain object, with sensible faker-backed defaults:
export class ClientMother {
static create(params?: Partial<ClientMotherProps>): Client {
return new Client({
name: params?.name ?? faker.person.firstName(),
team: params?.team ?? faker.number.int({ min: 0, max: 1 }),
// ...other faker defaults
...params,
});
}
}
// usage — override only what the test cares about
const client = ClientMother.create({ id: "1" });
Rule: one Mother per shared domain entity. If an entity is built more than one way across the suite, that is a bug to consolidate (see Migration targets).
Inline factory (local stubs)
For doubles that only one suite needs — compose with an overrides parameter:
const makeRepo = (overrides: Partial<BotlistRepository> = {}): BotlistRepository => ({
findAll: jest.fn().mockReturnValue([]),
findByName: jest.fn().mockReturnValue(null),
...overrides,
});
Mocking
| Need | Use |
|---|---|
| Ubiquitous infra interface (Logger, Socket, MessageRepository) | Shared Mock class from src/test-support/mocks/ |
A module-level singleton (e.g. WebSocketSingleton) |
jest.mock("...path...") at the top of the file |
| A one-off interface mock | mock<T>() from jest-mock-extended (type-safe, no hand-rolled jest.fn() objects) |
| A spy on a real method | jest.spyOn(obj, "method") |
Pick one for one-off interface mocks: mock<T>(). Do not hand-roll ad-hoc jest.fn() stub objects for interfaces a Mock class or mock<T>() already covers.
Always reset shared singleton state in afterEach so suites don't leak into each other.
Formatting & naming
| Topic | Decision |
|---|---|
| Indentation | Tabs, enforced by Biome (biome.json) and auto-applied on commit via lint-staged (biome check --write). |
describe |
The unit under test: describe("WindbotModule") or describe("WindbotModule.requestBot()"). |
it |
Behavior, present tense, no "should": it("throws when the token is missing"). |
| No scaffolding | No PR-N / REQ-XXX labels in comments or test names. Comments explain why, not what. |
PR checklist
- Test is co-located in
src/, next to its source. - Shared domain objects built via a Mother; local stubs via
make*factories. - Infra mocked via shared Mock classes /
mock<T>(); singletons reset inafterEach. - Tab indentation;
describe/itfollow the naming convention. npm run lintand the test suite pass.
Migration targets (incremental)
Known consolidation work, done opportunistically as modules are touched:
| Target | Status |
|---|---|
YGOProRoom built two ways |
✅ Consolidated onto YGOProRoomMother. |
Mothers & mocks under tests/ |
✅ Moved to src/test-support/. |
| Tab formatting | ✅ Enforced by Biome (biome check --write on pre-commit via lint-staged). |
26 legacy tests in tests/ |
✅ Migrated to co-location; tests/ removed and dropped from jest roots. |