DEV Community

Cover image for How to Test “User Is in Crisis” Without Treating Humans Like Mock Objects
CrisisCore-Systems
CrisisCore-Systems

Posted on • Edited on

How to Test “User Is in Crisis” Without Treating Humans Like Mock Objects

Look, I’ve been building systems that need to work when people are falling apart.

You can’t write a test for “user is having a breakdown.” But you can test whether your code notices the warning signs and actually helps.

This is my playbook for testing crisis features without turning humans into mock objects.


The Problem: You Can’t Mock Human Suffering

Unit tests verify code behavior. Trauma-informed features respond to human patterns breaking down in real time.

You can’t write this:


ts
// ❌ Doesn't exist
test('user having panic attack triggers support mode', () => {
  const user = simulatePanicAttack();
  expect(getSupportMode()).toBe(true);
});

But you can write this:

// ✅ Test the signals, not the human
test('error bursts plus help requests trigger escalation', () => {
  for (let i = 0; i < 8; i++) crisis.trackError();
  crisis.trackHelpRequest();
  crisis.trackHelpRequest();

  expect(crisis.level).toBe('moderate');
});

Rule one: test your interpretation of observable signals. The human experience stays human.


---

Strategy: Inject States, Don’t Simulate Trauma

Instead of faking distress, inject the system state that distress would change.

function TestWrapper({ children, crisis = {}, prefs = {} }) {
  return (
    <CrisisContext.Provider value={{ ...defaultCrisis, ...crisis }}>
      <PrefsContext.Provider value={{ ...defaultPrefs, ...prefs }}>
        {children}
      </PrefsContext.Provider>
    </CrisisContext.Provider>
  );
}

Now you can test preference combinations safely:

it('high sensitivity catches subtle patterns', () => {
  render(
    <TestWrapper prefs={{ sensitivity: 'high' }}>
      <CrisisDetector />
    </TestWrapper>
  );

  expect(screen.getByTestId('threshold')).toHaveTextContent('6');
});

Don’t simulate fog. Inject the config your app would use during fog.


---

Generate Realistic Patterns, Not Random Noise

Your detection logic looks for patterns. Your test data better have patterns too.

function generatePainSeries(days, trend = 'stable') {
  const entries = [];

  for (let i = 0; i < days; i++) {
    let intensity;

    switch (trend) {
      case 'worsening':
        intensity = Math.min(10, 3 + (i / days) * 6);
        break;
      case 'improving':
        intensity = Math.max(1, 9 - (i / days) * 5);
        break;
      case 'chaotic':
        intensity = Math.max(1, Math.min(10, 5 + Math.sin(i * 0.8) * 4));
        break;
      default:
        intensity = 5;
    }

    // NOTE: keep randomness deterministic if you want stable tests (seeded RNG)
    entries.push({
      date: new Date(Date.now() - (days - i) * 86400000),
      pain: Math.round(intensity),
      mood: intensity > 7 ? 2 : 7,
    });
  }

  return entries;
}

Then test with realistic trajectories:

test('catches worsening trends before they crater', () => {
  const data = generatePainSeries(14, 'worsening');
  const analysis = analyzePattern(data);

  expect(analysis.trend).toBe('deteriorating');
  expect(analysis.shouldAlert).toBe(true);
});

Make your test data look like sessions, not academic examples.


---

Assert Outcomes, Not Internal Labels

Don’t test “system thinks user is stressed.” Test “system helps stressed user.”

test('emergency mode simplifies the interface outcomes', () => {
  const { result } = renderHook(() => useCrisisMode(), { wrapper: TestWrapper });

  act(() => result.current.activate('emergency'));

  expect(result.current.prefs.touchTargets).toBe('large');
  expect(result.current.prefs.confirmations).toBe('high');
  expect(result.current.prefs.motion).toBe('reduced');
  expect(result.current.prefs.contrast).toBe('high');
});

The question isn’t “did we detect crisis correctly?”
It’s “if someone’s in crisis, does this help?”


---

Time Matters: Use Fake Timers

Crisis detection happens over time: bursts, cooldowns, recovery windows.

describe('crisis timing', () => {
  beforeEach(() => vi.useFakeTimers());
  afterEach(() => vi.useRealTimers());

  test('requires sustained problems before escalating', () => {
    const { result } = renderHook(() => useCrisisDetection());

    // burst shouldn't escalate
    act(() => {
      for (let i = 0; i < 5; i++) result.current.trackError();
    });
    vi.advanceTimersByTime(5000);
    expect(result.current.level).toBe('none');

    // sustained pattern should
    act(() => {
      for (let i = 0; i < 8; i++) {
        result.current.trackError();
        vi.advanceTimersByTime(15000);
      }
    });

    expect(result.current.level).not.toBe('none');
  });

  test('backs off after recovery period', () => {
    const { result } = renderHook(() => useCrisisDetection());

    act(() => result.current.forceLevel('moderate'));
    vi.advanceTimersByTime(600000); // 10 minutes

    expect(result.current.level).toBe('none');
  });
});

Test activation. Test deactivation.
Apps that never calm down become part of the problem.


---

One More Thing: Test False Positives (Because “Help” Can Become Harm)

If your system escalates too easily, you create noise, anxiety, or UI friction. So I keep tests like:

“one bad minute shouldn’t trigger emergency mode”

“random flailing shouldn’t outrank a stable recovery trend”

“after recovery, don’t re-trigger instantly (hysteresis)”


This is where you prevent the system from becoming the boy who cried wolf.


---

Keep One Manual Dashboard

Unit tests catch regressions. Humans catch when something feels wrong.

I keep a quick crisis simulator dev tool to trigger states and visually verify the actual UI changes:

does simplified mode actually feel simpler?

are large touch targets actually easier to hit?

does emergency UI feel supportive, not alarming?


This isn’t automation. It’s a sanity check that the system still behaves like it gives a damn.


---

What Usually Goes Wrong

Testing emotional labels instead of behavioral signals and UI outcomes

Hardcoding thresholds instead of testing pattern recognition

Mocking the detection system until you’ve mocked away the actual risk

Only testing activation, never recovery

Assuming synchronous updates when you have debouncing and timeouts



---

Testing Hierarchy

Bottom: fixtures and generators for realistic scenarios
Middle: signal interpretation and threshold logic
Top: manual validation that this actually helps humans

Automated tests keep it mechanically sound. Human review keeps it humane.


---

The honest truth? You can’t fully automate empathy. But you can make your crisis detection reliable, your UI adaptations consistent, and your recovery paths safe.

When someone’s world is falling apart, your code better work the first time.

Next: what happens when the network dies during a crisis.


---

If you're struggling: In Canada, 9-8-8. In the US, 988.

Question: What do you test when building adaptive UX — signals, outcomes, false positives, or the full end-to-end session?
Enter fullscreen mode Exit fullscreen mode

Top comments (0)