├── .gitignore ├── LICENSE ├── README.md ├── hp.jpg ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json └── src ├── .gitkeep ├── App.css ├── App.js ├── App.test.js ├── ButtonWithTextInput.js ├── ButtonWithTextInput.test.js ├── __snapshots__ └── ButtonWithTextInput.test.js.snap ├── elements ├── CustomButton.css ├── CustomButton.js ├── CustomButton.test.js ├── CustomTextInput.css ├── CustomTextInput.js ├── CustomTextInput.test.js ├── __snapshots__ │ ├── CustomButton.test.js.snap │ └── CustomTextInput.test.js.snap └── index.js ├── index.css ├── index.js └── setupTests.js /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | .vscode 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Mihail Gaberov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UI (React) Unit Testing 2 | "What should we test in our React components" - working examples. 3 | 4 | You can view the [presentation here](http://mihailgaberov.github.io/testing-reactjs-presentation) and the demo app [here](http://mihailgaberov.github.io/testing-reactjs-presentation-examples). 5 | 6 | ![Screenshot](https://github.com/mihailgaberov/testing-reactjs-examples/blob/master/hp.jpg) 7 | -------------------------------------------------------------------------------- /hp.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihailgaberov/testing-reactjs-examples/d63c1cca6df243f185330df6c9ddf5bdceeeebc5/hp.jpg -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "testing-reactjs-examples", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "react": "^16.14.0", 7 | "react-dom": "^16.14.0", 8 | "react-scripts": "5.0.1" 9 | }, 10 | "scripts": { 11 | "start": "react-scripts start", 12 | "build": "react-scripts build", 13 | "test": "react-scripts test --env=jsdom", 14 | "eject": "react-scripts eject" 15 | }, 16 | "devDependencies": { 17 | "enzyme": "^3.3.0", 18 | "enzyme-adapter-react-16": "^1.15.6", 19 | "enzyme-to-json": "^3.3.4", 20 | "sinon": "^5.1.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihailgaberov/testing-reactjs-examples/d63c1cca6df243f185330df6c9ddf5bdceeeebc5/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | UI (React) Unit Testing 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`CustomButton component renders correctly 1`] = ` 4 | 12 | `; 13 | -------------------------------------------------------------------------------- /src/.gitkeep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | background-color: #222; 4 | color: navajowhite; 5 | height: 100vh; 6 | } 7 | 8 | .App-title { 9 | font-size: 1.5em; 10 | } 11 | 12 | .App-intro { 13 | font-size: large; 14 | } 15 | .App-image { 16 | width: 800px; 17 | } 18 | a { 19 | color: orange; 20 | font: italic 1.2em "Fira Sans", serif; 21 | } -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import './App.css'; 3 | import hp from '../../hp.jpg'; 4 | import ButtonWithTextInput from './ButtonWithTextInput'; 5 | 6 | class App extends Component { 7 | render() { 8 | return ( 9 |
10 | Testing ReactJS 11 | 12 | 13 |
14 | ); 15 | } 16 | } 17 | 18 | export default App; 19 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /src/ButtonWithTextInput.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { 5 | CustomButton, 6 | CustomTextInput, 7 | } from "./elements"; 8 | 9 | class ButtonWithTextInput extends React.Component { 10 | state = { 11 | textInputValue: this.props.textInputValue 12 | }; 13 | 14 | dummyMethod = () => 3 + 5; 15 | setNewCityName = (name) => this.setState({ textInputValue: 'Santa Maria' }); 16 | 17 | /** Showing an example of internal implementation of the click handler 18 | * that we should not care about when testing it as long as the results 19 | * are what we expect. 20 | */ 21 | onClick = () => { 22 | // Inner implemtation of the Some other methods might be here ... 23 | this.dummyMethod(); 24 | this.setNewCityName('Santa Maria'); 25 | } 26 | 27 | render() { 28 | const { btnLabel } = this.props; 29 | const { textInputValue } = this.state; 30 | 31 | return ( 32 |
33 | 34 | 35 |
36 | ); 37 | } 38 | } 39 | 40 | ButtonWithTextInput.propTypes = { 41 | btnLabel: PropTypes.string.isRequired, 42 | textInputValue: PropTypes.string.isRequired, 43 | }; 44 | 45 | export default ButtonWithTextInput; -------------------------------------------------------------------------------- /src/ButtonWithTextInput.test.js: -------------------------------------------------------------------------------- 1 | import ButtonWithTextInput from "./ButtonWithTextInput"; 2 | 3 | it("renders correctly", () => { 4 | const wrapper = shallow( 5 | 6 | ); 7 | 8 | expect(wrapper).toMatchSnapshot(); 9 | }); 10 | 11 | it("renders correctly again", () => { 12 | const wrapper = render( 13 | 14 | ); 15 | 16 | expect(wrapper).toMatchSnapshot(); 17 | }); 18 | 19 | it("has correct button label", () => { 20 | const wrapper = mount( 21 | 22 | ); 23 | const text = wrapper.find("CustomButton").text(); 24 | expect(text).toEqual("Sie buton"); 25 | }); 26 | 27 | it("has correct text input value", () => { 28 | const wrapper = mount( 29 | 30 | ); 31 | const text = wrapper.find("CustomTextInput").props().textInputValue; 32 | expect(text).toEqual("Zurich"); 33 | }); 34 | 35 | it("test visual result after click - call setNewCityName", () => { 36 | const wrapper = mount( 37 | 38 | ); 39 | 40 | wrapper 41 | .find("button") 42 | .first() 43 | .simulate("click"); 44 | 45 | expect(wrapper.find("CustomTextInput").props().textInputValue).toEqual("Santa Maria"); 46 | }); -------------------------------------------------------------------------------- /src/__snapshots__/ButtonWithTextInput.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renders correctly 1`] = ` 4 |
5 | 8 | 12 |
13 | `; 14 | 15 | exports[`renders correctly again 1`] = ` 16 |
17 | 24 | 25 | 26 | 33 |
34 | `; 35 | -------------------------------------------------------------------------------- /src/elements/CustomButton.css: -------------------------------------------------------------------------------- 1 | .custom-btn { 2 | padding: 28px; 3 | font: bold 1.8em "Consolas", serif; 4 | cursor: pointer; 5 | } -------------------------------------------------------------------------------- /src/elements/CustomButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import './CustomButton.css' 5 | 6 | const CustomButton = ({btnLabel, clickHandler}) => { 7 | return ; 8 | } 9 | 10 | CustomButton.propTypes = { 11 | btnLabel: PropTypes.string.isRequired, 12 | clickHandler: PropTypes.func.isRequired, 13 | }; 14 | 15 | export default CustomButton; -------------------------------------------------------------------------------- /src/elements/CustomButton.test.js: -------------------------------------------------------------------------------- 1 | import CustomButton from "./CustomButton"; 2 | 3 | describe("CustomButton component", () => { 4 | it("renders correctly", () => { 5 | const wrapper = shallow( 6 | 7 | ); 8 | 9 | expect(wrapper).toMatchSnapshot(); 10 | }); 11 | 12 | it("should call setNewCityName on click", () => { 13 | const spy = sinon.spy(); 14 | 15 | const wrapper = mount( 16 | 17 | ); 18 | wrapper 19 | .find("button") 20 | .first() 21 | .simulate("click"); 22 | 23 | expect(spy.calledOnce).toBe(true); 24 | }); 25 | }); -------------------------------------------------------------------------------- /src/elements/CustomTextInput.css: -------------------------------------------------------------------------------- 1 | .custom-text-input { 2 | width: 300px; 3 | padding: 28px; 4 | margin: 28px; 5 | font: bold 1.8em "Consolas", serif; 6 | } -------------------------------------------------------------------------------- /src/elements/CustomTextInput.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import './CustomTextInput.css'; 5 | 6 | const CustomTextInput = ({textInputValue}) => 7 | 8 | CustomTextInput.propTypes = { 9 | textInputValue: PropTypes.string.isRequired, 10 | }; 11 | 12 | export default CustomTextInput; -------------------------------------------------------------------------------- /src/elements/CustomTextInput.test.js: -------------------------------------------------------------------------------- 1 | import CustomTextInput from "./CustomTextInput"; 2 | 3 | describe("CustomTextInput component", () => { 4 | it("renders correctly", () => { 5 | const wrapper = shallow( 6 | 7 | ); 8 | 9 | expect(wrapper).toMatchSnapshot(); 10 | }); 11 | 12 | it("should display proper text value", () => { 13 | const wrapper = shallow( 14 | 15 | ); 16 | 17 | expect((wrapper).find('input').props().value).toEqual('test'); 18 | }); 19 | }); -------------------------------------------------------------------------------- /src/elements/__snapshots__/CustomButton.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`CustomButton component renders correctly 1`] = ` 4 | 12 | `; 13 | -------------------------------------------------------------------------------- /src/elements/__snapshots__/CustomTextInput.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`CustomTextInput component renders correctly 1`] = ` 4 | 11 | `; 12 | -------------------------------------------------------------------------------- /src/elements/index.js: -------------------------------------------------------------------------------- 1 | import CustomButton from './CustomButton'; 2 | import CustomTextInput from './CustomTextInput'; 3 | 4 | export { 5 | CustomButton, 6 | CustomTextInput 7 | } -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | 6 | ReactDOM.render(, document.getElementById('root')); 7 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Enzyme, { shallow, render, mount } from "enzyme"; 3 | import Adapter from "enzyme-adapter-react-16"; 4 | import { createSerializer } from "enzyme-to-json"; 5 | import sinon from "sinon"; 6 | 7 | // Set the default serializer for Jest to be the from enzyme-to-json 8 | // This produces an easier to read (for humans) serialized format. 9 | expect.addSnapshotSerializer(createSerializer({ mode: "deep" })); 10 | 11 | // React 16 Enzyme adapter 12 | Enzyme.configure({ adapter: new Adapter() }); 13 | 14 | // Define globals to cut down on imports in test files 15 | global.React = React; 16 | global.shallow = shallow; 17 | global.render = render; 18 | global.mount = mount; 19 | global.sinon = sinon; --------------------------------------------------------------------------------