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
- 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