@davidkpiano → React Rally 2019
Let's fix this
And this
And this
And this
Requirements
Code
Tests
LOC
Time
"Write Tests Frequently" (WTF) technique
Red, Green, Refactor
Black and White
Unbreakable code
Human Nature
Lots of Unit Tests
Verify that you are a developer that knows how to code
What if tests can be generated
without being written?
What if tests can be regenerated
without being rewritten?
Automatic Test Generation
❓❓❓ three
❓❓❓❓❓ five
💧💧💧 three
❓❓❓❓❓ five
❓❓❓ three
💧💧💧❓❓ five
💧💧💧 three
💧💧💧❓❓five
💧❓❓ three
💧💧💧💧💧 five
💧❓❓ three
❓❓❓❓❓ five
❓❓❓ three
💧❓❓❓❓ five
💧💧💧 three
💧❓❓❓❓ five
❓❓❓ three
💧💧💧💧❓ five ✅
Loading
Success
Resolve
Initial state
States
Events
Transitions
Final State
Failed
REJECT
RETRY
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
Given [precondition]
precondition
postcondition
action
when [action]
then [postcondition]
🔍
🔍
💥
Kent C. Dots
🖱
⌨️
⏱
💥
Precondition
Postcondition
Model regenerates tests
How was your experience?
𝘅
Good
Bad
Tell me why?
𝘅
Submit
Thanks for your feedback.
𝘅
Close
Ain't nothin' but a heartache
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 { Machine } from 'xstate';
const feedbackMachine = Machine({
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
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 { Machine } from 'xstate';
import { createModel } from '@xstate/test';
import { render, fireEvent, cleanup } from '@testing-library/react';
const feedbackMachine = Machine({
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();
});
});
A
B
C
D
E
A → C → E
A
B
C
D
E
A → B → D → E
Weighted: Dijkstra's algorithm
A
B
C
D
E
A → C → E
A → B → D → E
A → B → C → E
A → C → B → D → E
Try that, Redux.
Absolutely nothing.
TEST
npm install @xstate/test
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/test
@xstate/fsm
@xstate/viz
@xstate/vue
@xstate/patterns
@xstate/analytics
import { useMachine } from '@xstate/machine';
import { someMachine } from '../idk/somewhere';
export const App = () => {
const [current, send] = useMachine(someMachine);
return (/* ... */);
}
import { useReducer } from 'react';
import { someReducer, initial } from '../idk/somewhere';
export const App = () => {
const [state, dispatch] = useReducer(someReducer, initial);
return (/* ... */);
}
Coming soon
Released
@davidkpiano → React Rally 2019