From 5d7050b2159afddaabed7fe8b8a0838777faee62 Mon Sep 17 00:00:00 2001 From: "Kent C. Dodds" Date: Mon, 16 Apr 2018 12:13:03 -0600 Subject: [PATCH] some progress here --- INSTRUCTIONS.md | 32 +++- client/src/__tests__/app.login.todo.js | 2 +- .../__snapshots__/login.step-4.js.snap | 148 ++++++++++++++++++ .../src/components/__tests__/login.final.js | 20 ++- .../src/components/__tests__/login.step-2.js | 8 +- .../components/__tests__/login.step-2.todo.js | 6 +- .../src/components/__tests__/login.step-3.js | 28 ++++ .../components/__tests__/login.step-3.todo.js | 30 ++-- .../src/components/__tests__/login.step-4.js | 41 +++++ .../components/__tests__/login.step-4.todo.js | 46 ++++++ client/src/screens/__tests__/editor.js | 18 ++- client/test/til-client-test-utils.js | 12 +- other/jest.config.js | 1 + other/simple-react/.babelrc | 3 + other/simple-react/__tests__/item-list.js | 17 ++ .../simple-react/__tests__/item-list.todo.js | 42 +++++ other/simple-react/item-list.js | 11 ++ other/simple-react/jest.config.js | 3 + package.json | 1 + 19 files changed, 431 insertions(+), 38 deletions(-) create mode 100644 client/src/components/__tests__/__snapshots__/login.step-4.js.snap create mode 100644 client/src/components/__tests__/login.step-3.js create mode 100644 client/src/components/__tests__/login.step-4.js create mode 100644 client/src/components/__tests__/login.step-4.todo.js create mode 100644 other/simple-react/.babelrc create mode 100644 other/simple-react/__tests__/item-list.js create mode 100644 other/simple-react/__tests__/item-list.todo.js create mode 100644 other/simple-react/item-list.js create mode 100644 other/simple-react/jest.config.js diff --git a/INSTRUCTIONS.md b/INSTRUCTIONS.md index da06d1f7..85d1eb13 100644 --- a/INSTRUCTIONS.md +++ b/INSTRUCTIONS.md @@ -31,6 +31,7 @@ through it on your own if you like. * [What types of testing are there?](#what-types-of-testing-are-there-1) * [What's a test](#whats-a-test-1) * [Intro to Jest](#intro-to-jest) + * [Testing a React Component](#testing-a-react-component) * [Configuring Jest](#configuring-jest) * [Unit testing components](#unit-testing-components) * [Effective Snapshot Testing](#effective-snapshot-testing) @@ -39,9 +40,10 @@ through it on your own if you like. * [End-to-end testing](#end-to-end-testing) * [Write tests. Not too many. Mostly integration.](#write-tests-not-too-many-mostly-integration-1) * [Shared Content](#shared-content) + * [What's a test](#whats-a-test-2) * [What types of testing are there?](#what-types-of-testing-are-there-2) * [Jest](#jest) - * [Code Coverage](#code-coverage) + * [Code Coverage](#code-coverage) * [Write tests. Not too many. Mostly integration.](#write-tests-not-too-many-mostly-integration-2) @@ -284,23 +286,43 @@ See below in the shared content * Configure Cypress for a web application * Write E2E (end-to-end) tests with Cypress -### What's a test +### What types of testing are there? See below in the shared content -### What types of testing are there? +### What's a test + +See below in the shared content > NOTE: This is duplicate content from the practices and principles workshop > In this one however, folks should just watch the instructor go through things > to make time for the rest of the content and not bore those who have already > gone through this material. -See below in the shared content - ### Intro to Jest See below in the shared content +### Testing a React Component + +**Instruction**: + +* Nothing much here, direct people to the exercise and inform them they can + use the solution for reference + +**Exercise**: + +* Start the simple react tests in watch mode with `npm run test:react` +* Open `other/simple-react/item-list.js` and `other/simple-react/__tests__/item-list.todo.js` +* Follow the instructions to test the component + +**Takeaways** + +* The key here is to render the component and assert on the output. +* Assuming this were the only component for your entire application, attempt to + use it the way the user would and let that inform your decisions of how you + test it. + ### Configuring Jest **New Things**: diff --git a/client/src/__tests__/app.login.todo.js b/client/src/__tests__/app.login.todo.js index 6b0329cb..a8c60f73 100644 --- a/client/src/__tests__/app.login.todo.js +++ b/client/src/__tests__/app.login.todo.js @@ -32,7 +32,7 @@ test('login as an existing user', async () => { // 3. Change submitted from `false` to `true` // 4. And you're all done! /* -http://ws.kcd.im/?ws=Testing&e=login.step-3&em= +http://ws.kcd.im/?ws=Testing&e=app.login&em= */ test.skip('I submitted my elaboration and feedback', () => { const submitted = false // change this when you've submitted! diff --git a/client/src/components/__tests__/__snapshots__/login.step-4.js.snap b/client/src/components/__tests__/__snapshots__/login.step-4.js.snap new file mode 100644 index 00000000..fd9d0402 --- /dev/null +++ b/client/src/components/__tests__/__snapshots__/login.step-4.js.snap @@ -0,0 +1,148 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`snapshot 1`] = ` +.css-0, +[data-css-0] { + display: -webkit-box; + display: -moz-box; + display: -ms-flexbox; + display: -webkit-flex; + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + min-width: 200px; + max-width: 400px; + margin-left: -116px; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -webkit-flex-direction: column; + -webkit-box-align: center; + -webkit-align-items: center; +} + +.css-1, +[data-css-1] { + display: grid; + grid-template-columns: 100px 1fr; + grid-gap: 16px; + font-size: 18px; + align-items: center; + margin-top: 30px; + margin-bottom: 30px; + width: 100%; + -ms-grid-template-columns: 100px 1fr; + -ms-grid-gap: 16px; + -webkit-box-align: center; + -webkit-align-items: center; +} + +.css-2, +[data-css-2] { + background: white; + height: 50px; + border: none; + border-radius: 10px; + box-shadow: var(--shadow); + border-bottom: 5px; + width: 100%; + min-width: 150px; + display: block; + padding-left: 10px; +} + +.css-2::-webkit-input-placeholder, +[data-css-2]::-webkit-input-placeholder { + opacity: 0.5; +} + +.css-2::-moz-placeholder, +[data-css-2]::-moz-placeholder { + opacity: 0.5; +} + +.css-2::-ms-input-placeholder, +[data-css-2]::-ms-input-placeholder { + opacity: 0.5; +} + +.css-2::placeholder, +[data-css-2]::placeholder { + opacity: 0.5; +} + +.css-7, +[data-css-7] { + font-size: 13px; + font-family: Raleway,sans-serif; + background: var(--green); + padding: 10px 20px; + display: block; + margin-left: auto; + color: white; + border: none; + border-radius: 10px; + box-shadow: var(--shadow); + cursor: pointer; + transition: 0.5s; + -webkit-transition: 0.5s; + -moz-transition: 0.5s; +} + +.css-7:hover, +[data-css-7]:hover { + box-shadow: var(--shadowHover); +} + +@media only screen and (max-width: 819px) { + .css-0, + [data-css-0] { + margin-left: 0; + width: 90%; + } +} + +
+
+
+
+ + + + +
+ +
+
+
+`; diff --git a/client/src/components/__tests__/login.final.js b/client/src/components/__tests__/login.final.js index a1683f69..506ec2fe 100644 --- a/client/src/components/__tests__/login.final.js +++ b/client/src/components/__tests__/login.final.js @@ -1,29 +1,33 @@ import React from 'react' -import {generate, render, Simulate} from 'til-client-test-utils' +import { + generate, + renderIntoDocument, + cleanup, + render, +} from 'til-client-test-utils' import Login from '../login' +afterEach(cleanup) + test('calls onSubmit with the username and password when submitted', () => { // Arrange const fakeUser = generate.loginForm() const handleSubmit = jest.fn() - const {container, queryByLabelText, queryByText} = render( + const {getByLabelText, getByText} = renderIntoDocument( , ) - const usernameNode = queryByLabelText('username') - const passwordNode = queryByLabelText('password') - const formNode = container.querySelector('form') - const submitButtonNode = queryByText('submit') + const usernameNode = getByLabelText('username') + const passwordNode = getByLabelText('password') // Act usernameNode.value = fakeUser.username passwordNode.value = fakeUser.password - Simulate.submit(formNode) + getByText('submit').click() // Assert expect(handleSubmit).toHaveBeenCalledTimes(1) expect(handleSubmit).toHaveBeenCalledWith(fakeUser) - expect(submitButtonNode.type).toBe('submit') }) test('snapshot', () => { diff --git a/client/src/components/__tests__/login.step-2.js b/client/src/components/__tests__/login.step-2.js index df6177e6..84f56633 100644 --- a/client/src/components/__tests__/login.step-2.js +++ b/client/src/components/__tests__/login.step-2.js @@ -6,14 +6,14 @@ test('calls onSubmit with the username and password when submitted', () => { // Arrange const fakeUser = generate.loginForm() const handleSubmit = jest.fn() - const {container, queryByLabelText, queryByText} = render( + const {container, getByLabelText, getByText} = render( , ) - const usernameNode = queryByLabelText('username') - const passwordNode = queryByLabelText('password') + const usernameNode = getByLabelText('username') + const passwordNode = getByLabelText('password') const formNode = container.querySelector('form') - const submitButtonNode = queryByText('submit') + const submitButtonNode = getByText('submit') // Act usernameNode.value = fakeUser.username diff --git a/client/src/components/__tests__/login.step-2.todo.js b/client/src/components/__tests__/login.step-2.todo.js index fe8f2071..2871e37d 100644 --- a/client/src/components/__tests__/login.step-2.todo.js +++ b/client/src/components/__tests__/login.step-2.todo.js @@ -2,6 +2,8 @@ import React from 'react' import ReactDOM from 'react-dom' // you'll need these: // import {generate, render, Simulate} from 'til-client-test-utils' +// note that til-client-test-utils is found in `client/test/til-client-test-utils` +// and it re-exports some utilities from react-testing-library (like render and Simulate) import Login from '../login' test('calls onSubmit with the username and password when submitted', () => { @@ -11,7 +13,7 @@ test('calls onSubmit with the username and password when submitted', () => { const handleSubmit = jest.fn() // use: render() // It'll give you back an object with - // `queryByLabelText` and `queryByText` functions + // `getByLabelText` and `getByText` functions // so you don't need a div anymore! const div = document.createElement('div') ReactDOM.render(, div) @@ -44,7 +46,7 @@ test('calls onSubmit with the username and password when submitted', () => { // 3. Change submitted from `false` to `true` // 4. And you're all done! /* -http://ws.kcd.im/?ws=Testing&e=login.step-2&em= +http://ws.kcd.im/?ws=Testing&e=login.step-2%20(react-testing-library)&em= */ test.skip('I submitted my elaboration and feedback', () => { const submitted = false // change this when you've submitted! diff --git a/client/src/components/__tests__/login.step-3.js b/client/src/components/__tests__/login.step-3.js new file mode 100644 index 00000000..909b582f --- /dev/null +++ b/client/src/components/__tests__/login.step-3.js @@ -0,0 +1,28 @@ +import React from 'react' +import {generate, renderIntoDocument, cleanup} from 'til-client-test-utils' +import Login from '../login' + +// If you render your components with renderIntoDocument via react-testing-library +// then you can get automatic cleanup of any components rendered like this: +afterEach(cleanup) + +test('calls onSubmit with the username and password when submitted', () => { + // Arrange + const fakeUser = generate.loginForm() + const handleSubmit = jest.fn() + const {getByLabelText, getByText} = renderIntoDocument( + , + ) + + const usernameNode = getByLabelText('username') + const passwordNode = getByLabelText('password') + + // Act + usernameNode.value = fakeUser.username + passwordNode.value = fakeUser.password + getByText('submit').click() + + // Assert + expect(handleSubmit).toHaveBeenCalledTimes(1) + expect(handleSubmit).toHaveBeenCalledWith(fakeUser) +}) diff --git a/client/src/components/__tests__/login.step-3.todo.js b/client/src/components/__tests__/login.step-3.todo.js index ec3debda..2e51c26d 100644 --- a/client/src/components/__tests__/login.step-3.todo.js +++ b/client/src/components/__tests__/login.step-3.todo.js @@ -2,18 +2,33 @@ import React from 'react' import {generate, render, Simulate} from 'til-client-test-utils' import Login from '../login' +// Due to the fact that our element is not in the document, the +// click event on the submit button will not be treated as a +// submit event on the form which is why we're simulating a submit +// event on the form rather than clicking the button and then +// asserting the button's type is set to submit rather than just +// clicking on the button. +// +// Alternatively, we could actually insert the element directly into +// the document, then click on the button and that should work! +// Try doing that! +// (Tip: document.body.appendChild(container) and getByText('submit').click()) +// +// Bonus: Don't forget to cleanup after yourselve when you're finished so you don't +// have things hanging out in the document! + test('calls onSubmit with the username and password when submitted', () => { // Arrange const fakeUser = generate.loginForm() const handleSubmit = jest.fn() - const {container, queryByLabelText, queryByText} = render( + const {container, getByLabelText, getByText} = render( , ) - const usernameNode = queryByLabelText('username') - const passwordNode = queryByLabelText('password') + const usernameNode = getByLabelText('username') + const passwordNode = getByLabelText('password') const formNode = container.querySelector('form') - const submitButtonNode = queryByText('submit') + const submitButtonNode = getByText('submit') // Act usernameNode.value = fakeUser.username @@ -26,11 +41,6 @@ test('calls onSubmit with the username and password when submitted', () => { expect(submitButtonNode.type).toBe('submit') }) -test('snapshot', () => { - // render the login, this will give you back an object with a `container` property - // expect the `container` property to match a snapshot -}) - //////// Elaboration & Feedback ///////// // When you've finished with the exercises: // 1. Copy the URL below into your browser and fill out the form @@ -38,7 +48,7 @@ test('snapshot', () => { // 3. Change submitted from `false` to `true` // 4. And you're all done! /* -http://ws.kcd.im/?ws=Testing&e=login.step-3&em= +http://ws.kcd.im/?ws=Testing&e=login.step-3%20(renderIntoDocument)&em= */ test.skip('I submitted my elaboration and feedback', () => { const submitted = false // change this when you've submitted! diff --git a/client/src/components/__tests__/login.step-4.js b/client/src/components/__tests__/login.step-4.js new file mode 100644 index 00000000..ba76da8d --- /dev/null +++ b/client/src/components/__tests__/login.step-4.js @@ -0,0 +1,41 @@ +import React from 'react' +import { + generate, + renderIntoDocument, + render, + cleanup, +} from 'til-client-test-utils' +import Login from '../login' + +afterEach(cleanup) + +test('calls onSubmit with the username and password when submitted', () => { + // Arrange + const fakeUser = generate.loginForm() + const handleSubmit = jest.fn() + const {getByLabelText, getByText} = renderIntoDocument( + , + ) + + const usernameNode = getByLabelText('username') + const passwordNode = getByLabelText('password') + + // Act + usernameNode.value = fakeUser.username + passwordNode.value = fakeUser.password + getByText('submit').click() + + // Assert + expect(handleSubmit).toHaveBeenCalledTimes(1) + expect(handleSubmit).toHaveBeenCalledWith(fakeUser) +}) + +test('snapshot', () => { + // note that we don't need to render this into the document so we'll just + // use a regular `render` here. + const {container} = render() + // note that we're snapshotting the firstChild rather than the container. + // That's just because the container will always be the same. A div. + // So no reason to include that in the snapshot. + expect(container.firstChild).toMatchSnapshot() +}) diff --git a/client/src/components/__tests__/login.step-4.todo.js b/client/src/components/__tests__/login.step-4.todo.js new file mode 100644 index 00000000..e0dd2203 --- /dev/null +++ b/client/src/components/__tests__/login.step-4.todo.js @@ -0,0 +1,46 @@ +import React from 'react' +import {generate, renderIntoDocument, cleanup} from 'til-client-test-utils' +import Login from '../login' + +afterEach(cleanup) + +test('calls onSubmit with the username and password when submitted', () => { + // Arrange + const fakeUser = generate.loginForm() + const handleSubmit = jest.fn() + const {getByLabelText, getByText} = renderIntoDocument( + , + ) + + const usernameNode = getByLabelText('username') + const passwordNode = getByLabelText('password') + + // Act + usernameNode.value = fakeUser.username + passwordNode.value = fakeUser.password + getByText('submit').click() + + // Assert + expect(handleSubmit).toHaveBeenCalledTimes(1) + expect(handleSubmit).toHaveBeenCalledWith(fakeUser) +}) + +test('snapshot', () => { + // render the login, this will give you back an object with a `container` property + // expect the `container` property to match a snapshot +}) + +//////// Elaboration & Feedback ///////// +// When you've finished with the exercises: +// 1. Copy the URL below into your browser and fill out the form +// 2. remove the `.skip` from the test below +// 3. Change submitted from `false` to `true` +// 4. And you're all done! +/* +http://ws.kcd.im/?ws=Testing&e=login.step-4%20(snapshots)&em= +*/ +test.skip('I submitted my elaboration and feedback', () => { + const submitted = false // change this when you've submitted! + expect(submitted).toBe(true) +}) +//////////////////////////////// diff --git a/client/src/screens/__tests__/editor.js b/client/src/screens/__tests__/editor.js index 7568b973..4b215be7 100644 --- a/client/src/screens/__tests__/editor.js +++ b/client/src/screens/__tests__/editor.js @@ -1,7 +1,16 @@ import React from 'react' -import {generate, render, Simulate, wait} from 'til-client-test-utils' +import { + generate, + wait, + cleanup, + fireEvent, + renderIntoDocument, + render, +} from 'til-client-test-utils' import Editor from '../editor' +afterEach(cleanup) + test('calls onSubmit with the username and password when submitted', async () => { // Arrange const fakeUser = generate.userData({id: generate.id()}) @@ -12,19 +21,17 @@ test('calls onSubmit with the username and password when submitted', async () => create: jest.fn(() => Promise.resolve()), }, } - const {container, getByText, getByLabelText} = render( + const {getByText, getByLabelText} = renderIntoDocument( , ) getByLabelText('Title').value = fakePost.title getByLabelText('Content').value = fakePost.content getByLabelText('Tags').value = fakePost.tags.join(', ') - const submitButtonNode = getByText('submit') - const formNode = container.querySelector('form') const preDate = Date.now() // Act - Simulate.submit(formNode) + fireEvent.click(getByText('submit')) // Assert expect(fakeApi.posts.create).toHaveBeenCalledTimes(1) @@ -39,7 +46,6 @@ test('calls onSubmit with the username and password when submitted', async () => const date = new Date(fakeApi.posts.create.mock.calls[0][0].date).getTime() expect(date).toBeGreaterThanOrEqual(preDate) expect(date).toBeLessThanOrEqual(postDate) - expect(submitButtonNode.type).toBe('submit') }) test('snapshot', () => { diff --git a/client/test/til-client-test-utils.js b/client/test/til-client-test-utils.js index 459d2d16..55e687ca 100644 --- a/client/test/til-client-test-utils.js +++ b/client/test/til-client-test-utils.js @@ -1,6 +1,6 @@ import React from 'react' import {Router} from 'react-router-dom' -import {render, wait, Simulate} from 'react-testing-library' +import {render, wait} from 'react-testing-library' import {createMemoryHistory} from 'history' import 'jest-dom/extend-expect' import * as generate from 'til-shared/generate' @@ -17,4 +17,12 @@ function renderWithRouter(ui, {route = '/', ...renderOptions} = {}) { } } -export {renderWithRouter, generate, render, wait, Simulate} +export { + Simulate, + wait, + render, + cleanup, + renderIntoDocument, + fireEvent, +} from 'react-testing-library' +export {renderWithRouter, generate} diff --git a/other/jest.config.js b/other/jest.config.js index ed83dcaf..af465b49 100644 --- a/other/jest.config.js +++ b/other/jest.config.js @@ -11,5 +11,6 @@ module.exports = { './other/whats-a-mock', './other/configuration/calculator.solution', './other/jest-expect', + './other/simple-react', ], } diff --git a/other/simple-react/.babelrc b/other/simple-react/.babelrc new file mode 100644 index 00000000..bed2455c --- /dev/null +++ b/other/simple-react/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": "../configuration/calculator.solution/.babelrc.js" +} diff --git a/other/simple-react/__tests__/item-list.js b/other/simple-react/__tests__/item-list.js new file mode 100644 index 00000000..db24e6f9 --- /dev/null +++ b/other/simple-react/__tests__/item-list.js @@ -0,0 +1,17 @@ +import React from 'react' +import ReactDOM from 'react-dom' +import ItemList from '../item-list' + +test('renders "no items" when the item list is empty', () => { + const container = document.createElement('div') + ReactDOM.render(, container) + expect(container.textContent).toMatch('no items') +}) + +test('renders the items in a list', () => { + const container = document.createElement('div') + ReactDOM.render(, container) + expect(container.textContent).toMatch('apple') + expect(container.textContent).toMatch('orange') + expect(container.textContent).toMatch('pear') +}) diff --git a/other/simple-react/__tests__/item-list.todo.js b/other/simple-react/__tests__/item-list.todo.js new file mode 100644 index 00000000..e0e75a76 --- /dev/null +++ b/other/simple-react/__tests__/item-list.todo.js @@ -0,0 +1,42 @@ +// Your job: +// Test the case where the items provided is empty: +// +// Test the case where there are items in the list: +// +// +// Don't overthink it. This is just a practice run to warm you up +// to testing react components. + +// So you can use JSX (which transpiles down to React.createElement): +// import React from 'react' +// +// So you can render the component for testing: +// import ReactDOM from 'react-dom' +// +// So you can create a react element for the component you're testing: +// import ItemList from '../item-list' + +// and here's an outline example of your first test: +// Create a "container" to render your component into (tip: use document.createElement('div')) +// +// Render your component (tip: use ReactDOM.render(JSX, container)) +// +// Make your assertion(s) on the textContent of the container +// (tip: expect's toMatch function might be what you want) +// +// For your second test, it will be very similar to the first. + +//////// Elaboration & Feedback ///////// +// When you've finished with the exercises: +// 1. Copy the URL below into your browser and fill out the form +// 2. remove the `.skip` from the test below +// 3. Change submitted from `false` to `true` +// 4. And you're all done! +/* +http://ws.kcd.im/?ws=Testing&e=basic%20react%20test&em= +*/ +test.skip('I submitted my elaboration and feedback', () => { + const submitted = false // change this when you've submitted! + expect(submitted).toBe(true) +}) +//////////////////////////////// diff --git a/other/simple-react/item-list.js b/other/simple-react/item-list.js new file mode 100644 index 00000000..4482225b --- /dev/null +++ b/other/simple-react/item-list.js @@ -0,0 +1,11 @@ +import React from 'react' + +function ItemList({items}) { + return items.length ? ( +
    {items.map(i =>
  • {i}
  • )}
+ ) : ( + 'no items' + ) +} + +export default ItemList diff --git a/other/simple-react/jest.config.js b/other/simple-react/jest.config.js new file mode 100644 index 00000000..0dd0e5cd --- /dev/null +++ b/other/simple-react/jest.config.js @@ -0,0 +1,3 @@ +module.exports = { + displayName: 'react', +} diff --git a/package.json b/package.json index fe969e42..69a4a93e 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "test:run": "jest --config ./other/jest.config.js --coverage", "test:mock": "jest --config ./other/whats-a-mock/jest.config.js --watch", "test:expect": "jest --config ./other/jest-expect/jest.config.js --watch", + "test:react": "jest --config ./other/simple-react/jest.config.js --watch", "pretest:e2e:run": "npm run build --silent", "test:e2e:run": "as-a E2E npm-run-all --parallel --race start:core cy:run", "test:e2e": "as-a E2E npm-run-all --parallel --race dev:core cy:open",