Skip to main content

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);
});

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

  1. Use setState: true: Auto-load tournaments into engine for convenience
  2. Use inContext: true: When you need full participant details, scheduling, or conflict detection
  3. Understand Performance: Use inContext: false for large datasets, true only when needed
  4. Reuse Tournament Structures: Generate once, test multiple scenarios
  5. Use Factory Functions: Create helper functions for common setups
  6. Add ID Prefixes: Make debugging easier with meaningful IDs
  7. Enable DevContext: Get detailed errors during development
  8. Test Edge Cases: Use matchUpStatusProfile for various outcomes
  9. Minimize Generation: Don't regenerate unnecessarily in loops
  10. Fixed Values for Snapshots: Use fixed dates/IDs for snapshot testing
  11. Test Complete Flows: Integrate generation with engine operations
  12. Organize Tests Logically: Group related tests, use shared setup
  13. Document Complex Scenarios: Add comments explaining non-obvious test setups
  14. Phase Your Operations: Get basic data first, hydrate only what you need

Next Steps