Advanced Patterns & Best Practices
This guide covers common testing patterns, best practices, and advanced techniques for using the mocksEngine effectively.
Test Organization
Shared Tournament Setup
Create reusable tournament setups:
import { mocksEngine } from 'tods-competition-factory';
import { describe, beforeEach, it, expect } from 'vitest';
describe('Tournament Scheduling', () => {
beforeEach(() => {
// Use setState: true for convenience - no need to call setState in each test
mocksEngine.generateTournamentRecord({
drawProfiles: [{ drawSize: 16 }],
venueProfiles: [{ courtsCount: 4 }],
setState: true, // Auto-loads into tournamentEngine
});
});
it('can schedule matches', () => {
// Tournament already loaded - can use engine methods directly
const { matchUps } = tournamentEngine.allCompetitionMatchUps({
inContext: true, // Fully hydrated matchUps for scheduling
nextMatchUps: true,
});
// Test scheduling logic...
});
it('detects scheduling conflicts', () => {
// Tournament already loaded
const { matchUps } = tournamentEngine.allCompetitionMatchUps({
inContext: true, // Required for conflict detection
nextMatchUps: true,
});
const { rowIssues } = tournamentEngine.proConflicts({ matchUps });
// Test conflict detection...
});
});
Factory Functions
Create factory functions for common scenarios:
// test/helpers/tournamentFactory.js
export function createTournamentWithScheduling(options = {}) {
const defaults = {
drawProfiles: [{ drawSize: 16 }],
venueProfiles: [{ courtsCount: 4 }],
startDate: '2024-06-01',
};
return mocksEngine.generateTournamentRecord({
...defaults,
...options,
});
}
export function createDoublesAndSinglesTournament() {
return mocksEngine.generateTournamentRecord({
participantsProfile: { participantsCount: 64 },
drawProfiles: [
{ drawSize: 32, eventType: 'SINGLES' },
{ drawSize: 16, eventType: 'DOUBLES' },
],
});
}
// In tests:
import { createTournamentWithScheduling } from './helpers/tournamentFactory';
test('scheduling test', () => {
const { tournamentRecord } = createTournamentWithScheduling({
drawProfiles: [{ drawSize: 32 }],
});
// ...
});
Testing Draw Structures
Complete Draw Generation
Test a complete draw with qualifying structure. Note that qualifying is a stage within the draw, not a separate draw:
test('generates complete championship draw with qualifying', () => {
const { tournamentRecord, drawIds } = mocksEngine.generateTournamentRecord({
participantsProfile: {
participantsCount: 128,
sex: 'FEMALE',
category: { categoryName: 'Open', ratingType: 'WTN' },
scaleAllParticipants: true,
},
drawProfiles: [
{
drawSize: 64,
drawName: "Women's Singles Championship",
seedsCount: 16,
qualifiersCount: 8, // 8 positions for qualifiers
qualifyingProfiles: [
{
roundTarget: 1, // Qualifiers enter round 1 of main draw
structureProfiles: [
{
stageSequence: 1,
drawSize: 16, // 16 players compete for 8 spots
seedsCount: 4,
},
],
},
],
completionGoal: 40, // Complete 40 matchUps total
},
],
});
tournamentEngine.setState(tournamentRecord);
const { matchUps } = tournamentEngine.allTournamentMatchUps();
const qualifyingMatches = matchUps.filter((m) => m.stage === 'QUALIFYING');
const mainDrawMatches = matchUps.filter((m) => m.stage === 'MAIN');
expect(qualifyingMatches.length).toBe(8); // 16 players = 8 matches
expect(mainDrawMatches.length).toBe(63); // 64-draw = 63 matches
expect(drawIds.length).toBe(1); // Single draw with qualifying stage
});
Testing Playoff Structures
test('generates playoffs for positions', () => {
const { tournamentRecord } = mocksEngine.generateTournamentRecord({
drawProfiles: [
{
drawSize: 16,
withPlayoffs: {
roundProfiles: [{ 4: 1 }], // Playoffs from round 4
playoffPositions: [3, 4], // 3rd/4th place playoff
playoffAttributes: {
'0-4': { name: 'Bronze Medal Match', abbreviation: 'BM' },
},
},
},
],
});
tournamentEngine.setState(tournamentRecord);
const { drawDefinition } = tournamentEngine.getEvent();
const playoffStructures = drawDefinition.structures.filter((s) => s.stage === 'PLAY_OFF');
expect(playoffStructures.length).toBeGreaterThan(0);
});
Recursive Playoff Structures (COMPASS-like Topologies)
The withPlayoffs parameter supports recursive nesting via roundPlayoffs. This enables building multi-level playoff trees — such as a full COMPASS draw — in a single generateTournamentRecord call:
test('generates full COMPASS topology via recursive withPlayoffs', () => {
const { tournamentRecord } = mocksEngine.generateTournamentRecord({
drawProfiles: [
{
drawSize: 32,
drawName: 'East',
withPlayoffs: {
roundProfiles: [{ 1: 1 }, { 2: 1 }, { 3: 1 }],
playoffAttributes: {
'0-1': { name: 'West', abbreviation: 'W' },
'0-2': { name: 'North', abbreviation: 'N' },
'0-3': { name: 'Northeast', abbreviation: 'NE' },
},
roundPlayoffs: {
1: {
// Sub-playoffs from West's losers
roundProfiles: [{ 1: 1 }, { 2: 1 }],
playoffAttributes: {
'0-1': { name: 'South', abbreviation: 'S' },
'0-2': { name: 'Southwest', abbreviation: 'SW' },
},
roundPlayoffs: {
1: {
// Sub-playoffs from South's losers
roundProfiles: [{ 1: 1 }],
playoffAttributes: {
'0-1': { name: 'Southeast', abbreviation: 'SE' },
},
},
},
},
2: {
// Sub-playoffs from North's losers
roundProfiles: [{ 1: 1 }],
playoffAttributes: {
'0-1': { name: 'Northwest', abbreviation: 'NW' },
},
},
},
},
},
],
});
tournamentEngine.setState(tournamentRecord);
const { drawDefinition } = tournamentEngine.getEvent();
// Full COMPASS: 8 structures, 7 LOSER links, 72 matchUps
expect(drawDefinition.structures.length).toBe(8);
expect(drawDefinition.links.length).toBe(7);
const { matchUps } = tournamentEngine.allTournamentMatchUps();
expect(matchUps.length).toBe(72);
});
The roundPlayoffs field maps a source round number to a child WithPlayoffsArgs object. Each child can itself contain roundPlayoffs, enabling arbitrarily deep trees. See Custom Playoff Topologies and the withPlayoffs API reference for the full type definition and more examples.
Testing Scheduling Scenarios
Auto-Scheduling with Conflict Detection
test('detects participant conflicts in scheduling', () => {
const { tournamentRecord } = mocksEngine.generateTournamentRecord({
participantsProfile: { participantsCount: 32 },
drawProfiles: [
{ drawSize: 16, eventType: 'SINGLES', idPrefix: 'singles' },
{ drawSize: 8, eventType: 'DOUBLES', idPrefix: 'doubles' },
],
venueProfiles: [{ courtsCount: 5 }],
});
tournamentEngine.setState(tournamentRecord);
// Schedule all matches
let { matchUps } = tournamentEngine.allCompetitionMatchUps({
inContext: true,
nextMatchUps: true,
});
const result = tournamentEngine.proAutoSchedule({
scheduledDate: '2024-06-01',
matchUps,
});
expect(result.success).toBe(true);
// Verify no conflicts
({ matchUps } = tournamentEngine.allCompetitionMatchUps({
inContext: true,
nextMatchUps: true,
matchUpFilters: { scheduledDate: '2024-06-01' },
}));
const { rowIssues } = tournamentEngine.proConflicts({ matchUps });
const conflicts = Object.values(rowIssues)
.flat()
.filter((issue) => issue.issue === 'CONFLICT');
expect(conflicts.length).toBe(0);
});
Time-based Scheduling
test('schedules matches with time slots', () => {
const { tournamentRecord, venueIds } = mocksEngine.generateTournamentRecord({
drawProfiles: [{ drawSize: 16 }],
venueProfiles: [
{
courtsCount: 4,
startTime: '08:00',
endTime: '20:00',
},
],
});
tournamentEngine.setState(tournamentRecord);
const { matchUps } = tournamentEngine.allTournamentMatchUps();
matchUps.slice(0, 4).forEach((matchUp, index) => {
const scheduledTime = `${8 + index * 2}:00`;
tournamentEngine.addMatchUpScheduleItems({
matchUpId: matchUp.matchUpId,
drawId: matchUp.drawId,
schedule: {
scheduledDate: '2024-06-01',
scheduledTime,
},
});
});
const { dateMatchUps } = tournamentEngine.competitionScheduleMatchUps({
matchUpFilters: { scheduledDate: '2024-06-01' },
});
expect(dateMatchUps.length).toBe(4);
expect(dateMatchUps[0].schedule.scheduledTime).toBeDefined();
});
Testing Match Completion
Progressive Completion
Test draw advancement through rounds:
test('advances draw through rounds', () => {
const { tournamentRecord } = mocksEngine.generateTournamentRecord({
drawProfiles: [{ drawSize: 8, idPrefix: 'match' }],
});
tournamentEngine.setState(tournamentRecord);
// Complete first round
let { matchUps } = tournamentEngine.allTournamentMatchUps();
const firstRoundMatches = matchUps.filter((m) => m.roundNumber === 1);
firstRoundMatches.forEach((matchUp) => {
const { outcome } = mocksEngine.generateOutcome({
matchUpFormat: matchUp.matchUpFormat,
winningSide: 1,
});
tournamentEngine.setMatchUpStatus({
matchUpId: matchUp.matchUpId,
drawId: matchUp.drawId,
outcome,
});
});
// Verify second round is ready
({ matchUps } = tournamentEngine.allTournamentMatchUps());
const secondRoundMatches = matchUps.filter((m) => m.roundNumber === 2);
expect(secondRoundMatches.every((m) => m.sides.every((s) => s.participantId))).toBe(true);
});
Different Match Outcomes
Test various outcome scenarios:
test('handles various match outcomes', () => {
const { tournamentRecord } = mocksEngine.generateTournamentRecord({
drawProfiles: [
{
drawSize: 8,
outcomes: [
{ roundNumber: 1, roundPosition: 1, matchUpStatus: 'COMPLETED', scoreString: '6-4 6-2', winningSide: 1 },
{ roundNumber: 1, roundPosition: 2, matchUpStatus: 'RETIRED', winningSide: 1 },
{ roundNumber: 1, roundPosition: 3, matchUpStatus: 'WALKOVER', winningSide: 2 },
{ roundNumber: 1, roundPosition: 4, matchUpStatus: 'DEFAULTED', winningSide: 1 },
],
},
],
});
tournamentEngine.setState(tournamentRecord);
const { completedMatchUps } = tournamentEngine.tournamentMatchUps();
expect(completedMatchUps.length).toBe(4);
expect(completedMatchUps.map((m) => m.matchUpStatus).sort()).toEqual([
'COMPLETED',
'DEFAULTED',
'RETIRED',
'WALKOVER',
]);
});
Testing Participant Scenarios
Entry Status Testing
test('manages alternates and direct acceptances', () => {
const { tournamentRecord } = mocksEngine.generateTournamentRecord({
participantsProfile: { participantsCount: 50 },
drawProfiles: [
{
drawSize: 32,
// 32 get DIRECT_ACCEPTANCE, 18 remain as potential alternates
},
],
});
tournamentEngine.setState(tournamentRecord);
const { participants } = tournamentEngine.getParticipants();
const { event } = tournamentEngine.getEvent();
const { entries } = event;
const directAcceptance = entries.filter((e) => e.entryStatus === 'DIRECT_ACCEPTANCE');
const remaining = participants.length - directAcceptance.length;
expect(directAcceptance.length).toBe(32);
expect(remaining).toBe(18);
});
Seeding Tests
test('seeds participants by rating', () => {
const { tournamentRecord } = mocksEngine.generateTournamentRecord({
participantsProfile: {
participantsCount: 64,
category: { categoryName: 'Open', ratingType: 'WTN' },
scaleAllParticipants: true,
},
drawProfiles: [
{
drawSize: 32,
seedsCount: 8,
},
],
});
tournamentEngine.setState(tournamentRecord);
const { seedAssignments } = tournamentEngine.getEvent();
expect(Object.keys(seedAssignments).length).toBe(8);
// Verify top seeds are in expected positions
const { positionAssignments } = tournamentEngine.getPositionAssignments();
const topSeedPosition = positionAssignments.find((pa) => pa.seedNumber === 1);
expect(topSeedPosition.drawPosition).toBe(1);
});
Testing Team Events
Team Creation from Attributes
test('creates teams from participant attributes', () => {
const { tournamentRecord } = mocksEngine.generateTournamentRecord({
participantsProfile: {
participantsCount: 64,
teamKey: 'person.addresses[0].state', // Group by state
addressProps: {
statesProfile: {
CA: 16,
TX: 16,
NY: 16,
FL: 16,
},
},
},
drawProfiles: [
{
drawSize: 4,
eventType: 'TEAM',
},
],
});
tournamentEngine.setState(tournamentRecord);
const { participants } = tournamentEngine.getParticipants({
participantFilters: { participantTypes: ['TEAM'] },
});
expect(participants.length).toBe(4);
participants.forEach((team) => {
expect(team.individualParticipantIds.length).toBeGreaterThan(0);
});
});
Understanding inContext: Hydrated vs Basic MatchUps
A critical concept when working with matchUps is the difference between basic and fully hydrated matchUps.
Basic MatchUps (inContext: false or omitted)
const { matchUps } = tournamentEngine.allTournamentMatchUps();
// Basic matchUp contains:
{
matchUpId: 'abc-123',
roundNumber: 1,
roundPosition: 1,
sides: [
{ participantId: 'player-1' }, // Only ID, not full participant
{ participantId: 'player-2' }
],
// Missing: event details, participant details, venue info, etc.
}
Fully Hydrated MatchUps (inContext: true)
const { matchUps } = tournamentEngine.allTournamentMatchUps({
inContext: true, // Fully hydrate with contextual data
});
// Hydrated matchUp contains everything from basic, PLUS:
{
// ... basic fields ...
// Event context
eventName: 'Singles Championship',
eventType: 'SINGLES',
gender: 'FEMALE',
category: { categoryName: 'U18' },
// Draw context
drawName: 'Main Draw',
drawType: 'SINGLE_ELIMINATION',
stage: 'MAIN',
structureName: 'Main',
roundName: 'Round of 16',
// Full participant details
sides: [
{
participantId: 'player-1',
participant: {
participantName: 'Jane Doe',
person: {
standardGivenName: 'Jane',
standardFamilyName: 'Doe',
nationalityCode: 'USA',
// ... full person details
},
// ... rankings, ratings, etc.
}
},
// ... side 2 with full details
],
// Scheduling context (if scheduled)
schedule: {
venueId: 'venue-1',
venueName: 'Main Stadium',
venueAbbreviation: 'MS',
courtId: 'court-1',
courtName: 'Center Court',
scheduledDate: '2024-06-01',
scheduledTime: '10:00',
},
// Potential participants for future rounds
potentialParticipants: [[...], [...]],
// Dependency information
winnerTo: { /* next matchUp info */ },
loserTo: { /* consolation matchUp info */ },
}
When inContext is REQUIRED
Certain operations require inContext: true:
1. Scheduling Operations
// ❌ WRONG: Will fail or produce incorrect results
const { matchUps } = tournamentEngine.allCompetitionMatchUps();
tournamentEngine.proAutoSchedule({ matchUps, scheduledDate: '2024-06-01' });
// ✅ CORRECT: Scheduling needs participant context
const { matchUps } = tournamentEngine.allCompetitionMatchUps({
inContext: true,
nextMatchUps: true, // Also needed for dependency info
});
tournamentEngine.proAutoSchedule({ matchUps, scheduledDate: '2024-06-01' });
2. Conflict Detection
// ❌ WRONG: Can't detect participant conflicts without context
const { matchUps } = tournamentEngine.allCompetitionMatchUps();
const { rowIssues } = tournamentEngine.proConflicts({ matchUps });
// ✅ CORRECT: Needs participant details to detect conflicts
const { matchUps } = tournamentEngine.allCompetitionMatchUps({
inContext: true,
nextMatchUps: true,
});
const { rowIssues } = tournamentEngine.proConflicts({ matchUps });
3. Display/Reporting
// ❌ WRONG: Can't display names, only IDs
const { matchUps } = tournamentEngine.allTournamentMatchUps();
console.log(matchUps[0].sides[0].participantId); // Just an ID
// ✅ CORRECT: Has full names and details for display
const { matchUps } = tournamentEngine.allTournamentMatchUps({
inContext: true,
});
console.log(matchUps[0].sides[0].participant.participantName); // "Jane Doe"
Performance Considerations
// For large datasets, consider performance tradeoff
test('performance-critical operation', () => {
// ❌ SLOW: Hydrating 1000+ matchUps is expensive
const { matchUps } = tournamentEngine.allTournamentMatchUps({
inContext: true,
});
// Just checking IDs
const matchUpIds = matchUps.map((m) => m.matchUpId);
// ✅ FASTER: Only get what you need
const { matchUps: basicMatchUps } = tournamentEngine.allTournamentMatchUps();
const matchUpIds = basicMatchUps.map((m) => m.matchUpId);
// ✅ BEST: Get full context only when needed
const matchUpId = basicMatchUps[0].matchUpId;
const { matchUp } = tournamentEngine.findMatchUp({
matchUpId,
inContext: true, // Hydrate just this one
});
});
Best Practice Pattern
test('efficient matchUp operations', () => {
mocksEngine.generateTournamentRecord({
drawProfiles: [{ drawSize: 32 }],
setState: true,
});
// Phase 1: Find what you need (fast, no hydration)
const { matchUps } = tournamentEngine.allTournamentMatchUps();
const firstRoundMatches = matchUps.filter((m) => m.roundNumber === 1);
// Phase 2: Get full details only for what you're using
const { matchUps: hydratedMatches } = tournamentEngine.allTournamentMatchUps({
inContext: true,
matchUpFilters: {
roundNumbers: [1], // Only hydrate first round
},
});
// Now work with fully hydrated matchUps
hydratedMatches.forEach((matchUp) => {
console.log(`${matchUp.sides[0].participant.participantName} vs ${matchUp.sides[1].participant.participantName}`);
});
});
Debugging Patterns
Use ID Prefixes
Make debugging easier with meaningful prefixes:
test('debug with prefixes', () => {
const { tournamentRecord } = mocksEngine.generateTournamentRecord({
participantsProfile: {
participantsCount: 32,
idPrefix: 'player',
},
drawProfiles: [
{
drawSize: 16,
idPrefix: 'match',
},
],
});
tournamentEngine.setState(tournamentRecord);
const { matchUps } = tournamentEngine.allTournamentMatchUps();
// Console output will show: match-1-1, match-1-2, etc.
console.log(matchUps[0].matchUpId);
// And: player-I-0, player-I-1, etc.
console.log(matchUps[0].sides[0].participantId);
});
DevContext for Detailed Errors
test('with devContext for debugging', () => {
mocksEngine.devContext(true);
const { tournamentRecord } = mocksEngine.generateTournamentRecord({
drawProfiles: [{ drawSize: 16 }],
});
tournamentEngine.devContext(true).setState(tournamentRecord);
// Now get detailed error messages for any issues
const result = tournamentEngine.setMatchUpStatus({
matchUpId: 'invalid-id',
outcome: {},
});
// Detailed error information available
expect(result.error).toBeDefined();
});
Integration Testing
Full Tournament Lifecycle
test('complete tournament lifecycle', () => {
// 1. Generate tournament
const { tournamentRecord, eventIds, venueIds } = mocksEngine.generateTournamentRecord({
drawProfiles: [{ drawSize: 8 }],
venueProfiles: [{ courtsCount: 3 }],
});
// 2. Load into engine
tournamentEngine.setState(tournamentRecord);
// 3. Schedule matches
const { matchUps } = tournamentEngine.allCompetitionMatchUps({
inContext: true,
nextMatchUps: true,
});
const scheduleResult = tournamentEngine.proAutoSchedule({
scheduledDate: '2024-06-01',
matchUps,
});
expect(scheduleResult.success).toBe(true);
// 4. Complete first round
const { matchUps: scheduled } = tournamentEngine.allTournamentMatchUps();
const firstRound = scheduled.filter((m) => m.roundNumber === 1);
firstRound.forEach((matchUp) => {
const { outcome } = mocksEngine.generateOutcome();
tournamentEngine.setMatchUpStatus({
matchUpId: matchUp.matchUpId,
drawId: matchUp.drawId,
outcome,
});
});
// 5. Verify progression
const { upcomingMatchUps } = tournamentEngine.tournamentMatchUps();
const secondRoundReady = upcomingMatchUps.filter((m) => m.roundNumber === 2 && m.sides.every((s) => s.participantId));
expect(secondRoundReady.length).toBeGreaterThan(0);
// 6. Export and verify
const { tournamentRecord: final } = tournamentEngine.getTournament();
expect(final.events[0].drawDefinitions[0].structures[0].matchUps).toBeDefined();
});
Best Practices Summary
- Use setState: true: Auto-load tournaments into engine for convenience
- Use inContext: true: When you need full participant details, scheduling, or conflict detection
- Understand Performance: Use
inContext: falsefor large datasets, true only when needed - Reuse Tournament Structures: Generate once, test multiple scenarios
- Use Factory Functions: Create helper functions for common setups
- Add ID Prefixes: Make debugging easier with meaningful IDs
- Enable DevContext: Get detailed errors during development
- Test Edge Cases: Use matchUpStatusProfile for various outcomes
- Minimize Generation: Don't regenerate unnecessarily in loops
- Fixed Values for Snapshots: Use fixed dates/IDs for snapshot testing
- Test Complete Flows: Integrate generation with engine operations
- Organize Tests Logically: Group related tests, use shared setup
- Document Complex Scenarios: Add comments explaining non-obvious test setups
- Phase Your Operations: Get basic data first, hydrate only what you need
Next Steps
- Tournament Generation - Complete tournament generation options
- Participant Generation - Types and demographics
- Outcome Generation - Match results and scores