├── .babelrc
├── .editorconfig
├── .eslintignore
├── .eslintrc
├── .gitignore
├── INSTRUCTIONS.md
├── LICENSE
├── README.md
├── exercises-final
├── components
│ ├── Button.js
│ ├── Button.test.js
│ ├── README.md
│ ├── Toggle.js
│ ├── Toggle.test.js
│ └── __snapshots__
│ │ └── Button.test.js.snap
├── containers
│ ├── CustomerList.js
│ ├── CustomerList.test.js
│ ├── README.md
│ └── __snapshots__
│ │ └── CustomerList.test.js.snap
├── jest.config.json
└── store
│ ├── Customers.js
│ ├── Customers.stub.js
│ ├── Customers.test.js
│ └── README.md
├── exercises
├── components
│ ├── Button.js
│ ├── Button.test.js
│ ├── README.md
│ ├── Toggle.js
│ └── Toggle.test.js
├── containers
│ ├── CustomerList.js
│ ├── CustomerList.test.js
│ └── README.md
├── jest.config.json
└── store
│ ├── Customers.js
│ ├── Customers.stub.js
│ ├── Customers.test.js
│ └── README.md
├── package.json
├── scripts
├── install.js
└── verify.js
├── templates
├── components
│ ├── Button.js
│ ├── Button.test.js
│ ├── README.md
│ ├── Toggle.js
│ └── Toggle.test.js
├── containers
│ ├── CustomerList.js
│ ├── CustomerList.test.js
│ └── README.md
├── jest.config.json
└── store
│ ├── Customers.js
│ ├── Customers.stub.js
│ ├── Customers.test.js
│ └── README.md
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "stage-2", "react"],
3 | "env": {
4 | "test": {
5 | "plugins": [
6 | "istanbul"
7 | ]
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig is awesome: http://EditorConfig.org
2 |
3 | # top-most EditorConfig file
4 | root = true
5 |
6 | # all files
7 | [*]
8 | end_of_line = lf
9 | insert_final_newline = true
10 | indent_style = space
11 | indent_size = 2
12 | charset = utf-8
13 | trim_trailing_whitespace = true
14 | max_line_length = 120
15 |
16 | [*.js]
17 | quote_type = single
18 | curly_bracket_next_line = false
19 | spaces_around_operators = true
20 | spaces_around_brackets = inside
21 | indent_brace_style = BSD KNF
22 |
23 | # HTML
24 | [*.html]
25 | quote_type = double
26 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | coverage/
2 |
3 | # these scripts have to work on older versions of node
4 | scripts/install.js
5 | scripts/verify.js
6 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "kentcdodds/best-practices",
4 | "kentcdodds/possible-errors",
5 | "kentcdodds/es6/best-practices",
6 | "kentcdodds/es6/possible-errors",
7 | "kentcdodds/import/best-practices",
8 | "kentcdodds/import/possible-errors",
9 | "kentcdodds/jest",
10 | "kentcdodds/react",
11 | ],
12 | "rules": {
13 | "no-invalid-this": 0,
14 | "global-require": 0,
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | coverage/
3 |
--------------------------------------------------------------------------------
/INSTRUCTIONS.md:
--------------------------------------------------------------------------------
1 | # Instructions
2 |
3 | If you'd like to follow along:
4 |
5 | 1. `$ git clone https://github.com/kentcdodds/react-jest-workshop.git`
6 | 2. `$ cd react-jest-workshop`
7 | 4. `$ npm run setup`
8 |
9 | You'll notice that this repository is already (mostly) set up for a React project.
10 | It's a bit contrived and doesn't actually amount to anything but a couple
11 | disconnected components and a fairly worthless (non-flux) store.
12 |
13 | You'll also notice that right next to each module, there's a `.test.js` file where
14 | there's a single test that just verifies that your tests are running.
15 |
16 | We already have many of the same dependencies you would have in a normal react
17 | project like `babel` and friends, `react` (and friends), and
18 | `eslint`... and friends. However, to get our tests going, we're going to need
19 | a few more dependencies. Oh, and one more thing, let me introduce you to your new
20 | best friend:
21 |
22 | 🐯 *- Hi! I'm Terry the Tiger! These instructions are really long and boring! So*
23 | *I'll pop up here and there where you'll be expected to actually do something!*
24 | *And if you really want to skip around, just copy me and ⌘+f*
25 | *(or CTRL+f on windows) for me on the page. See you around!*
26 |
27 | # Incomplete :-(
28 |
29 | Unfortunately I haven't had time to finish this (as I did with the AVA counterpart)
30 |
31 | On the plus side, Jest is pretty easy to get set up! And I have a few videos
32 | on [egghead.io](https://egghead.io/) [here](http://kcd.im/egghead-jest) to help you
33 | get going.
34 |
35 | Also, this repo has `exercises` (where you'll do most of your work) and
36 | `exercises-final` (where you can check your work). If you notice any typos or anything
37 | feel free to [make a pull request](http://makeapullrequest.com/) to the `templates`
38 | directory :)
39 |
40 | Thanks!
41 |
42 | ---
43 |
44 | ## Appendix
45 |
46 | ### Redux
47 |
48 | You may be wondering, "how do I test components that use Redux?" Well, this repo
49 | doesn't really show that, but it's because it's pretty much exactly how you do
50 | a normal `Props` input test because if you're using `connect` from `react-redux`
51 | then you simply `export` the component that you're wrapping in `connect` for
52 | testing purposes, and just test that the same way you do other components with
53 | `Props` inputs.
54 |
55 | If you're not using `connect` and you're subscribing to it yourself, then you'll
56 | simply treat it like the `Data` input test where you accept the store as a prop
57 | and add an item in `defaultProps` for the actual store singleton.
58 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 | Copyright (c) 2016 Kent C. Dodds
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining a copy
5 | of this software and associated documentation files (the "Software"), to deal
6 | in the Software without restriction, including without limitation the rights
7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | copies of the Software, and to permit persons to whom the Software is
9 | furnished to do so, subject to the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be included in all
12 | copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | SOFTWARE.
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # [React][React] + [Jest][Jest] + [Enzyme][Enzyme] = :heart:
2 |
3 | [![slides-badge][slides-badge]][slides]
4 | [![PRs Welcome][prs-badge]][prs]
5 | [![Donate][donate-badge]][donate]
6 |
7 |
8 |
9 | Find slides [here](http://kcd.im/testing-react)
10 |
11 | This is a workshop for learning how to test [React][React] with the [Jest][Jest] testing framework and the
12 | [Enzyme][Enzyme] testing library.
13 |
14 | ## Project Setup
15 |
16 | This project assumes you have [NodeJS v6](http://nodejs.org/) or greater installed. It's also recommended to use the
17 | [`yarn`](https://yarnpkg.com/) client (rather than [npm](https://www.npmjs.com/)). If you'd rather stick with `npm`,
18 | that's fine. Just replace `yarn` with `npm` in the instructions below and hope that things don't break 😏.
19 | You'll also need a recent version of [git](https://git-scm.com/) installed as well.
20 |
21 | With that, run:
22 |
23 | ```
24 | git clone https://github.com/kentcdodds/react-jest-workshop.git
25 | cd react-jest-workshop
26 | yarn run setup
27 | ```
28 |
29 | If the `yarn run setup` script finishes without errors (don't worry about warnings) then you're good to go. Otherwise,
30 | please [file an issue](https://help.github.com/articles/creating-an-issue/).
31 |
32 | ## Testing Instructions
33 |
34 | There are two directories in this project that you should be interested in:
35 |
36 | - `exercises`: Where the unfinished tests are (where you should add your tests).
37 | - `exercises-final`: Where the finished tests are (where you can reference if you get stuck).
38 |
39 | The tests in `exercises` are actually all scaffolded for you. So your goal is to go through and write all the tests. Do this:
40 |
41 | 1. Run `yarn run watch:test` which will start running the tests in watch mode, meaning that as you save your file, it
42 | will automatically re-run your tests so you can quickly see how you're doing.
43 | 2. Choose a file in the `exercises` directory that ends in `.test.js` and implement the tests one-by-one.
44 |
45 | Good luck!
46 |
47 | # LICENSE
48 |
49 | MIT
50 |
51 | [React]: https://facebook.github.io/react/
52 | [Jest]: http://facebook.github.io/jest/
53 | [Enzyme]: http://airbnb.io/enzyme/
54 | [slides]: http://kcd.im/react-jest
55 | [slides-badge]: https://cdn.rawgit.com/kentcdodds/custom-badges/2/badges/slides.svg
56 | [donate]: http://kcd.im/donate
57 | [prs-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square
58 | [prs]: http://makeapullrequest.com
59 | [donate-badge]: https://img.shields.io/badge/$-support-green.svg?style=flat-square
60 |
--------------------------------------------------------------------------------
/exercises-final/components/Button.js:
--------------------------------------------------------------------------------
1 | import React, {PropTypes} from 'react'
2 |
3 | export default Button
4 |
5 | function Button({children}, {color}) {
6 | return (
7 |
10 | )
11 | }
12 |
13 | Button.propTypes = {
14 | children: PropTypes.any.isRequired,
15 | }
16 |
17 | Button.contextTypes = {
18 | color: React.PropTypes.string
19 | }
20 |
--------------------------------------------------------------------------------
/exercises-final/components/Button.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {mount} from 'enzyme'
3 | import {mountToJson} from 'enzyme-to-json'
4 | import Button from './Button'
5 |
6 | test('styles the button with a background of the context color', () => {
7 | const wrapper = mount(, {
8 | context: {color: 'blue'}
9 | })
10 | expect(mountToJson(wrapper)).toMatchSnapshot()
11 | })
12 |
--------------------------------------------------------------------------------
/exercises-final/components/README.md:
--------------------------------------------------------------------------------
1 | # Toggle
2 |
3 | This demonstrates how to test for output and avoid testing implementation details.
4 | One of the things you want to avoid in testing, be it unit, integration, whatever,
5 | is testing *how* something works rather than simply that it accomplishes what it
6 | needs to accomplish.
7 |
8 | In this component, we want to test as much as we can by purely changing the props
9 | used to initialize it. However, this component also responds to user interaction
10 | to alter some of its state, so we'll work with simulating user-invoked events to
11 | test how that interaction changes the output of our component as well as how it
12 | interacts with the props that we pass it.
13 |
14 | # Button
15 |
16 | This demonstrates how you might test something that utilizes context.
17 | It is unlikely you have very many places where you'll be doing this
18 | (using context), however there are some cases where you'll do that.
19 |
--------------------------------------------------------------------------------
/exercises-final/components/Toggle.js:
--------------------------------------------------------------------------------
1 | import React, {PropTypes, Component} from 'react'
2 |
3 | class Toggle extends Component {
4 | constructor(props, ...rest) {
5 | super(props, ...rest)
6 | this.state = {
7 | toggledOn: props.initialToggledOn || false,
8 | }
9 | }
10 |
11 | handleToggleClick = () => {
12 | const toggledOn = !this.state.toggledOn
13 | this.props.onToggle(toggledOn)
14 | this.setState({toggledOn})
15 | }
16 |
17 | render() {
18 | const {children} = this.props
19 | const {toggledOn} = this.state
20 |
21 | const onOff = toggledOn ? 'on' : 'off'
22 | const toggledClassName = `toggle--${onOff}`
23 | return (
24 |
25 |
28 |
29 | )
30 | }
31 | }
32 |
33 | Toggle.propTypes = {
34 | initialToggledOn: PropTypes.bool,
35 | onToggle: PropTypes.func.isRequired,
36 | children: PropTypes.any.isRequired,
37 | }
38 |
39 | export default Toggle
40 |
--------------------------------------------------------------------------------
/exercises-final/components/Toggle.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {render, mount} from 'enzyme'
3 | import Toggle from './Toggle'
4 |
5 | test('has toggle--off class applied by default', () => {
6 | const wrapper = renderToggle()
7 | expect(rootHasClass(wrapper, 'toggle--off')).toBe(true)
8 | })
9 |
10 | test('has toggle--on class applied when initialToggledOn specified to true', () => {
11 | const wrapper = renderToggle({initialToggledOn: true})
12 | expect(rootHasClass(wrapper, 'toggle--on')).toBe(true)
13 | })
14 |
15 | test('invokes the onToggle prop when clicked', () => {
16 | const onToggle = jest.fn()
17 | const wrapper = mountToggle({onToggle})
18 | clickButton(wrapper)
19 | expect(onToggle).toHaveBeenCalledTimes(1)
20 | expect(onToggle).toBeCalledWith(true)
21 | })
22 |
23 |
24 | /**
25 | * Uses enzyme to mount the Toggle component
26 | * @param {Object} props - the props to mount the component with
27 | * @return {Object} - the enzyme wrapper
28 | */
29 | function mountToggle(props = {}) {
30 | return mount(
31 | {}}
33 | children="Toggle Me"
34 | {...props}
35 | />
36 | )
37 | }
38 |
39 | /**
40 | * Uses enzyme to render the Toggle component
41 | * @param {Object} props - the props to render the component with
42 | * @return {Object} - the enzyme wrapper
43 | */
44 | function renderToggle(props = {}) {
45 | return render(
46 | {}}
48 | children="Toggle Me"
49 | {...props}
50 | />
51 | )
52 | }
53 |
54 | /**
55 | * finds the button in the given wrapper and simulates a click event
56 | * @param {Object} wrapper - the enzyme wrapper
57 | */
58 | function clickButton(wrapper) {
59 | wrapper.find('button').first().simulate('click')
60 | }
61 |
62 | /**
63 | * Returns whether the root of the given wrapper has the given className
64 | * @param {Object} wrapper - the wrapper to get the root element from
65 | * @param {String} className - the class to check for
66 | * @return {Boolean} whether the root element has the given class
67 | */
68 | function rootHasClass(wrapper, className) {
69 | return wrapper.children().first().hasClass(className)
70 | }
71 |
--------------------------------------------------------------------------------
/exercises-final/components/__snapshots__/Button.test.js.snap:
--------------------------------------------------------------------------------
1 | exports[`test styles the button with a background of the context color 1`] = `
2 |
12 | `;
13 |
--------------------------------------------------------------------------------
/exercises-final/containers/CustomerList.js:
--------------------------------------------------------------------------------
1 | import React, {PropTypes, Component} from 'react'
2 | import store from '../store/Customers'
3 |
4 | class CustomerList extends Component {
5 | constructor(props) {
6 | super(props)
7 | this.state = {
8 | customers: props.store.getCustomers(),
9 | }
10 | }
11 | componentDidMount() {
12 | this.unsubscribe = this.props.store.subscribe(this.updateStateWithCustomers)
13 | }
14 | componentWillUnmount() {
15 | this.unsubscribe()
16 | }
17 |
18 | updateStateWithCustomers = () => {
19 | const customers = this.props.store.getCustomers()
20 | this.setState({customers})
21 | }
22 |
23 | render() {
24 | const {customers} = this.state
25 | if (customers.length === 0) {
26 | return
27 | } else {
28 | return
29 | }
30 | }
31 | }
32 |
33 | CustomerList.defaultProps = {store}
34 |
35 | CustomerList.propTypes = {
36 | store: PropTypes.shape({
37 | getCustomers: PropTypes.func,
38 | subscribe: PropTypes.func,
39 | }),
40 | }
41 |
42 | function ListOfCustomers({customers}) {
43 | return (
44 |
60 | You have no customers. Better get to work!
61 |
62 | )
63 | }
64 |
65 | export default CustomerList
66 |
--------------------------------------------------------------------------------
/exercises-final/containers/CustomerList.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {render, mount} from 'enzyme'
3 | import {renderToJson, mountToJson} from 'enzyme-to-json'
4 | import getStoreStub from '../store/Customers.stub'
5 | import CustomerList from './CustomerList'
6 |
7 | test('should render no customers', () => {
8 | snapshotCustomerList()
9 | })
10 |
11 | test('should render customers', () => {
12 | const {store} = getStoreStub([{name: 'Bob'}, {name: 'Joanna'}])
13 | snapshotCustomerList({store})
14 | })
15 |
16 | test('should respond to store updates', () => {
17 | const {store, updateCustomers} = getStoreStub()
18 | const wrapper = mountCustomerList({store})
19 | expect(mountToJson(wrapper)).toMatchSnapshot()
20 | updateCustomers([{name: 'Jill'}, {name: 'Fred'}])
21 | expect(mountToJson(wrapper)).toMatchSnapshot()
22 | })
23 |
24 | test('unsubscribe when unmounted', () => {
25 | const {unsubscribe, store} = getStoreStub()
26 | const wrapper = mountCustomerList({store})
27 | wrapper.unmount()
28 | expect(unsubscribe).toHaveBeenCalledTimes(1)
29 | })
30 |
31 | /**
32 | * Render the and snapshot it
33 | * @param {Object} props - the props to render with
34 | */
35 | function snapshotCustomerList(props = {}) {
36 | const wrapper = renderCustomerList(props)
37 | expect(renderToJson(wrapper)).toMatchSnapshot()
38 | }
39 |
40 | /**
41 | * Renders with the given props
42 | * @param {Object} props - the props to render with
43 | * @return {Object} the rendered component
44 | */
45 | function renderCustomerList({store = getStoreStub().store}) {
46 | return render()
47 | }
48 |
49 | /**
50 | * Mounts with the given props
51 | * @param {Object} props - the props to mount with
52 | * @return {Object} the rendered component
53 | */
54 | function mountCustomerList({store = getStoreStub().store}) {
55 | return mount()
56 | }
57 |
--------------------------------------------------------------------------------
/exercises-final/containers/README.md:
--------------------------------------------------------------------------------
1 | # CustomerList
2 |
3 | This demonstrates how to test a component that has conditional logic in its
4 | render method. More usefully, it demonstrates how you can test components
5 | that re-render based off of updates from an external data source. In this
6 | case this is a (non-flux) Customer Store.
7 |
8 | With Redux, doing this test is simpler because you don't concern yourself
9 | with state in your components and they're simply passed the state via props.
10 |
11 | In this scenario, we're depending on a "singleton" store for our application
12 | and our component's lifecycle hooks are subscribing to this store. This can
13 | cause issues with our tests potentially mucking with the state of other tests.
14 | To combat this, if we make it possible to override the store we're using (by
15 | utilizing `defaultProps`) then we can easily pass our own stubbed version of
16 | this store to avoid issues with sharing a singleton across tests and allow
17 | us to perform assertions on properties of this stubbed store.
18 |
19 |
--------------------------------------------------------------------------------
/exercises-final/containers/__snapshots__/CustomerList.test.js.snap:
--------------------------------------------------------------------------------
1 | exports[`test should render customers 1`] = `
2 |
3 | Here is your list of customers!
4 |
5 |
6 | Bob
7 |
8 |
9 | Joanna
10 |
11 |
12 |
13 | `;
14 |
15 | exports[`test should render no customers 1`] = `
16 |
17 | You have no customers. Better get to work!
18 |
19 | `;
20 |
21 | exports[`test should respond to store updates 1`] = `
22 |
29 |
30 |
31 | You have no customers. Better get to work!
32 |
33 |
34 |
35 | `;
36 |
37 | exports[`test should respond to store updates 2`] = `
38 |
45 |
56 |
57 |
58 |
59 | Jill
60 |
61 |
62 | Fred
63 |
64 |
65 |
66 |
67 |
68 | `;
69 |
--------------------------------------------------------------------------------
/exercises-final/jest.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "testPathDirs": [
3 | "exercises-final"
4 | ],
5 | "coverageThreshold": {
6 | "global": {
7 | "branches": 100,
8 | "functions": 95,
9 | "lines": 100,
10 | "statements": 100
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/exercises-final/store/Customers.js:
--------------------------------------------------------------------------------
1 | let _customers = []
2 | const callbacks = []
3 |
4 | export default {
5 | getCustomers,
6 | setCustomers,
7 | subscribe,
8 | }
9 |
10 | /**
11 | * Returns the current list of customers
12 | * @return {Array} customers
13 | */
14 | function getCustomers() {
15 | return _customers
16 | }
17 |
18 | /**
19 | * Sets the current list of customers to the given customers
20 | * and lets all the subscribers know about the update
21 | * @param {Array} customers - An array of objects that have a name property that is a string
22 | */
23 | function setCustomers(customers) {
24 | _customers = customers
25 | _letSubscribersKnow()
26 | }
27 |
28 | /**
29 | * Adds the given callback to a list of functions to be called when the current customers are set
30 | * @param {Function} callback - the callback to be called
31 | * @return {Function} - a function to call to unsubscribe
32 | */
33 | function subscribe(callback) {
34 | callbacks.push(callback)
35 | return function removeCallback() {
36 | callbacks.splice(callbacks.indexOf(callback), 1)
37 | }
38 | }
39 |
40 | /**
41 | * Iterates through all callbacks and calls them
42 | */
43 | function _letSubscribersKnow() {
44 | callbacks.forEach(cb => cb())
45 | }
46 |
--------------------------------------------------------------------------------
/exercises-final/store/Customers.stub.js:
--------------------------------------------------------------------------------
1 | export default getStoreStub
2 |
3 | /**
4 | * Create a stub for the store which can be used for assertions
5 | * @param {Array} customers - the array of customers
6 | * @returns {Object} - ref property has customers and will haf ref.callback when
7 | * store.callback is invoked. store.getCustomers will return ref.customers
8 | */
9 | function getStoreStub(customers = []) {
10 | let callback
11 | const unsubscribe = jest.fn()
12 | const ref = {customers}
13 |
14 | const store = {
15 | getCustomers: () => ref.customers,
16 | subscribe: cb => {
17 | callback = cb
18 | return unsubscribe
19 | },
20 | }
21 | return {unsubscribe, store, updateCustomers}
22 |
23 | function updateCustomers(newCustomers) {
24 | ref.customers = newCustomers
25 | callback()
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/exercises-final/store/Customers.test.js:
--------------------------------------------------------------------------------
1 | test('should start with empty', () => {
2 | const {store} = setup()
3 | const customers = store.getCustomers()
4 | expect(customers.length).toBe(0)
5 | })
6 |
7 | test('should allow you to set customers and get them', () => {
8 | const {store} = setup()
9 | const c0 = {name: 'Bill'}
10 | const c1 = {name: 'Francine'}
11 | store.setCustomers([c0, c1])
12 | const customers = store.getCustomers()
13 | const [sc0, sc1] = customers
14 | expect(customers.length).toBe(2)
15 | expect(c0).toBe(sc0)
16 | expect(c1).toBe(sc1)
17 | })
18 |
19 | test('should allow you to subscribe to the store', () => {
20 | const {store} = setup()
21 | const subscriber = jest.fn()
22 | const unsubscribe = store.subscribe(subscriber)
23 | store.setCustomers([])
24 | expect(subscriber).toHaveBeenCalledTimes(1)
25 | subscriber.mockClear()
26 | unsubscribe()
27 | store.setCustomers([])
28 | expect(subscriber).not.toBeCalled()
29 | })
30 |
31 | /**
32 | * Prepares our environment for an individual test and returns whatever is needed for that test to run.
33 | * @return {Object} what is needed for tests to run. In this case it is only a fresh copy of the store
34 | */
35 | function setup() {
36 | // clear the require cache so when we require the store we get a fresh copy
37 | jest.resetModules()
38 | const store = require('./Customers').default
39 | return {store}
40 | }
41 |
--------------------------------------------------------------------------------
/exercises-final/store/README.md:
--------------------------------------------------------------------------------
1 | # Old-school
2 |
3 | Not all applications use Redux 😞. I hacked together this (non-flux) Customers store
4 | to pretty much demonstrate how you test dealing with updates that happen to your
5 | components when they're updated from an external source of data (like a store).
6 |
7 | I don't recommend you write your stores this way.
8 |
9 | This store is tested, but there's not really much going on in here...
10 |
--------------------------------------------------------------------------------
/exercises/components/Button.js:
--------------------------------------------------------------------------------
1 | import React, {PropTypes} from 'react'
2 |
3 | export default Button
4 |
5 | function Button({children}, {color}) {
6 | return (
7 |
8 | {children}
9 |
10 | )
11 | }
12 |
13 | Button.propTypes = {
14 | children: PropTypes.any.isRequired,
15 | }
16 |
17 | Button.contextTypes = {
18 | color: React.PropTypes.string
19 | }
20 |
--------------------------------------------------------------------------------
/exercises/components/Button.test.js:
--------------------------------------------------------------------------------
1 | // You're going to need react, mount from enzyme,
2 | // mountToJson from enzyme-to-json, and ./Button
3 |
4 | test('styles the button with a background of the context color', () => {
5 | // get a new version of the Button component by using stubContext to stub it with the color blue
6 | // render that component
7 | // take a snapshot of the result and verify the snapshot
8 | })
9 |
--------------------------------------------------------------------------------
/exercises/components/README.md:
--------------------------------------------------------------------------------
1 | # Toggle
2 |
3 | This demonstrates how to test for output and avoid testing implementation details.
4 | One of the things you want to avoid in testing, be it unit, integration, whatever,
5 | is testing *how* something works rather than simply that it accomplishes what it
6 | needs to accomplish.
7 |
8 | In this component, we want to test as much as we can by purely changing the props
9 | used to initialize it. However, this component also responds to user interaction
10 | to alter some of its state, so we'll work with simulating user-invoked events to
11 | test how that interaction changes the output of our component as well as how it
12 | interacts with the props that we pass it.
13 |
14 | # Button
15 |
16 | This demonstrates how you might test something that utilizes context.
17 | It is unlikely you have very many places where you'll be doing this
18 | (using context), however there are some cases where you'll do that.
19 |
--------------------------------------------------------------------------------
/exercises/components/Toggle.js:
--------------------------------------------------------------------------------
1 | import React, {PropTypes, Component} from 'react'
2 |
3 | class Toggle extends Component {
4 | constructor(props, ...rest) {
5 | super(props, ...rest)
6 | this.state = {
7 | toggledOn: props.initialToggledOn || false,
8 | }
9 | }
10 |
11 | handleToggleClick = () => {
12 | const toggledOn = !this.state.toggledOn
13 | this.props.onToggle(toggledOn)
14 | this.setState({toggledOn})
15 | }
16 |
17 | render() {
18 | const {children} = this.props
19 | const {toggledOn} = this.state
20 |
21 | const onOff = toggledOn ? 'on' : 'off'
22 | const toggledClassName = `toggle--${onOff}`
23 | return (
24 |
25 |
26 | {children}
27 |
28 |
29 | )
30 | }
31 | }
32 |
33 | Toggle.propTypes = {
34 | initialToggledOn: PropTypes.bool,
35 | onToggle: PropTypes.func.isRequired,
36 | children: PropTypes.any.isRequired,
37 | }
38 |
39 | export default Toggle
40 |
--------------------------------------------------------------------------------
/exercises/components/Toggle.test.js:
--------------------------------------------------------------------------------
1 | // you'll need to import react, enzyme's render and mount functions,
2 | // and ./Toggle
3 |
4 | test('has toggle--off class applied by default', () => {
5 | // create a renderToggle function and call that without arguments to get a wrapper with the defaults
6 | // expect the first child to have the class toggle--off (tip: create rootHasClass(wrapper, className) function)
7 | })
8 |
9 | test('has toggle--on class applied when initialToggledOn specified to true', () => {
10 | // use the renderToggle function and call it with {initialToggledOn: true}
11 | // expect the first child to have the class toggle--on
12 | })
13 |
14 | test('invokes the onToggle prop when clicked', () => {
15 | // create a mock function of onToggle with jest.fn()
16 | // create a mountToggle function and call that with {onToggle}
17 | // take the returned enzyme wrapper and simulate a click event on the button
18 | // assert that onToggle was called once
19 | // assert that it was called with `true`
20 | })
21 |
22 |
23 | // create a renderToggle function that accepts some props and applies those to a render of the component
24 | // you should also provide defaults for any required props
25 | // create a mountToggle function that does basically the same thing except with mount
26 | // Also a clickButton(wrapper) function would be handy to create here as well as both tests will need to do that.
27 |
--------------------------------------------------------------------------------
/exercises/containers/CustomerList.js:
--------------------------------------------------------------------------------
1 | import React, {PropTypes, Component} from 'react'
2 | import store from '../store/Customers'
3 |
4 | class CustomerList extends Component {
5 | constructor(props) {
6 | super(props)
7 | this.state = {
8 | customers: store.getCustomers(),
9 | }
10 | }
11 | componentDidMount() {
12 | this.unsubscribe = store.subscribe(this.updateStateWithCustomers)
13 | }
14 | componentWillUnmount() {
15 | this.unsubscribe()
16 | }
17 |
18 | updateStateWithCustomers = () => {
19 | const customers = store.getCustomers()
20 | this.setState({customers})
21 | }
22 |
23 | render() {
24 | const {customers} = this.state
25 | if (customers.length === 0) {
26 | return
27 | } else {
28 | return
29 | }
30 | }
31 | }
32 |
33 | function ListOfCustomers({customers}) {
34 | return (
35 |
51 | You have no customers. Better get to work!
52 |
53 | )
54 | }
55 |
56 | export default CustomerList
57 |
--------------------------------------------------------------------------------
/exercises/containers/CustomerList.test.js:
--------------------------------------------------------------------------------
1 | // you're going to need to import a few things here:
2 | // react, react-test-renderer, ../store/Customers.stub,
3 | // and the ./CustomerList component (which we're testing)
4 |
5 | test('should render no customers', () => {
6 | // create a snapshotCustomerList function and test the default
7 | // behavior by calling it without arguments
8 | // Then use the resulting component to check the snapshot
9 | })
10 |
11 | test('should render customers', () => {
12 | // get a store from the stub and initialize it with two customers
13 | // we need to have the component use our stub instead of the singleton store somehow...
14 | // We _could_ use Jest's mocking capabilities. Or, we could just alter the CustomerList component to allow you
15 | // to specify a store! So go to the CustomerList.js file and add a prop called `store`. Wherever the singleton
16 | // `store` is used, use `this.props.store` instead and use defaultProps to have the `store` default to the singleton
17 | // `store` (that way actual users of the component don't have to specify the store).
18 | // Now use the snapshotCustomerList function you wrote to pass the store as a prop
19 | })
20 |
21 | test('should respond to store updates', () => {
22 | // get both the store and the updateCustomers from a call to `../store/Customers.stub`
23 | // render the customer list with the store stub
24 | // take a snapshot
25 | // call updateCustomers with a few customers
26 | // take another snapshot
27 | })
28 |
29 | test('unsubscribe when unmounted', () => {
30 | // we want to make sure that the unsubscribe function is called on the store
31 | // so get the store stub and the unsubscribe mock function from '../store/Customers.stub'
32 | // Then use enzyme's `mount` function to mount `./CustomerList` with the store stub.
33 | // Take the resulting wrapper from that `mount` and unmount it by calling `wrapper.unmount`
34 | // Then assert that the `unsubscribe` mock was called once with toHaveBeenCalledTimes(1)
35 | })
36 |
37 | // Create a snapshotCustomerList function that:
38 | // 1. Accepts props
39 | // 2. Creates a component with those props with a call to renderer.create (tip: you may wanna do this in a separate function)
40 | // 3. Asserts on a snapshot of that component with expect(component).toMatchSnapshot()
41 | // Create a renderCustomerList function that:
42 | // 1. Accepts props and defaults the store to the store stub
43 | // 2. Returns a render the CustomerList with those propse
44 | // Create a mountCustomerList function that:
45 | // 1. Accepts props and defaults the store to the store stub
46 | // 2. Returns a mount the CustomerList with those propse
47 |
--------------------------------------------------------------------------------
/exercises/containers/README.md:
--------------------------------------------------------------------------------
1 | # CustomerList
2 |
3 | This demonstrates how to test a component that has conditional logic in its
4 | render method. More usefully, it demonstrates how you can test components
5 | that re-render based off of updates from an external data source. In this
6 | case this is a (non-flux) Customer Store.
7 |
8 | With Redux, doing this test is simpler because you don't concern yourself
9 | with state in your components and they're simply passed the state via props.
10 |
11 | In this scenario, we're depending on a "singleton" store for our application
12 | and our component's lifecycle hooks are subscribing to this store. This can
13 | cause issues with our tests potentially mucking with the state of other tests.
14 | To combat this, if we make it possible to override the store we're using (by
15 | utilizing `defaultProps`) then we can easily pass our own stubbed version of
16 | this store to avoid issues with sharing a singleton across tests and allow
17 | us to perform assertions on properties of this stubbed store.
18 |
19 |
--------------------------------------------------------------------------------
/exercises/jest.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "testPathDirs": [
3 | "exercises"
4 | ],
5 | "coverageThreshold": {
6 | "global": {
7 | "branches": 100,
8 | "functions": 95,
9 | "lines": 100,
10 | "statements": 100
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/exercises/store/Customers.js:
--------------------------------------------------------------------------------
1 | let _customers = []
2 | const callbacks = []
3 |
4 | export default {
5 | getCustomers,
6 | setCustomers,
7 | subscribe,
8 | }
9 |
10 | /**
11 | * Returns the current list of customers
12 | * @return {Array} customers
13 | */
14 | function getCustomers() {
15 | return _customers
16 | }
17 |
18 | /**
19 | * Sets the current list of customers to the given customers
20 | * and lets all the subscribers know about the update
21 | * @param {Array} customers - An array of objects that have a name property that is a string
22 | */
23 | function setCustomers(customers) {
24 | _customers = customers
25 | _letSubscribersKnow()
26 | }
27 |
28 | /**
29 | * Adds the given callback to a list of functions to be called when the current customers are set
30 | * @param {Function} callback - the callback to be called
31 | * @return {Function} - a function to call to unsubscribe
32 | */
33 | function subscribe(callback) {
34 | callbacks.push(callback)
35 | return function removeCallback() {
36 | callbacks.splice(callbacks.indexOf(callback), 1)
37 | }
38 | }
39 |
40 | /**
41 | * Iterates through all callbacks and calls them
42 | */
43 | function _letSubscribersKnow() {
44 | callbacks.forEach(cb => cb())
45 | }
46 |
--------------------------------------------------------------------------------
/exercises/store/Customers.stub.js:
--------------------------------------------------------------------------------
1 | export default getStoreStub
2 |
3 | /**
4 | * Create a stub for the store which can be used for assertions
5 | * @param {Array} customers - the array of customers
6 | * @returns {Object} - ref property has customers and will haf ref.callback when
7 | * store.callback is invoked. store.getCustomers will return ref.customers
8 | */
9 | function getStoreStub(customers = []) {
10 | let callback
11 | const unsubscribe = jest.fn()
12 | const ref = {customers}
13 |
14 | const store = {
15 | getCustomers: () => ref.customers,
16 | subscribe: cb => {
17 | callback = cb
18 | return unsubscribe
19 | },
20 | }
21 | return {unsubscribe, store, updateCustomers}
22 |
23 | function updateCustomers(newCustomers) {
24 | ref.customers = newCustomers
25 | callback()
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/exercises/store/Customers.test.js:
--------------------------------------------------------------------------------
1 | test('should start with empty', () => {
2 | // get the store from your setup function
3 | // call getCustomers on it
4 | // assert that the lenth of customers is 0
5 | })
6 |
7 | test('should allow you to set customers and get them', () => {
8 | // get the store
9 | // create two customers and set the store to them
10 | // get the customers from the store
11 | // assert that there are two customers
12 | // assert that the customers you got are the ones you set
13 | })
14 |
15 | test('should allow you to subscribe to the store', () => {
16 | // get the store
17 | // setup a jest mock function (jest.fn()) for your subscriber
18 | // subscribe to the store with that function
19 | // call setCustomers
20 | // assert your subscriber was called once
21 | // clear your subscriber mock function (subscriber.mockClear())
22 | // call the unsubscribe function you got when subscribing
23 | // call setCustomers
24 | // assert that your mock function was not called
25 | })
26 |
27 | // Create a `setup` function:
28 | // clear the require cache with jest.resetModules() so you can require a fresh copy of the store
29 | // require the ./Customers module (note: because it's using `export default`,
30 | // the store is on the `default` property of what you're requiring)
31 | // return {store}
32 |
--------------------------------------------------------------------------------
/exercises/store/README.md:
--------------------------------------------------------------------------------
1 | # Old-school
2 |
3 | Not all applications use Redux 😞. I hacked together this (non-flux) Customers store
4 | to pretty much demonstrate how you test dealing with updates that happen to your
5 | components when they're updated from an external source of data (like a store).
6 |
7 | I don't recommend you write your stores this way.
8 |
9 | This store is tested, but there's not really much going on in here...
10 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-jest-workshop",
3 | "private": true,
4 | "version": "1.0.0",
5 | "description": "🐯 A workshop repository for testing React ⚛ with Jest 🃏",
6 | "main": "index.js",
7 | "scripts": {
8 | "lint": "eslint .",
9 | "test": "jest --coverage --config=exercises/jest.config.json",
10 | "test:final": "jest --coverage --config=exercises-final/jest.config.json",
11 | "watch:test": "jest --watch --config=exercises/jest.config.json",
12 | "generate": "split-guide generate",
13 | "validate": "npm run generate -s && npm run lint -s && npm run test:final -s",
14 | "setup": "node ./scripts/verify && node ./scripts/install && npm run validate"
15 | },
16 | "repository": {
17 | "type": "git",
18 | "url": "git+https://github.com/kentcdodds/react-jest-workshop.git"
19 | },
20 | "keywords": [],
21 | "author": "Kent C. Dodds (http://kentcdodds.com/)",
22 | "license": "MIT",
23 | "bugs": {
24 | "url": "https://github.com/kentcdodds/react-jest-workshop/issues"
25 | },
26 | "homepage": "https://github.com/kentcdodds/react-jest-workshop#readme",
27 | "dependencies": {
28 | "react": "^15.4.1",
29 | "react-dom": "^15.4.1"
30 | },
31 | "devDependencies": {
32 | "babel-jest": "^17.0.2",
33 | "babel-polyfill": "6.16.0",
34 | "babel-preset-es2015": "^6.18.0",
35 | "babel-preset-react": "6.16.0",
36 | "babel-preset-stage-2": "^6.18.0",
37 | "enzyme": "^2.6.0",
38 | "enzyme-to-json": "^1.4.4",
39 | "eslint": "^3.11.1",
40 | "eslint-config-kentcdodds": "^11.1.0",
41 | "glob": "7.1.1",
42 | "jest": "^17.0.3",
43 | "mkdirp": "0.5.1",
44 | "react-addons-test-utils": "^15.3.2",
45 | "rimraf": "2.5.4",
46 | "split-guide": "1.1.0"
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/scripts/install.js:
--------------------------------------------------------------------------------
1 | var cp = require('child_process')
2 | var spawn = cp.spawn, execSync = cp.execSync
3 |
4 | var useYarn = false
5 | try {
6 | useYarn = !!execSync('yarn --version')
7 | } catch (e) {
8 | // use npm instead :-(
9 | }
10 |
11 | var installer = useYarn ? 'yarn' : 'npm'
12 |
13 | console.log('\n📦 Installing dependencies via `' + installer + ' install`')
14 |
15 | spawn(installer, ['install'], {stdio: 'inherit', shell: true})
16 |
--------------------------------------------------------------------------------
/scripts/verify.js:
--------------------------------------------------------------------------------
1 | var execSync = require('child_process').execSync
2 |
3 | var desiredVersions = {
4 | yarn: '0.17.10',
5 | node: '6.0.0',
6 | npm: '4.0.3',
7 | }
8 |
9 | var errors = {
10 | noYarn: {
11 | message: 'You do not have yarn installed. This is a package manager client that installs from the regular npm ' +
12 | 'registry, but ensures you get the same versions of all dependencies required for this repository. ' +
13 | 'It is highly recommended that you install yarn: `npm install --global yarn` (learn more: https://yarnpkg.com/)',
14 | isProblem: false,
15 | },
16 | oldYarn: {
17 | getMessage: function(desired, actual) {
18 | return 'Your version of yarn (' + actual + ') is older than the recommended version of ' + desired + '. ' +
19 | 'Run `yarn self-update` (or `npm install --global yarn@latest`) to update.'
20 | },
21 | isProblem: false,
22 | },
23 | oldNode: {
24 | getMessage: function(desired, actual) {
25 | return 'Your version of node (' + actual + ') is older than the recommended version of ' + desired + '. ' +
26 | 'Please install a more recent version. You can use http://git.io/nvm or https://github.com/coreybutler/nvm-windows ' +
27 | 'to make upgrading your version of node easier.'
28 | },
29 | isProblem: false,
30 | },
31 | oldNpm: {
32 | getMessage: function(desired, actual) {
33 | return 'Your version of npm (' + actual + ') is older than the recommended version of ' + desired + '. ' +
34 | 'You should install yarn anyway, but if you would rather use npm, please at least have a more recent version. ' +
35 | 'You can install the latest version by running `npm install --global npm@latest`.'
36 | },
37 | isProblem: false,
38 | },
39 | }
40 |
41 | var nodeVersion = process.versions.node
42 | errors.oldNode.isProblem = !versionIsGreater(desiredVersions.node, nodeVersion)
43 | errors.oldNode.message = errors.oldNode.getMessage(desiredVersions.node, nodeVersion)
44 |
45 | try {
46 | var yarnVersion = execSync('yarn --version').toString().trim()
47 | errors.oldYarn.isProblem = !versionIsGreater(desiredVersions.yarn, yarnVersion)
48 | errors.oldYarn.message = errors.oldYarn.getMessage(desiredVersions.yarn, yarnVersion)
49 | } catch (e) {
50 | errors.noYarn.isProblem = true
51 | var npmVersion = execSync('npm --version').toString().trim()
52 | errors.oldNpm.isProblem = !versionIsGreater(desiredVersions.npm, npmVersion)
53 | errors.oldNpm.message = errors.oldNpm.getMessage(desiredVersions.npm, npmVersion)
54 | }
55 |
56 | var systemErrors = Object.keys(errors)
57 | .filter(function(key) { return errors[key].isProblem })
58 |
59 | var errorCount = systemErrors.length
60 |
61 | if (errorCount) {
62 | var errorMessage = systemErrors
63 | .reduce(function(messages, key) {
64 | messages.push(' - ' + errors[key].message)
65 | return messages
66 | }, [])
67 | .join('\n')
68 | var one = errorCount === 1
69 |
70 | console.error(
71 | 'There ' + (one ? 'is an issue' : 'are some issues') + ' with your system. ' +
72 | 'It is quite likely that if you do not resolve these, you will have a hard time running this repository.\n' +
73 | errorMessage
74 | )
75 | console.info('If you don\'t care about these warnings, go ahead and install dependencies with `node ./scripts/install`')
76 | process.exitCode = 1
77 | }
78 |
79 | // returns actualVersion >= desiredVersion
80 | function versionIsGreater(desiredVersion, actualVersion) {
81 | var desiredVersions = /v?(\d+)\.(\d+)\.(\d+)/.exec(desiredVersion)
82 | var desiredMajor = Number(desiredVersions[1]), desiredMinor = Number(desiredVersions[2]), desiredPatch = Number(desiredVersions[3])
83 | var actualVersions = /v?(\d+)\.(\d+)\.(\d+)/.exec(actualVersion)
84 | var actualMajor = Number(actualVersions[1]), actualMinor = Number(actualVersions[2]), actualPatch = Number(actualVersions[3])
85 | if (actualMajor < desiredMajor) {
86 | return false
87 | } else if (actualMajor > desiredMajor) {
88 | return true
89 | }
90 | if (actualMinor < desiredMinor) {
91 | return false
92 | } else if (actualMinor > desiredMinor) {
93 | return true
94 | }
95 | if (actualPatch < desiredPatch) {
96 | return false
97 | } else if (actualPatch > desiredPatch) {
98 | return true
99 | }
100 | // by this point they should be equal
101 | return true
102 | }
--------------------------------------------------------------------------------
/templates/components/Button.js:
--------------------------------------------------------------------------------
1 | import React, {PropTypes} from 'react'
2 |
3 | export default Button
4 |
5 | function Button({children}, {color}) {
6 | return (
7 |
8 | {children}
9 |
10 | )
11 | }
12 |
13 | Button.propTypes = {
14 | children: PropTypes.any.isRequired,
15 | }
16 |
17 | Button.contextTypes = {
18 | color: React.PropTypes.string
19 | }
20 |
--------------------------------------------------------------------------------
/templates/components/Button.test.js:
--------------------------------------------------------------------------------
1 | // FINAL_START
2 | import React from 'react'
3 | import {mount} from 'enzyme'
4 | import {mountToJson} from 'enzyme-to-json'
5 | import Button from './Button'
6 | // FINAL_END
7 | // WORKSHOP_START
8 | // You're going to need react, mount from enzyme,
9 | // mountToJson from enzyme-to-json, and ./Button
10 | // WORKSHOP_END
11 |
12 | test('styles the button with a background of the context color', () => {
13 | // FINAL_START
14 | const wrapper = mount(Click Me, {
15 | context: {color: 'blue'}
16 | })
17 | expect(mountToJson(wrapper)).toMatchSnapshot()
18 | // FINAL_END
19 | // WORKSHOP_START
20 | // get a new version of the Button component by using stubContext to stub it with the color blue
21 | // render that component
22 | // take a snapshot of the result and verify the snapshot
23 | // WORKSHOP_END
24 | })
25 |
--------------------------------------------------------------------------------
/templates/components/README.md:
--------------------------------------------------------------------------------
1 | # Toggle
2 |
3 | This demonstrates how to test for output and avoid testing implementation details.
4 | One of the things you want to avoid in testing, be it unit, integration, whatever,
5 | is testing *how* something works rather than simply that it accomplishes what it
6 | needs to accomplish.
7 |
8 | In this component, we want to test as much as we can by purely changing the props
9 | used to initialize it. However, this component also responds to user interaction
10 | to alter some of its state, so we'll work with simulating user-invoked events to
11 | test how that interaction changes the output of our component as well as how it
12 | interacts with the props that we pass it.
13 |
14 | # Button
15 |
16 | This demonstrates how you might test something that utilizes context.
17 | It is unlikely you have very many places where you'll be doing this
18 | (using context), however there are some cases where you'll do that.
19 |
--------------------------------------------------------------------------------
/templates/components/Toggle.js:
--------------------------------------------------------------------------------
1 | import React, {PropTypes, Component} from 'react'
2 |
3 | class Toggle extends Component {
4 | constructor(props, ...rest) {
5 | super(props, ...rest)
6 | this.state = {
7 | toggledOn: props.initialToggledOn || false,
8 | }
9 | }
10 |
11 | handleToggleClick = () => {
12 | const toggledOn = !this.state.toggledOn
13 | this.props.onToggle(toggledOn)
14 | this.setState({toggledOn})
15 | }
16 |
17 | render() {
18 | const {children} = this.props
19 | const {toggledOn} = this.state
20 |
21 | const onOff = toggledOn ? 'on' : 'off'
22 | const toggledClassName = `toggle--${onOff}`
23 | return (
24 |
25 |
26 | {children}
27 |
28 |
29 | )
30 | }
31 | }
32 |
33 | Toggle.propTypes = {
34 | initialToggledOn: PropTypes.bool,
35 | onToggle: PropTypes.func.isRequired,
36 | children: PropTypes.any.isRequired,
37 | }
38 |
39 | export default Toggle
40 |
--------------------------------------------------------------------------------
/templates/components/Toggle.test.js:
--------------------------------------------------------------------------------
1 | // FINAL_START
2 | import React from 'react'
3 | import {render, mount} from 'enzyme'
4 | import Toggle from './Toggle'
5 | // FINAL_END
6 | // WORKSHOP_START
7 | // you'll need to import react, enzyme's render and mount functions,
8 | // and ./Toggle
9 | // WORKSHOP_END
10 |
11 | test('has toggle--off class applied by default', () => {
12 | // FINAL_START
13 | const wrapper = renderToggle()
14 | expect(rootHasClass(wrapper, 'toggle--off')).toBe(true)
15 | // FINAL_END
16 | // WORKSHOP_START
17 | // create a renderToggle function and call that without arguments to get a wrapper with the defaults
18 | // expect the first child to have the class toggle--off (tip: create rootHasClass(wrapper, className) function)
19 | // WORKSHOP_END
20 | })
21 |
22 | test('has toggle--on class applied when initialToggledOn specified to true', () => {
23 | // FINAL_START
24 | const wrapper = renderToggle({initialToggledOn: true})
25 | expect(rootHasClass(wrapper, 'toggle--on')).toBe(true)
26 | // FINAL_END
27 | // WORKSHOP_START
28 | // use the renderToggle function and call it with {initialToggledOn: true}
29 | // expect the first child to have the class toggle--on
30 | // WORKSHOP_END
31 | })
32 |
33 | test('invokes the onToggle prop when clicked', () => {
34 | // FINAL_START
35 | const onToggle = jest.fn()
36 | const wrapper = mountToggle({onToggle})
37 | clickButton(wrapper)
38 | expect(onToggle).toHaveBeenCalledTimes(1)
39 | expect(onToggle).toBeCalledWith(true)
40 | // FINAL_END
41 | // WORKSHOP_START
42 | // create a mock function of onToggle with jest.fn()
43 | // create a mountToggle function and call that with {onToggle}
44 | // take the returned enzyme wrapper and simulate a click event on the button
45 | // assert that onToggle was called once
46 | // assert that it was called with `true`
47 | // WORKSHOP_END
48 | })
49 |
50 | // COMMENT_START
51 | // this one isn't working for some reason... Anyone wanna give it a look?
52 | test('changes the class to toggle--on when clicked', () => {
53 | // FINAL_START
54 | const wrapper = mountToggle()
55 | clickButton(wrapper)
56 | expect(rootHasClass(wrapper, 'toggle--on')).toBe(true)
57 | // FINAL_END
58 | // WORKSHOP_START
59 | // mountToggle with no specified props (just use defaults from your mountToggle function)
60 | // click the button
61 | // take a snapshot of the wrapper with mountToJson from enzyme-to-json and verify it looks good
62 | // WORKSHOP_END
63 | })
64 | // COMMENT_END
65 |
66 | // FINAL_START
67 | /**
68 | * Uses enzyme to mount the Toggle component
69 | * @param {Object} props - the props to mount the component with
70 | * @return {Object} - the enzyme wrapper
71 | */
72 | function mountToggle(props = {}) {
73 | return mount(
74 | {}}
76 | children="Toggle Me"
77 | {...props}
78 | />
79 | )
80 | }
81 |
82 | /**
83 | * Uses enzyme to render the Toggle component
84 | * @param {Object} props - the props to render the component with
85 | * @return {Object} - the enzyme wrapper
86 | */
87 | function renderToggle(props = {}) {
88 | return render(
89 | {}}
91 | children="Toggle Me"
92 | {...props}
93 | />
94 | )
95 | }
96 |
97 | /**
98 | * finds the button in the given wrapper and simulates a click event
99 | * @param {Object} wrapper - the enzyme wrapper
100 | */
101 | function clickButton(wrapper) {
102 | wrapper.find('button').first().simulate('click')
103 | }
104 |
105 | /**
106 | * Returns whether the root of the given wrapper has the given className
107 | * @param {Object} wrapper - the wrapper to get the root element from
108 | * @param {String} className - the class to check for
109 | * @return {Boolean} whether the root element has the given class
110 | */
111 | function rootHasClass(wrapper, className) {
112 | return wrapper.children().first().hasClass(className)
113 | }
114 | // FINAL_END
115 | // WORKSHOP_START
116 | // create a renderToggle function that accepts some props and applies those to a render of the component
117 | // you should also provide defaults for any required props
118 | // create a mountToggle function that does basically the same thing except with mount
119 | // Also a clickButton(wrapper) function would be handy to create here as well as both tests will need to do that.
120 | // WORKSHOP_END
121 |
--------------------------------------------------------------------------------
/templates/containers/CustomerList.js:
--------------------------------------------------------------------------------
1 | // COMMENT_START
2 | /* eslint no-dupe-keys:0, no-redeclare:0 */
3 | // COMMENT_END
4 | import React, {PropTypes, Component} from 'react'
5 | import store from '../store/Customers'
6 |
7 | class CustomerList extends Component {
8 | constructor(props) {
9 | super(props)
10 | this.state = {
11 | // FINAL_START the key is getting the store from props
12 | customers: props.store.getCustomers(),
13 | // FINAL_END
14 | // WORKSHOP_START normally you'll just use the singleton store
15 | customers: store.getCustomers(),
16 | // WORKSHOP_END
17 | }
18 | }
19 | componentDidMount() {
20 | // FINAL_START
21 | this.unsubscribe = this.props.store.subscribe(this.updateStateWithCustomers)
22 | // FINAL_END
23 | // WORKSHOP_START
24 | this.unsubscribe = store.subscribe(this.updateStateWithCustomers)
25 | // WORKSHOP_END
26 | }
27 | componentWillUnmount() {
28 | this.unsubscribe()
29 | }
30 |
31 | updateStateWithCustomers = () => {
32 | // FINAL_START
33 | const customers = this.props.store.getCustomers()
34 | // FINAL_END
35 | // WORKSHOP_START
36 | const customers = store.getCustomers()
37 | // WORKSHOP_END
38 | this.setState({customers})
39 | }
40 |
41 | render() {
42 | const {customers} = this.state
43 | if (customers.length === 0) {
44 | return
45 | } else {
46 | return
47 | }
48 | }
49 | }
50 |
51 | // FINAL_START
52 | CustomerList.defaultProps = {store}
53 |
54 | CustomerList.propTypes = {
55 | store: PropTypes.shape({
56 | getCustomers: PropTypes.func,
57 | subscribe: PropTypes.func,
58 | }),
59 | }
60 |
61 | // FINAL_END
62 | function ListOfCustomers({customers}) {
63 | return (
64 |
80 | You have no customers. Better get to work!
81 |
82 | )
83 | }
84 |
85 | export default CustomerList
86 |
--------------------------------------------------------------------------------
/templates/containers/CustomerList.test.js:
--------------------------------------------------------------------------------
1 | // FINAL_START
2 | import React from 'react'
3 | import {render, mount} from 'enzyme'
4 | import {renderToJson, mountToJson} from 'enzyme-to-json'
5 | import getStoreStub from '../store/Customers.stub'
6 | import CustomerList from './CustomerList'
7 | // FINAL_END
8 | // WORKSHOP_START
9 | // you're going to need to import a few things here:
10 | // react, react-test-renderer, ../store/Customers.stub,
11 | // and the ./CustomerList component (which we're testing)
12 | // WORKSHOP_END
13 |
14 | test('should render no customers', () => {
15 | // FINAL_START
16 | snapshotCustomerList()
17 | // FINAL_END
18 | // WORKSHOP_START
19 | // create a snapshotCustomerList function and test the default
20 | // behavior by calling it without arguments
21 | // Then use the resulting component to check the snapshot
22 | // WORKSHOP_END
23 | })
24 |
25 | test('should render customers', () => {
26 | // FINAL_START
27 | const {store} = getStoreStub([{name: 'Bob'}, {name: 'Joanna'}])
28 | snapshotCustomerList({store})
29 | // FINAL_END
30 | // WORKSHOP_START
31 | // get a store from the stub and initialize it with two customers
32 | // we need to have the component use our stub instead of the singleton store somehow...
33 | // We _could_ use Jest's mocking capabilities. Or, we could just alter the CustomerList component to allow you
34 | // to specify a store! So go to the CustomerList.js file and add a prop called `store`. Wherever the singleton
35 | // `store` is used, use `this.props.store` instead and use defaultProps to have the `store` default to the singleton
36 | // `store` (that way actual users of the component don't have to specify the store).
37 | // Now use the snapshotCustomerList function you wrote to pass the store as a prop
38 | // WORKSHOP_END
39 | })
40 |
41 | test('should respond to store updates', () => {
42 | // FINAL_START
43 | const {store, updateCustomers} = getStoreStub()
44 | const wrapper = mountCustomerList({store})
45 | expect(mountToJson(wrapper)).toMatchSnapshot()
46 | updateCustomers([{name: 'Jill'}, {name: 'Fred'}])
47 | expect(mountToJson(wrapper)).toMatchSnapshot()
48 | // FINAL_END
49 | // WORKSHOP_START
50 | // get both the store and the updateCustomers from a call to `../store/Customers.stub`
51 | // render the customer list with the store stub
52 | // take a snapshot
53 | // call updateCustomers with a few customers
54 | // take another snapshot
55 | // WORKSHOP_END
56 | })
57 |
58 | test('unsubscribe when unmounted', () => {
59 | // FINAL_START
60 | const {unsubscribe, store} = getStoreStub()
61 | const wrapper = mountCustomerList({store})
62 | wrapper.unmount()
63 | expect(unsubscribe).toHaveBeenCalledTimes(1)
64 | // FINAL_END
65 | // WORKSHOP_START
66 | // we want to make sure that the unsubscribe function is called on the store
67 | // so get the store stub and the unsubscribe mock function from '../store/Customers.stub'
68 | // Then use enzyme's `mount` function to mount `./CustomerList` with the store stub.
69 | // Take the resulting wrapper from that `mount` and unmount it by calling `wrapper.unmount`
70 | // Then assert that the `unsubscribe` mock was called once with toHaveBeenCalledTimes(1)
71 | // WORKSHOP_END
72 | })
73 |
74 | // FINAL_START
75 | /**
76 | * Render the and snapshot it
77 | * @param {Object} props - the props to render with
78 | */
79 | function snapshotCustomerList(props = {}) {
80 | const wrapper = renderCustomerList(props)
81 | expect(renderToJson(wrapper)).toMatchSnapshot()
82 | }
83 |
84 | /**
85 | * Renders with the given props
86 | * @param {Object} props - the props to render with
87 | * @return {Object} the rendered component
88 | */
89 | function renderCustomerList({store = getStoreStub().store}) {
90 | return render()
91 | }
92 |
93 | /**
94 | * Mounts with the given props
95 | * @param {Object} props - the props to mount with
96 | * @return {Object} the rendered component
97 | */
98 | function mountCustomerList({store = getStoreStub().store}) {
99 | return mount()
100 | }
101 | // FINAL_END
102 | // WORKSHOP_START
103 | // Create a snapshotCustomerList function that:
104 | // 1. Accepts props
105 | // 2. Creates a component with those props with a call to renderer.create (tip: you may wanna do this in a separate function)
106 | // 3. Asserts on a snapshot of that component with expect(component).toMatchSnapshot()
107 | // Create a renderCustomerList function that:
108 | // 1. Accepts props and defaults the store to the store stub
109 | // 2. Returns a render the CustomerList with those propse
110 | // Create a mountCustomerList function that:
111 | // 1. Accepts props and defaults the store to the store stub
112 | // 2. Returns a mount the CustomerList with those propse
113 | // WORKSHOP_END
114 |
--------------------------------------------------------------------------------
/templates/containers/README.md:
--------------------------------------------------------------------------------
1 | # CustomerList
2 |
3 | This demonstrates how to test a component that has conditional logic in its
4 | render method. More usefully, it demonstrates how you can test components
5 | that re-render based off of updates from an external data source. In this
6 | case this is a (non-flux) Customer Store.
7 |
8 | With Redux, doing this test is simpler because you don't concern yourself
9 | with state in your components and they're simply passed the state via props.
10 |
11 | In this scenario, we're depending on a "singleton" store for our application
12 | and our component's lifecycle hooks are subscribing to this store. This can
13 | cause issues with our tests potentially mucking with the state of other tests.
14 | To combat this, if we make it possible to override the store we're using (by
15 | utilizing `defaultProps`) then we can easily pass our own stubbed version of
16 | this store to avoid issues with sharing a singleton across tests and allow
17 | us to perform assertions on properties of this stubbed store.
18 |
19 |
--------------------------------------------------------------------------------
/templates/jest.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "testPathDirs": [
3 | // WORKSHOP_START
4 | "exercises"
5 | // WORKSHOP_END
6 | // FINAL_START
7 | "exercises-final"
8 | // FINAL_END
9 | ],
10 | "coverageThreshold": {
11 | "global": {
12 | "branches": 100,
13 | "functions": 95,
14 | "lines": 100,
15 | "statements": 100
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/templates/store/Customers.js:
--------------------------------------------------------------------------------
1 | let _customers = []
2 | const callbacks = []
3 |
4 | export default {
5 | getCustomers,
6 | setCustomers,
7 | subscribe,
8 | }
9 |
10 | /**
11 | * Returns the current list of customers
12 | * @return {Array} customers
13 | */
14 | function getCustomers() {
15 | return _customers
16 | }
17 |
18 | /**
19 | * Sets the current list of customers to the given customers
20 | * and lets all the subscribers know about the update
21 | * @param {Array} customers - An array of objects that have a name property that is a string
22 | */
23 | function setCustomers(customers) {
24 | _customers = customers
25 | _letSubscribersKnow()
26 | }
27 |
28 | /**
29 | * Adds the given callback to a list of functions to be called when the current customers are set
30 | * @param {Function} callback - the callback to be called
31 | * @return {Function} - a function to call to unsubscribe
32 | */
33 | function subscribe(callback) {
34 | callbacks.push(callback)
35 | return function removeCallback() {
36 | callbacks.splice(callbacks.indexOf(callback), 1)
37 | }
38 | }
39 |
40 | /**
41 | * Iterates through all callbacks and calls them
42 | */
43 | function _letSubscribersKnow() {
44 | callbacks.forEach(cb => cb())
45 | }
46 |
--------------------------------------------------------------------------------
/templates/store/Customers.stub.js:
--------------------------------------------------------------------------------
1 | export default getStoreStub
2 |
3 | /**
4 | * Create a stub for the store which can be used for assertions
5 | * @param {Array} customers - the array of customers
6 | * @returns {Object} - ref property has customers and will haf ref.callback when
7 | * store.callback is invoked. store.getCustomers will return ref.customers
8 | */
9 | function getStoreStub(customers = []) {
10 | let callback
11 | const unsubscribe = jest.fn()
12 | const ref = {customers}
13 |
14 | const store = {
15 | getCustomers: () => ref.customers,
16 | subscribe: cb => {
17 | callback = cb
18 | return unsubscribe
19 | },
20 | }
21 | return {unsubscribe, store, updateCustomers}
22 |
23 | function updateCustomers(newCustomers) {
24 | ref.customers = newCustomers
25 | callback()
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/templates/store/Customers.test.js:
--------------------------------------------------------------------------------
1 | test('should start with empty', () => {
2 | // FINAL_START
3 | const {store} = setup()
4 | const customers = store.getCustomers()
5 | expect(customers.length).toBe(0)
6 | // FINAL_END
7 | // WORKSHOP_START
8 | // get the store from your setup function
9 | // call getCustomers on it
10 | // assert that the lenth of customers is 0
11 | // WORKSHOP_END
12 | })
13 |
14 | test('should allow you to set customers and get them', () => {
15 | // FINAL_START
16 | const {store} = setup()
17 | const c0 = {name: 'Bill'}
18 | const c1 = {name: 'Francine'}
19 | store.setCustomers([c0, c1])
20 | const customers = store.getCustomers()
21 | const [sc0, sc1] = customers
22 | expect(customers.length).toBe(2)
23 | expect(c0).toBe(sc0)
24 | expect(c1).toBe(sc1)
25 | // FINAL_END
26 | // WORKSHOP_START
27 | // get the store
28 | // create two customers and set the store to them
29 | // get the customers from the store
30 | // assert that there are two customers
31 | // assert that the customers you got are the ones you set
32 | // WORKSHOP_END
33 | })
34 |
35 | test('should allow you to subscribe to the store', () => {
36 | // FINAL_START
37 | const {store} = setup()
38 | const subscriber = jest.fn()
39 | const unsubscribe = store.subscribe(subscriber)
40 | store.setCustomers([])
41 | expect(subscriber).toHaveBeenCalledTimes(1)
42 | subscriber.mockClear()
43 | unsubscribe()
44 | store.setCustomers([])
45 | expect(subscriber).not.toBeCalled()
46 | // FINAL_END
47 | // WORKSHOP_START
48 | // get the store
49 | // setup a jest mock function (jest.fn()) for your subscriber
50 | // subscribe to the store with that function
51 | // call setCustomers
52 | // assert your subscriber was called once
53 | // clear your subscriber mock function (subscriber.mockClear())
54 | // call the unsubscribe function you got when subscribing
55 | // call setCustomers
56 | // assert that your mock function was not called
57 | // WORKSHOP_END
58 | })
59 |
60 | // FINAL_START
61 | /**
62 | * Prepares our environment for an individual test and returns whatever is needed for that test to run.
63 | * @return {Object} what is needed for tests to run. In this case it is only a fresh copy of the store
64 | */
65 | function setup() {
66 | // clear the require cache so when we require the store we get a fresh copy
67 | jest.resetModules()
68 | const store = require('./Customers').default
69 | return {store}
70 | }
71 | // FINAL_END
72 | // WORKSHOP_START
73 | // Create a `setup` function:
74 | // clear the require cache with jest.resetModules() so you can require a fresh copy of the store
75 | // require the ./Customers module (note: because it's using `export default`,
76 | // the store is on the `default` property of what you're requiring)
77 | // return {store}
78 | // WORKSHOP_END
79 |
--------------------------------------------------------------------------------
/templates/store/README.md:
--------------------------------------------------------------------------------
1 | # Old-school
2 |
3 | Not all applications use Redux 😞. I hacked together this (non-flux) Customers store
4 | to pretty much demonstrate how you test dealing with updates that happen to your
5 | components when they're updated from an external source of data (like a store).
6 |
7 | I don't recommend you write your stores this way.
8 |
9 | This store is tested, but there's not really much going on in here...
10 |
--------------------------------------------------------------------------------