Automatic Testing

This guide covers automated testing of the mortgage system, focusing on the mocking system for controlling integration responses and test personas for consistent, repeatable test scenarios.

Overview

Building a testing rig follows the same patterns as building a channel (like the customer-facing frontend). You interact with the API to start processes, poll for tasks, and complete them. The Frontend documentation covers these patterns in detail.

For testing, you add:

  1. Mocking system - Control third-party integration responses
  2. Test personas - Pre-defined individuals with aligned mock data

Writing Tests

Automated tests verify that the process behaves correctly given specific inputs. The goal is to ensure that:

  • Different personas produce different outcomes - An applicant with good credit should reach an offer, while one with poor credit should be declined
  • Tasks appear in the correct order - The flow progresses through expected steps
  • Task completion advances the process - Completing a task triggers the next step
  • Edge cases are handled - Unusual data combinations don't break the flow

A typical test starts a process with specific inputs, progresses through tasks, and asserts that the flow arrives at expected states.

Basic Test Structure

test('applicant with good credit reaches offer task', async () => {
  // Start process with a persona
  const { flowId } = await startProcess({
    persona: goodCreditPersona,
    purpose: 'refinance'
  })

  // Complete tasks until we reach the expected state
  await completeTasksUntil(flowId, 'offer-task')

  // Assert we arrived at the expected task
  const tasks = await getPendingTasks(flowId)
  expect(tasks[0].taskType).toBe('offer-task')
})

Testing Different Paths

test('applicant with poor credit is declined', async () => {
  const { flowId } = await startProcess({
    persona: poorCreditPersona,
    purpose: 'refinance'
  })

  await completeTasksUntil(flowId, 'decline-task')

  const tasks = await getPendingTasks(flowId)
  expect(tasks[0].taskType).toBe('decline-task')
})

Testing Task Completion

test('completing income verification progresses to next task', async () => {
  const { flowId } = await startProcess({ persona: testPersona })

  // Wait for specific task
  const incomeTask = await waitForTask(flowId, 'verify-income')

  // Complete with specific input
  await completeTask(incomeTask.taskId, {
    decision: 'approved'
  })

  // Assert next task appears
  const nextTask = await waitForTask(flowId, 'verify-assets')
  expect(nextTask).toBeDefined()
})

Helper Functions

async function startProcess({ persona, purpose }) {
  return fetch('/api/flow-definitions/mortgage', {
    method: 'POST',
    body: JSON.stringify({
      purpose,
      households: [{
        stakeholders: [{
          nationalId: persona.nationalId,
          role: 'applicant'
        }]
      }]
    })
  }).then(r => r.json())
}

async function waitForTask(flowId, taskType) {
  while (true) {
    const tasks = await getPendingTasks(flowId)
    const task = tasks.find(t => t.taskType === taskType)
    if (task) return task
    await sleep(1000)
  }
}

async function completeTasksUntil(flowId, targetTaskType) {
  while (true) {
    const tasks = await getPendingTasks(flowId)
    if (tasks[0]?.taskType === targetTaskType) return
    if (tasks[0]) {
      await completeTask(tasks[0].taskId, {})
    }
    await sleep(1000)
  }
}

Mocking System

The mocking system controls responses from third-party integrations, allowing tests to run without hitting real external services.

How It Works

Each integration has mock files containing request/response pairs. When a request is made, the system matches against identifiers in the request to find the appropriate mock response.

{
  "/endpoint": {
    "POST": [
      {
        "search": { "identifier": "value-1" },
        "responseBody": { "name": "Person One" }
      },
      {
        "search": { "identifier": "value-2" },
        "responseBody": { "name": "Person Two" }
      }
    ]
  }
}

Matching Identifiers

Integrations use identifiers from the request to match against mock entries. The specific identifier fields vary by integration type and solution configuration.

Mix and Match

The mocking system allows mixing mocked and real requests within the same process instance. If a matching mock entry exists for an identifier, the mock is used. Otherwise, the real integration is called.

This enables:

  • Testing specific integration failures
  • Creating edge-case data scenarios
  • Running partial integration tests

Test Personas

Test personas are pre-defined individuals with aligned mock data across all integrations.

Why Personas?

  • Consistency - All mocks return coherent data (income matches tax records, properties match registry)
  • Repeatability - Same persona always produces same results
  • Scenario coverage - Different personas test different paths (high/low income, good/poor credit)

Data Alignment

When you start a process with a persona's identifier, all integration lookups return that persona's aligned data:

  • Party lookup returns their personal information
  • Income service returns their registered income
  • Credit scoring returns their credit grade
  • Property registry returns their property details
  • Debt registry returns their existing obligations

Available Personas

See the Manual Testing page for the complete list of available personas for your solution.