Write Fewer Tests!

Model-Based Testing in React

Let's fix this

And this

And this

And this

Requirements

Code

Tests

Lines of Code

Time

Lots of Unit Tests

Verify that you are a developer that knows how to code

What if tests can be

generated

What if tests can be

generated

without being

written?

What if tests can be

re

without being

written?

generated

What if tests can be

re

without being

written?

generated

re

Model Based Testing

Automatic Test Generation

From 3 and 5 gallon jugs, how can you

Measure 4 gallons exactly?

❓❓

❓❓❓ three

❓❓❓❓❓ five

💧💧💧 three

❓❓❓❓❓ five

❓❓❓ three

💧💧💧❓❓ five

💧💧💧 three

💧💧💧❓❓five

💧❓❓ three

💧💧💧💧💧 five

💧❓❓ three

❓❓❓❓❓ five

❓❓❓ three

💧❓❓❓❓ five

💧💧💧 three

💧❓❓❓❓ five

❓❓❓ three

💧💧💧💧❓ five ✅

Finite state machines

Idle

Every app starts in

some initial state

Idle

There can be a finite
number of states

Loading

Success

Failure

Idle

State can transition

to each other

Success

Failure

Loading

Idle

Events cause states

to transition

Success

Failure

Loading

LOAD

RESOLVE

REJECT

Idle

Loading

💥

0, 0

0, 5

3, 0

3, 5

3, 2

0, 2

2, 0

2, 5

3, 4

3, 3

1, 0

1, 5

0, 1

3, 1

0, 4

0, 3

0, 0

0, 5

3, 2

0, 2

2, 0

2, 5

3, 4

Requirements

Given [precondition]

when [action]

then [postcondition]

Requirements

Abstract Model

Given [precondition]

precondition

postcondition

action

when [action]

then [postcondition]

Requirements

Abstract Model

Given [precondition]

precondition

postcondition

action

when [action]

then [postcondition]

Executable Tests

🔍

🔍

💥

1. Create a model

1. Create a model

2. Generate abstract tests

1. Create a model

2. Generate abstract tests

3. Make them real

🖱

⌨️

💥

Precondition

Postcondition

1. Create a model

2. Generate abstract tests

3. Make them real

4. Execute the tests

1. Create a model

2. Generate abstract tests

3. Make them real

4. Execute the tests

Model regenerates tests

5. Profit (seriously)

How was your experience?

𝘅

Good

Bad

Tell me why?

𝘅

Submit

Thanks for your feedback.

𝘅

Close

Ain't nothin' but a heartache

Loading...
(concurrent mode would have fixed this)

🐙

import { render, fireEvent, cleanup } from '@testing-library/react';
// ...

describe('feedback app', () => {
  afterEach(cleanup);

  it('should show the thanks screen when "Good" is clicked', () => {
    const { getByTestId } = render(<Feedback />);

    // The question screen should be visible at first
    assert.ok(getByTestId('question-screen'));

    // Click the "Good" button
    fireEvent.click(getByTestId('good-button'));

    // Now the thanks screen should be visible
    assert.ok(getByTestId('thanks-screen'));
  });

  it('should show the form screen when "Bad" is clicked', () => {
    const { getByTestId } = render(<Feedback />);

    // The question screen should be visible at first
    assert.ok(getByTestId('question-screen'));

    // Click the "Bad" button
    fireEvent.click(getByTestId('bad-button'));

    // Now the form screen should be visible
    assert.ok(getByTestId('form-screen'));
  });
});
import { createMachine } from 'xstate';

const feedbackMachine = createMachine({
  id: 'feedback',
  initial: 'question',
  states: {
    question: {
      on: {
        CLICK_GOOD: 'thanks',
        CLICK_BAD: 'form',
        CLOSE: 'closed'
      }
    },
    form: {
      on: {
        SUBMIT: 'thanks',
        CLOSE: 'closed'
      }
    },
    thanks: {
      on: {
        CLOSE: 'closed'
      }
    },
    closed: {
      type: 'final'
    }
  }
});

question

thanks

form

closed

CLICK_GOOD

CLICK_BAD

SUBMIT

CLOSE

CLOSE

CLOSE

A

B

C

D

E

Shortest paths

A

B

C

D

E

Shortest paths

A → C → E

A

B

C

D

E

Shortest paths

A → B → D → E

Weighted: Dijkstra's algorithm

A

B

C

D

E

Simple paths

A → C → E

A

B

C

D

E

Simple paths

A → C → E

A → B → D → E

A

B

C

D

E

Simple paths

A → C → E

A → B → D → E

A → B → C → E

A

B

C

D

E

Simple paths

A → C → E

A → B → D → E

A → B → C → E

A → C → B → D → E 

import { getSimplePaths } from '@xstate/graph';
import { feedbackMachine } from '../path/to/feedbackMachine';

const shortestPaths = getShortestPaths(feedbackMachine);

question

question

form

question → CLICK_BAD form

thanks

question → CLICK_BAD form SUBMIT → thanks

question → CLICK_GOOD → thanks

closed

question → CLOSE→ closed

question → CLICK_BAD → form → CLOSE → closed

question → CLICK_GOOD → thanks → CLOSE → closed

question → CLICK_BAD → form → SUBMIT → thanks → CLOSE → closed

import { createMachine } from 'xstate';
import { createModel } from '@xstate/test';
import { render, fireEvent, cleanup } from '@testing-library/react';

const feedbackMachine = createMachine({
  id: 'feedback',
  initial: 'question',
  states: {
    question: {
      on: {
        CLICK_GOOD: 'thanks',
        CLICK_BAD: 'form',
        CLOSE: 'closed'
      },
      meta: {
        test: ({ getByTestId }) => {
          assert.ok(getByTestId('question-screen'));
        }
      }
    },
    form: {
      on: {
        SUBMIT: [
          {
            target: 'thanks',
            cond: (_, e) => e.value.length
          }
        ],
        CLOSE: 'closed'
      },
      meta: {
        test: ({ getByTestId }) => {
          assert.ok(getByTestId('form-screen'));
        }
      }
    },
    thanks: {
      on: {
        CLOSE: 'closed'
      },
      meta: {
        test: ({ getByTestId }) => {
          assert.ok(getByTestId('thanks-screen'));
        }
      }
    },
    closed: {
      type: 'final',
      meta: {
        test: ({ queryByTestId }) => {
          assert.isNull(queryByTestId('thanks-screen'));
        }
      }
    }
  }
});
// ...
import { Machine } from 'xstate';
import { createModel } from '@xstate/test';

const feedbackMachine = Machine(/* ... */);

const feedbackModel = createModel(feedbackMachine, {
  events: {
    CLICK_GOOD: ({ getByText }) => {
      fireEvent.click(getByText('Good'));
    },
    CLICK_BAD: ({ getByText }) => {
      fireEvent.click(getByText('Bad'));
    },
    CLOSE: ({ getByTestId }) => {
      fireEvent.click(getByTestId('close-button'));
    },
    ESC: ({ baseElement }) => {
      fireEvent.keyDown(baseElement, { key: 'Escape' });
    },
    SUBMIT: {
      exec: async ({ getByTestId }, event) => {
        fireEvent.change(getByTestId('response-input'), {
          target: { value: event.value }
        });
        fireEvent.click(getByTestId('submit-button'));
      },
      cases: [
        { type: 'SUBMIT', value: 'something' },
        { type: 'SUBMIT', value: '' }
      ]
    }
  }
});
// ...
import { Machine } from 'xstate';
import { createModel } from '@xstate/test';

const feedbackMachine = Machine(/* ... */);

const feedbackModel = createModel(/* ... */);

describe('feedback app', () => {
  const testPlans = feedbackModel.getShortestPathPlans();

  testPlans.forEach(plan => {
    describe(plan.description, () => {
      afterEach(cleanup);

      plan.paths.forEach(path => {
        it(path.description, () => {
          const rendered = render(<Feedback />);
          return path.test(rendered);
        });
      });
    });
  });
});
// ...
import { Machine } from 'xstate';
import { createModel } from '@xstate/test';

const feedbackMachine = Machine(/* ... */);

const feedbackModel = createModel(/* ... */);

describe('feedback app', () => {
  const testPlans = feedbackModel.getShortestPathPlans();

  testPlans.forEach(/* ... */);
  
  it('should have full coverage', () => {
    return testModel.testCoverage();
  });
});

Let's take this further.

Let's take this further.

Demo 😅

Time-travel debugging

IN THE FUTURE.

So what does this have to do with React?

Absolutely nothing.

TEST

npm install @xstate/test

GraphWalker

HARDER

BETTER

FASTER

STRONGER

Learning curve

Limited tooling

No guarantees

Model might be wrong

(but that's your fault)

Requirements integrity

Test generation

Flexibility

Reduce costs/time

Efficiency

Edge case discovery

Maintenance

Implementation agnostic

xstate
@xstate/react
@xstate/graph
@xstate/vue
@xstate/test
@xstate/fsm
@xstate/analytics
@xstate/viz

🔜

🔜

npm install xstate

@xstate/scxml

🔜

@xstate/immer

🔜

🙂 Other Companies

Make your code

model-driven

generate

tests

docs

prototypes

do more.

Thank you Sydney! 🇦🇺

Write fewer tests! Model-based testing in React

By David Khourshid

Write fewer tests! Model-based testing in React

React Conf AU 2020

  • 8,536