├── .babelrc ├── .github └── FUNDING.yml ├── .gitignore ├── .prettierrc ├── .travis.yml ├── README.md ├── cypress.json ├── cypress ├── fixtures │ └── example.json ├── integration │ └── App.e2e.js ├── plugins │ └── index.js └── support │ ├── commands.js │ └── index.js ├── dist └── index.html ├── package-lock.json ├── package.json ├── src ├── App.js ├── App.spec.js └── index.js ├── test ├── dom.js └── helpers.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react" 5 | ] 6 | } -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: rwieruch 4 | patreon: # rwieruch 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with a single custom sponsorship URL 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "es5", 4 | "singleQuote": true, 5 | "printWidth": 70, 6 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - stable 5 | 6 | addons: 7 | apt: 8 | packages: 9 | # Ubuntu 16+ does not install this dependency by default, so we need to install it ourselves 10 | - libgconf-2-4 11 | 12 | install: 13 | - npm install 14 | 15 | script: 16 | - npm run test -- --coverage && npm run test:cypress 17 | 18 | after_script: 19 | - COVERALLS_REPO_TOKEN=$coveralls_repo_token npm run coveralls 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-testing-mocha-chai-enzyme 2 | 3 | [![Build Status](https://travis-ci.org/the-road-to-learn-react/react-testing-mocha-chai-enzyme.svg?branch=master)](https://travis-ci.org/the-road-to-learn-react/react-testing-mocha-chai-enzyme) [![Coverage Status](https://coveralls.io/repos/github/the-road-to-learn-react/react-testing-mocha-chai-enzyme/badge.svg?branch=master)](https://coveralls.io/github/the-road-to-learn-react/react-testing-mocha-chai-enzyme?branch=master) [![Slack](https://slack-the-road-to-learn-react.wieruch.com/badge.svg)](https://slack-the-road-to-learn-react.wieruch.com/) [![Greenkeeper badge](https://badges.greenkeeper.io/the-road-to-learn-react/react-testing-mocha-chai-enzyme.svg)](https://greenkeeper.io/) 4 | 5 | A test setup for React components with Mocha, Chai and Enzyme in a [React + Webpack](https://github.com/the-road-to-learn-react/minimal-react-webpack-babel-setup) application. [Read more about it.](https://www.robinwieruch.de/react-testing-mocha-chai-enzyme-sinon/) 6 | 7 | **Optional:** 8 | 9 | - [Cypress Tutorial](https://www.robinwieruch.de/react-testing-cypress/) 10 | - [CI Tutorial](https://www.robinwieruch.de/javascript-continuous-integration/) 11 | - [Test Coverage Tutorial](https://www.robinwieruch.de/javascript-test-coverage/) 12 | 13 | **Recommended alternative: Instead of Mocha/Chai, [using Jest as test runner and assertion library](https://github.com/the-road-to-learn-react/react-testing-jest-enzyme/).** 14 | 15 | ## Features 16 | 17 | - React 18 | - Webpack 19 | - Testing 20 | - Mocha, Chai, Enzyme: Unit/Integration 21 | - Optional E2E Tests: Cypress 22 | - Optional CI: Travis CI 23 | - Optional Reporting: Coveralls.io 24 | 25 | ## Installation 26 | 27 | - `git clone git@github.com:the-road-to-learn-react/react-testing-mocha-chai-enzyme.git` 28 | - cd react-testing-mocha-chai-enzyme 29 | - npm install 30 | - npm start 31 | - visit `http://localhost:8080/` 32 | - npm test 33 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "video": false, 3 | "baseUrl": "http://localhost:8080" 4 | } -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } -------------------------------------------------------------------------------- /cypress/integration/App.e2e.js: -------------------------------------------------------------------------------- 1 | describe('App E2E', () => { 2 | it('should have a header', () => { 3 | cy.visit('/'); 4 | 5 | cy.get('h1') 6 | .should('have.text', 'My Counter'); 7 | }); 8 | 9 | it('should increment and decrement the counter', () => { 10 | cy.visit('/'); 11 | 12 | cy.get('p') 13 | .should('have.text', '0'); 14 | 15 | cy.contains('Increment').click(); 16 | cy.get('p') 17 | .should('have.text', '1'); 18 | 19 | cy.contains('Increment').click(); 20 | cy.get('p') 21 | .should('have.text', '2'); 22 | 23 | cy.contains('Decrement').click(); 24 | cy.get('p') 25 | .should('have.text', '1'); 26 | }); 27 | }); -------------------------------------------------------------------------------- /cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example plugins/index.js can be used to load plugins 3 | // 4 | // You can change the location of this file or turn off loading 5 | // the plugins file with the 'pluginsFile' configuration option. 6 | // 7 | // You can read more here: 8 | // https://on.cypress.io/plugins-guide 9 | // *********************************************************** 10 | 11 | // This function is called when a project is opened or re-opened (e.g. due to 12 | // the project's config changing) 13 | 14 | module.exports = (on, config) => { 15 | // `on` is used to hook into various events Cypress emits 16 | // `config` is the resolved Cypress config 17 | } 18 | -------------------------------------------------------------------------------- /cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add("login", (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This is will overwrite an existing command -- 25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 26 | -------------------------------------------------------------------------------- /cypress/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The Minimal React Webpack Babel Setup 5 | 6 | 7 |
8 | 9 | 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "minimal-react-webpack-babel-setup", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "webpack-dev-server --config ./webpack.config.js --mode development", 8 | "test": "mocha --require @babel/register --require ./test/helpers.js --require ./test/dom.js --require ignore-styles 'src/**/*.spec.js'", 9 | "test:watch": "npm run test -- --watch", 10 | "test:cypress": "start-server-and-test start http://localhost:8080 cypress", 11 | "cypress": "cypress run", 12 | "coveralls": "cat ./coverage/lcov.info | node node_modules/.bin/coveralls" 13 | }, 14 | "keywords": [], 15 | "author": "", 16 | "license": "ISC", 17 | "devDependencies": { 18 | "@babel/core": "^7.5.5", 19 | "@babel/node": "^7.5.5", 20 | "@babel/preset-env": "^7.5.5", 21 | "@babel/preset-react": "^7.0.0", 22 | "@babel/register": "^7.5.5", 23 | "babel-loader": "^8.0.6", 24 | "chai": "^4.2.0", 25 | "coveralls": "^3.0.5", 26 | "cypress": "^3.4.0", 27 | "enzyme": "^3.10.0", 28 | "enzyme-adapter-react-16": "^1.14.0", 29 | "ignore-styles": "^5.0.1", 30 | "jsdom": "^15.1.1", 31 | "mocha": "^6.2.0", 32 | "react-hot-loader": "^4.12.10", 33 | "sinon": "^7.3.2", 34 | "start-server-and-test": "^1.9.1", 35 | "webpack": "^4.38.0", 36 | "webpack-cli": "^3.3.6", 37 | "webpack-dev-server": "^3.7.2" 38 | }, 39 | "dependencies": { 40 | "axios": "^0.19.0", 41 | "react": "^16.8.6", 42 | "react-dom": "^16.8.6" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import axios from 'axios'; 3 | 4 | export const doIncrement = prevState => ({ 5 | counter: prevState.counter + 1, 6 | }); 7 | 8 | export const doDecrement = prevState => ({ 9 | counter: prevState.counter - 1, 10 | }); 11 | 12 | class App extends Component { 13 | constructor() { 14 | super(); 15 | 16 | this.state = { 17 | counter: 0, 18 | asyncCounters: null, 19 | }; 20 | 21 | this.onIncrement = this.onIncrement.bind(this); 22 | this.onDecrement = this.onDecrement.bind(this); 23 | } 24 | 25 | componentDidMount() { 26 | axios 27 | .get('http://mydomain/counter') 28 | .then(counter => this.setState({ asyncCounters: counter })) 29 | .catch(error => console.log(error)); 30 | } 31 | 32 | onIncrement() { 33 | this.setState(doIncrement); 34 | } 35 | 36 | onDecrement() { 37 | this.setState(doDecrement); 38 | } 39 | 40 | render() { 41 | const { counter } = this.state; 42 | 43 | return ( 44 |
45 |

My Counter

46 | 47 | 48 | 51 | 52 | 55 |
56 | ); 57 | } 58 | } 59 | 60 | export const Counter = ({ counter }) =>

{counter}

; 61 | 62 | export default App; 63 | -------------------------------------------------------------------------------- /src/App.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import axios from 'axios'; 3 | import App, { doIncrement, doDecrement, Counter } from './App'; 4 | 5 | describe('Local State', () => { 6 | it('should increment the counter in state', () => { 7 | const state = { counter: 0 }; 8 | const newState = doIncrement(state); 9 | 10 | expect(newState.counter).to.equal(1); 11 | }); 12 | 13 | it('should decrement the counter in state', () => { 14 | const state = { counter: 0 }; 15 | const newState = doDecrement(state); 16 | 17 | expect(newState.counter).to.equal(-1); 18 | }); 19 | }); 20 | 21 | describe('App Component', () => { 22 | const result = [3, 5, 9]; 23 | const promise = Promise.resolve(result); 24 | 25 | before(() => { 26 | sinon.stub(axios, 'get').withArgs('http://mydomain/counter').returns(promise); 27 | }); 28 | 29 | after(() => { 30 | axios.get.restore(); 31 | }); 32 | 33 | it('renders the Counter wrapper', () => { 34 | const wrapper = shallow(); 35 | expect(wrapper.find(Counter)).to.have.length(1); 36 | }); 37 | 38 | it('passes all props to Counter wrapper', () => { 39 | const wrapper = shallow(); 40 | let counterWrapper = wrapper.find(Counter); 41 | 42 | expect(counterWrapper.props().counter).to.equal(0); 43 | 44 | wrapper.setState({ counter: -1 }); 45 | 46 | counterWrapper = wrapper.find(Counter); 47 | expect(counterWrapper.props().counter).to.equal(-1); 48 | }); 49 | 50 | it('increments the counter', () => { 51 | const wrapper = shallow(); 52 | 53 | wrapper.setState({ counter: 0 }); 54 | wrapper.find('button').at(0).simulate('click'); 55 | 56 | expect(wrapper.state().counter).to.equal(1); 57 | }); 58 | 59 | it('decrements the counter', () => { 60 | const wrapper = shallow(); 61 | 62 | wrapper.setState({ counter: 0 }); 63 | wrapper.find('button').at(1).simulate('click'); 64 | 65 | expect(wrapper.state().counter).to.equal(-1); 66 | }); 67 | 68 | it('calls componentDidMount', () => { 69 | sinon.spy(App.prototype, 'componentDidMount'); 70 | 71 | const wrapper = mount(); 72 | expect(App.prototype.componentDidMount.calledOnce).to.equal(true); 73 | }); 74 | 75 | it('fetches async counters', () => { 76 | const wrapper = shallow(); 77 | 78 | expect(wrapper.state().asyncCounters).to.equal(null); 79 | 80 | promise.then(() => { 81 | expect(wrapper.state().asyncCounters).to.equal(result); 82 | }); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import App from './App'; 5 | 6 | ReactDOM.render( 7 | , 8 | document.getElementById('app') 9 | ); 10 | 11 | module.hot.accept(); -------------------------------------------------------------------------------- /test/dom.js: -------------------------------------------------------------------------------- 1 | import { JSDOM } from 'jsdom'; 2 | 3 | const { window } = new JSDOM(''); 4 | 5 | function copyProps(src, target) { 6 | const props = Object.getOwnPropertyNames(src) 7 | .filter(prop => typeof target[prop] === 'undefined') 8 | .reduce((result, prop) => ({ 9 | ...result, 10 | [prop]: Object.getOwnPropertyDescriptor(src, prop), 11 | }), {}); 12 | Object.defineProperties(target, props); 13 | } 14 | 15 | global.window = window; 16 | global.document = window.document; 17 | global.navigator = { 18 | userAgent: 'node.js', 19 | }; 20 | 21 | copyProps(window, global); -------------------------------------------------------------------------------- /test/helpers.js: -------------------------------------------------------------------------------- 1 | import sinon from 'sinon'; 2 | import { expect } from 'chai'; 3 | import { mount, render, shallow, configure} from 'enzyme'; 4 | import Adapter from 'enzyme-adapter-react-16'; 5 | 6 | configure({ adapter: new Adapter() }); 7 | 8 | global.expect = expect; 9 | 10 | global.sinon = sinon; 11 | 12 | global.mount = mount; 13 | global.render = render; 14 | global.shallow = shallow; -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | 3 | module.exports = { 4 | entry: './src/index.js', 5 | module: { 6 | rules: [ 7 | { 8 | test: /\.(js|jsx)$/, 9 | exclude: /node_modules/, 10 | use: ['babel-loader'] 11 | } 12 | ] 13 | }, 14 | resolve: { 15 | extensions: ['*', '.js', '.jsx'] 16 | }, 17 | output: { 18 | path: __dirname + '/dist', 19 | publicPath: '/', 20 | filename: 'bundle.js' 21 | }, 22 | plugins: [ 23 | new webpack.HotModuleReplacementPlugin() 24 | ], 25 | devServer: { 26 | contentBase: './dist', 27 | hot: true 28 | } 29 | }; --------------------------------------------------------------------------------