├── .babelrc ├── .gitignore ├── dist ├── bundle.js └── index.html ├── karma.config.js ├── package.json ├── readme.md ├── src ├── components │ └── CommentList.js ├── containers │ └── Root.js └── main.js ├── test ├── components │ └── CommentList.spec.js ├── containers │ └── Root.spec.js ├── helloWorld.spec.js └── test_helper.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | 4 | -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /karma.config.js: -------------------------------------------------------------------------------- 1 | var argv = require('yargs').argv; 2 | var path = require('path'); 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | browsers: ['PhantomJS'], 7 | singleRun: !argv.watch, // just run once by default 8 | frameworks: ['mocha', 'chai'], 9 | // npm i karma-spec-reporter --save-dev 10 | // displays tests in a nice readable format 11 | reporters: ['spec'], 12 | 13 | // include some polyfills for babel and phantomjs 14 | files: [ 15 | 'node_modules/babel-polyfill/dist/polyfill.js', 16 | './node_modules/phantomjs-polyfill/bind-polyfill.js', 17 | './test/**/*.js' // specify files to watch for tests 18 | ], 19 | preprocessors: { 20 | // these files we want to be precompiled with webpack 21 | // also run tests throug sourcemap for easier debugging 22 | ['./test/**/*.js']: ['webpack', 'sourcemap'] 23 | }, 24 | webpack: { 25 | devtool: 'inline-source-map', 26 | resolve: { 27 | // allow us to import components in tests like: 28 | // import Example from 'components/Example'; 29 | root: path.resolve(__dirname, './src'), 30 | 31 | // allow us to avoid including extension name 32 | extensions: ['', '.js', '.jsx'], 33 | 34 | // required for enzyme to work properly 35 | alias: { 36 | 'sinon': 'sinon/pkg/sinon' 37 | } 38 | }, 39 | module: { 40 | // don't run babel-loader through the sinon module 41 | noParse: [ 42 | /node_modules\/sinon\// 43 | ], 44 | // run babel loader for our tests 45 | loaders: [ 46 | { test: /\.js?$/, exclude: /node_modules/, loader: 'babel' }, 47 | ], 48 | }, 49 | // required for enzyme to work properly 50 | externals: { 51 | 'jsdom': 'window', 52 | 'cheerio': 'window', 53 | 'react/lib/ExecutionEnvironment': true, 54 | 'react/lib/ReactContext': 'window' 55 | }, 56 | }, 57 | webpackMiddleware: { 58 | noInfo: true 59 | }, 60 | // tell karma all the plugins we're going to be using 61 | plugins: [ 62 | 'karma-mocha', 63 | 'karma-chai', 64 | 'karma-webpack', 65 | 'karma-phantomjs-launcher', 66 | 'karma-spec-reporter', 67 | 'karma-sourcemap-loader' 68 | ] 69 | }); 70 | }; 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-testing-starter-kit", 3 | "version": "0.1.0", 4 | "description": "React starter kit with nice testing environment set up.", 5 | "main": "src/main.js", 6 | "directories": { 7 | "test": "tests", 8 | "src": "src", 9 | "dist": "dist" 10 | }, 11 | "dependencies": { 12 | "react": "^0.14.6", 13 | "react-dom": "^0.14.6", 14 | "yargs": "^3.31.0" 15 | }, 16 | "devDependencies": { 17 | "babel-core": "^6.4.0", 18 | "babel-loader": "^6.2.1", 19 | "babel-polyfill": "^6.3.14", 20 | "babel-preset-es2015": "^6.3.13", 21 | "babel-preset-react": "^6.3.13", 22 | "babel-register": "^6.3.13", 23 | "chai": "^3.4.1", 24 | "enzyme": "^1.2.0", 25 | "json-loader": "^0.5.4", 26 | "karma": "^0.13.19", 27 | "karma-chai": "^0.1.0", 28 | "karma-mocha": "^0.2.1", 29 | "karma-phantomjs-launcher": "^0.2.3", 30 | "karma-sourcemap-loader": "^0.3.6", 31 | "karma-spec-reporter": "0.0.23", 32 | "karma-webpack": "^1.7.0", 33 | "mocha": "^2.3.4", 34 | "phantomjs": "^1.9.19", 35 | "phantomjs-polyfill": "0.0.1", 36 | "react-addons-test-utils": "^0.14.6", 37 | "sinon": "^1.17.2", 38 | "webpack": "^1.12.11", 39 | "webpack-dev-server": "^1.14.1" 40 | }, 41 | "scripts": { 42 | "test": "node_modules/.bin/karma start karma.config.js", 43 | "test:dev": "npm run test -- --watch", 44 | "build": "webpack", 45 | "dev": "webpack-dev-server --port 3000 --devtool eval --progress --colors --hot --content-base dist", 46 | "old_test": "mocha --compilers js:babel-register --require ./test/test_helper.js --recursive", 47 | "old_test:watch": "npm test -- --watch" 48 | }, 49 | "repository": { 50 | "type": "git", 51 | "url": "tbd" 52 | }, 53 | "keywords": [ 54 | "react", 55 | "mocha", 56 | "chai", 57 | "enzyme", 58 | "testing" 59 | ], 60 | "author": "Spencer Dixon", 61 | "license": "ISC" 62 | } 63 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ## OUTDATED!!! --> Use Jest, it's great :-) 2 | 3 | This starter kit goes with a blog post I wrote on how to set up TDD React 4 | environment. [The blog post can be found here](http://spencerdixon.com/blog/setting-up-testing-in-react.html). 5 | 6 | **Goal**: Goal of this starter kit is just to act as an example of a pretty 7 | robust testing environment for React. 8 | 9 | ### Getting Started 10 | ``` 11 | git clone git@github.com:SpencerCDixon/react-testing-starter-kit.git myProject 12 | cd myProject 13 | 14 | nvm use 5.1.0 # make sure to use v4 or greater 15 | npm install 16 | 17 | # open split terminal window 18 | npm run dev # get webpack server running 19 | npm run dev:test # to get karma test server going 20 | ``` 21 | 22 | **Note** The webpack config in this project is not out of the box ready for 23 | production. You will want to set up Plugins like UglifyJS and a 24 | compile script that compiles webpack in production mode. 25 | 26 | 27 | ### Tooling 28 | This starter kit uses: 29 | * Mocha 30 | * Chai 31 | * Sinon 32 | * Enzyme 33 | * Karma 34 | * Webpack 35 | -------------------------------------------------------------------------------- /src/components/CommentList.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | const propTypes = { 4 | onMount: PropTypes.func.isRequired, 5 | isActive: PropTypes.bool 6 | }; 7 | 8 | class CommentList extends Component { 9 | componentDidMount() { 10 | this.props.onMount(); 11 | } 12 | 13 | render() { 14 | const { isActive } = this.props; 15 | const className = isActive ? 'active-list' : 'inactive-list'; 16 | 17 | return ( 18 | 21 | ) 22 | } 23 | } 24 | 25 | CommentList.propTypes = propTypes; 26 | export default CommentList; 27 | 28 | -------------------------------------------------------------------------------- /src/containers/Root.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | const styles = { 4 | height: '100%', 5 | background: '#333' 6 | } 7 | 8 | class Root extends Component { 9 | render() { 10 | return ( 11 |
12 |

Welcome to testing React!

13 |
14 | ) 15 | } 16 | } 17 | 18 | export default Root; 19 | 20 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { render } from 'react-dom'; 3 | import Root from 'containers/Root'; 4 | 5 | render(, document.getElementById('root')); 6 | -------------------------------------------------------------------------------- /test/components/CommentList.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | // Once we set up Karma to run our tests through webpack 4 | // we will no longer need to have these long relative paths 5 | import CommentList from '../../src/components/CommentList'; 6 | import { 7 | describeWithDOM, 8 | mount, 9 | shallow, 10 | spyLifecycle 11 | } from 'enzyme'; 12 | 13 | describe('(Component) CommentList', () => { 14 | 15 | // using special describeWithDOM helper that enzyme 16 | // provides so if other devs on my team don't have JSDom set up 17 | // properly or are using old version of node it won't bork their test suite 18 | // 19 | // All of our tests that depend on mounting should go inside one of these 20 | // special describe blocks 21 | describeWithDOM('Lifecycle methods', () => { 22 | it('calls componentDidMount', () => { 23 | spyLifecycle(CommentList); 24 | 25 | const props = { 26 | onMount: () => {}, 27 | isActive: false 28 | } 29 | 30 | // using destructuring to pass props down 31 | // easily and then mounting the component 32 | mount(); 33 | 34 | // CommentList's componentDidMount should have been 35 | // called once. spyLifecyle attaches sinon spys so we can 36 | // make this assertion 37 | expect( 38 | CommentList.prototype.componentDidMount.calledOnce 39 | ).to.be.true; 40 | }); 41 | 42 | it('calls onMount prop once it mounts', () => { 43 | // create a spy for the onMount function 44 | const props = { onMount: sinon.spy() }; 45 | 46 | // mount our component 47 | mount(); 48 | 49 | // expect that onMount was called 50 | expect(props.onMount.calledOnce).to.be.true; 51 | }); 52 | }); 53 | 54 | it('should render as a
    ', () => { 55 | const props = { onMount: () => {} }; 56 | const wrapper = shallow(); 57 | expect(wrapper.type()).to.eql('ul'); 58 | }); 59 | 60 | describe('when active...', () => { 61 | const wrapper = shallow( 62 | // just passing isActive is an alias for true 63 | {}} isActive /> 64 | ) 65 | it('should render with className active-list', () => { 66 | expect(wrapper.prop('className')).to.eql('active-list'); 67 | }); 68 | }); 69 | 70 | describe('when inactive...', () => { 71 | const wrapper = shallow( 72 | // just passing isActive is an alias for true 73 | {}} isActive={false} /> 74 | ) 75 | it('should render with className inactive-list', () => { 76 | expect(wrapper.prop('className')).to.eql('inactive-list'); 77 | }); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /test/containers/Root.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import Root from '../../src/containers/Root'; 4 | 5 | // Original tests 6 | // describe('(Container) Root', () => { 7 | // it('renders as a
    ', () => { 8 | // const wrapper = shallow(); 9 | // expect(wrapper.type()).to.eql('div'); 10 | // }); 11 | 12 | // it('has style with height 100%', () => { 13 | // const wrapper = shallow(); 14 | // const expectedStyles = { 15 | // height: '100%', 16 | // background: '#333' 17 | // } 18 | // expect(wrapper.prop('style')).to.eql(expectedStyles); 19 | // }); 20 | 21 | // it('contains a header explaining the app', () => { 22 | // const wrapper = shallow(); 23 | // expect(wrapper.find('.welcome-header')).to.have.length(1); 24 | // }); 25 | // }); 26 | 27 | // Refactored tests 28 | describe('(Container) Root', () => { 29 | const wrapper = shallow(); 30 | 31 | it('renders as a
    ', () => { 32 | expect(wrapper.type()).to.eql('div'); 33 | }); 34 | 35 | it('has style with height 100%', () => { 36 | const expectedStyles = { 37 | height: '100%', 38 | background: '#333' 39 | } 40 | expect(wrapper.prop('style')).to.eql(expectedStyles); 41 | }); 42 | 43 | it('contains a header explaining the app', () => { 44 | expect(wrapper.find('.welcome-header')).to.have.length(1); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /test/helloWorld.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | describe('hello world', () => { 4 | it('works!', () => { 5 | expect(true).to.be.true; 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /test/test_helper.js: -------------------------------------------------------------------------------- 1 | // no longer being used once Karma is set up 2 | import { expect } from 'chai'; 3 | import sinon from 'sinon'; 4 | 5 | global.expect = expect; 6 | global.sinon = sinon; 7 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var path = require('path'); 3 | 4 | var config = { 5 | entry: [ 6 | 'webpack/hot/dev-server', 7 | 'webpack-dev-server/client?http://localhost:3000', 8 | './src/main.js' 9 | ], 10 | resolve: { 11 | root: [ 12 | // allows us to import modules as if /src was the root. 13 | // so I can do: import Comment from 'components/Comment' 14 | // instead of: import Comment from '../components/Comment' or whatever relative path would be 15 | path.resolve(__dirname, './src') 16 | ], 17 | // allows you to require without the .js at end of filenames 18 | // import Component from 'component' vs. import Component from 'component.js' 19 | extensions: ['', '.js', '.json', '.jsx'] 20 | }, 21 | output: { 22 | path: path.resolve(__dirname, 'dist'), 23 | filename: 'bundle.js' 24 | }, 25 | module: { 26 | loaders: [ 27 | { 28 | test: /\.js?$/, 29 | // dont run node_modules or bower_components through babel loader 30 | exclude: /(node_modules|bower_components)/, 31 | // babel is alias for babel-loader 32 | // npm i babel-core babel-loader --save-dev 33 | loader: 'babel' 34 | } 35 | ], 36 | } 37 | } 38 | 39 | module.exports = config; 40 | --------------------------------------------------------------------------------