1. Introduction
- Virtual DOM
- Support among browsers
2. Node.js and Node Package Manager
- Node
- NPM
- New NPM project
- Starting project
- Installing a package locally
- installing a package globally
- Installing specific version of package
- Configuration
3. Visual Studio
- Visual Studio Code
- Visual Studio Code Extensions
- Node.js tools for Visual Studio
4. Webpack
- Installation
- Configuration and new project
- Running
- Creating bundle.js for deployment
- Creating packages
5. React
- Resources
- Hello World
- Hello World with Node.js
6. JSX
7. Build a react component
- ES6
- Prebuild components
8. Managing state
9. Routing
- Simple routing
- Router
10. Display a list
11. PropTypes
12. Redux
- Store
- Actions (events)
- Reducers (event handlers)
- Enrich page
- Responding to events
13. MobX
- Installation
- Refreshing data
14. TypeScript
- Resources
- The idea
- Installing
- Compiling and running in Visual Studio
- TypScript in Node.js
- Debugging
15. Using external libraries with TypeScript
- jQuery
- React
16. Typescript - Advanced features
17. Testing
- Test runner - Mocha
- Test setup/tear down
- Custom test suite
- Setup environment
- Simple Test
- Running test upon save
- Assertion library - Expect
- Assertion library - Chai
- Mocking (with expect)
- Assertion library comparison
- Testing React Code (React Utils)
- Testing React Code (React Utils + Enzyme)
- Adding TypeScript support1
- JEST test framework
1. Introduction
React is a JavaScript framework for building responsive (single page) applications.
React web
React web - Tic-tac-toe game
Code pen to play with React
Why React.js?
node.js tutorial on guru99.com
Virtual DOM
Support among browsers
2. Node.js and Node Package Manager
Node.js tutorial at w3resource
Node
Having index.js
JavaScript console.log('Hello from Basics');Then
CMD node index.jswill write "Hello from Basics" to console output
NPM
New NPM project
CMD npm initThis will create package.json project file (similar to .csproj in C#)
JavaScript { "name": "tutorial", "version": "1.0.0", "description": "tutorial", "scripts": { "start": "node src/index.js" }, "author": "Zbynek", }
Starting project
CMD npm startNPM will look in package.json for scripts/start
JavaScript "scripts": { "start": "node server.js", }and start "node server.js" in in the example above
Installing a package locally
CMD npm install <package>will create local folder node_modules\<package> and add will dependency to package.json
JavaScript "dependencies": { "jquery": "2.2.3", "node_module": "0.0.0" }, "devDependencies": { "mocha": "2.4.5" }
CMD npm installwill read the list of dependencies from package.json and check if they are already downloaded
Note: if an error "cannot rename file" coccurs, close all apps that can lock projects folders (especially Free Commander or browser). Worst case, restart computer.
installing a package globally
CMD npm install -g <package>will copy the package to C:\Users\<user>\AppData\Roaming\npm\<package>
Downloaded packages are held in npm cache under C:\Users\<user>\AppData\Roaming\npm-cache
Installing specific version of package
CMD npm install myPackage@1.0.1
Configuration
CMD npm config editConfig file is stored as C:\windows\<user>\.npmrc
List config
CMD npm config ls -l
3. Visual Studio
Visual Studio Code
Visual Studio Code Extensions
Debugger for Chrome
Code coverage Code coverage - github
Read here how to write Create custom extensions
Node.js tools for Visual Studio
4. Webpack
Helps with:
- bundling all .js files into one .js file - runs as dev server (with the bundled .js file in memory)
Installation
CMD npm install -g webpack
Configuration and new project
JSON { "name": "basics1", "description": "Run simple script with webpack", "scripts": { "start": "node devServer.js" }, "dependencies": { "jquery": "2.2.3" }, "devDependencies": { "extract-text-webpack-plugin": "^1.0.1", "transfer-webpack-plugin": "^0.1.4", "webpack": "^1.13.3", "webpack": "1.13.0", "webpack-dev-server": "^1.16.2", "write-file-webpack-plugin": "^3.4.2" } }Create webpack.config.dev.js in root of your project
JSON var webpack = require('webpack'); var TransferWebpackPlugin = require('transfer-webpack-plugin'); var WriteFilePlugin = require('write-file-webpack-plugin'); module.exports = { entry: [ 'webpack-dev-server/client?http://localhost:3000', './index.js' ], output: { filename: "bundle.js", path: __dirname + "/dist", publicPath: '/' }, resolve: { extensions: ["", ".js"] } };Create devserver.js in root of your project
JavaScript var webpack = require('webpack'); var WebpackDevServer = require('webpack-dev-server'); var config = require('./webpack.config.dev'); var port=3000; new WebpackDevServer(webpack(config), { publicPath: config.output.publicPath, // hot: true, // enables hot reloading historyApiFallback: true }).listen(port, 'localhost', function (err, result) { if (err) { console.log(err); } console.log('Listening at localhost:'+port); });Create index.html as starting point
HTML <html> <body> <div id="root"></div> <script src="bundle.js"></script> <!-- bundle.je gets created webpack (all .js in one file) --> </body> </html>Create index.js as starting point
JavaScript document.write("hello from Basics1/index.js");
Running
Bundle.js can be seen by typing "localhost:3000/bundle.js"
Creating bundle.js for deployment
JSON webpack.publish.config.json ... module.exports = { entry: [ './index.tsx' ], output: { filename: "bundle.js", path: __dirname + "/dist", publicPath: '' }, ..the entry elements doesn ot have to have any dev-server info
Create new command in package.json to use new config
JSON package.json "scripts": { "package": "webpack --config webpack.publish.config.json" ... },and run
CMD npm run packageto create bundle.js under dist
Creating packages
Add following to package.json
JSON package.json "scripts": { "build": "gulp pack", ... }, "devDependencies": { "gulp": "^3.9.1", "gulp-clean": "^0.3.2", "gulp-shell": "^0.5.2", ... }and run "npm install" to install gulp
Create new gulpfile.js in root
JSON package.json var gulp = require('gulp'), shell = require('gulp-shell'), clean = require('gulp-clean'); gulp.task('clean', function () { return gulp.src(['./dist'], { read: false }).pipe(clean()); }); gulp.task('compile-tsx', ['clean'], shell.task(['tsc'])) gulp.task('copy-files', ['compile-tsx'], function() { return gulp.src(['src/**/!(*.tsx)', 'package.json']).pipe(gulp.dest('./dist')); }); gulp.task('pack', ['copy-files'], shell.task(['npm pack dist/'])); gulp.task('publish', ['copy-files'], shell.task(['npm publish dist/']));and run
CMD npm run buildto create my-package-1.0.0.tgz in root
To consume the file from other project add the package to package.json
JSON package.json "dependencies": { "my-package": "1.0.0", ... }and either copy the my-package-1.0.0.tgz to the root of the project that should consume it or publish the package to repository and run "npm install" to install the package in under "node_module"
5. React
Resources
Great tutorials at tutorialspoint.com https://www.tutorialspoint.com/reactjs/
Hello World
HTML <html> <body onload="myFunction()" /> <h1>React.js basics</h1> <div id="demo" >This will be replaced by pure JavaScript</div> <div id="react-app" >This will be replaced by React.js</div> <div id="react-app1" >This will be replaced by React.js 1</div> <script src="https://unpkg.com/react@15/dist/react.js"></script> <script src="https://unpkg.com/react-dom@15/dist/react-dom.js"></script> <script> function myFunction() { document.getElementById("demo").innerHTML = "Hello Wrold - Replaced by pure JavaScript"; var rootElement = React.createElement('div', {}, React.createElement('h1', {}, "People"), React.createElement('ul', {}, React.createElement('li', {}, React.createElement('h2', {}, "John First"), React.createElement('a', {href: 'mailto:john@first.com'}, 'john@first.com') ), React.createElement('li', {}, React.createElement('h2', {}, "Paul Second"), React.createElement('a', {href: 'mailto:paul@second.com'}, 'paul@second.com') ) ) ) ReactDOM.render(rootElement, document.getElementById('react-app')) ReactDOM.render(rootElement, document.getElementById('react-app1')) } </script> </body> </html>
Hello World with Node.js
HTML import * as React from 'react'; import * as ReactDOM from 'react-dom'; var rootElement = React.createElement('div', {}, React.createElement('h1', {}, "People"), React.createElement('ul', {}, React.createElement('li', {}, React.createElement('h2', {}, "John First"), React.createElement('a', {href: 'mailto:john@first.com'}, 'john@first.com') ), React.createElement('li', {}, React.createElement('h2', {}, "Paul Second"), React.createElement('a', {href: 'mailto:paul@second.com'}, 'paul@second.com') ) ) ) ReactDOM.render(rootElement, document.getElementById('root'))and add following to package.json
JSON "dependencies": { ... "@types/react": "^0.14.47", "@types/react-dom": "^0.14.18", "@types/react-redux": "^4.4.35", "@types/redux-logger": "^2.6.32", "@types/redux-thunk": "^2.1.32", "react": "15.3.2", "react-dom": "15.3.2" }, "devDependencies": { ... "react-hot-loader": "^1.3.0", "react-logger": "^1.1.0", "react-redux": "^4.4.6", "react-router": "^3.0.0", "react-thunk": "^1.0.0", "redux": "^3.6.0", "redux-logger": "^2.7.4", "redux-thunk": "^2.1.0", }and run "nmp install" or install the lastest by "nmp install <package> --save"
6. JSX
Babel preprocessor
JSX on typescriptlang.org
JSX aloows embedding hmlt tag into JavaScrip.
JavaScript var AboutPage = React.createClass({ render: funtion() { return ( <div> <h1 color="blue">About</h1> <p>Demo page</p> </div> ); } });The JSX code with nested elements above compiles to JavaScript with React
JavaScript React.createElement("div", null, React.createElement("h1", {color:"blue"}, "About" ), React.createElement("p", null, "Demo page"}, )
7. Build a react component
JavaScript var React = require('react'); // includes React package var AboutPage = React.createClass({ render: function() { return ( <div> <h1 color="blue">About</h1> <p>Demo page</p> </div> ); } }); module.Export = AboutPage; // makes the page availableInclude that page in main page
JavaScript var React = require('react'); var AboutPage = require('./components/aboutPage'); // includes AbouPage component React.render(<AboutPage>, document.getElementById('root')); // renders AboutPage in root elementCreate index.html that uses the app built above
HTML <html> <body> <div id="root"></div> <script src="scripts/bundle.js"></script> // gets created by gulp or webpack (all .js in one file) </body> </html>
ES6
JavaScript <div onClick={this.handleClick}/>ES6 requires bind
JavaScript <div onClick={this.handleClick.bind(this)}/>It can be handled in class constructor
JavaScript class People extends React.Component{ constructor(props) { super(props); this.handleClick=this.handleClick.bind(this); }ES5 Stateless component (since React 1.4)
JavaScript var ContactPage = function(props) { return ( <h1> Main Street 1, Small City</h1> ); });ES6 Stateless component
JavaScript const ContactPage = (props) => { return ( <h1> Main Street 1, Small City</h1> ); }); Stateless components do not have Lifecycle methods
Prebuild components
8. Managing state
Assuming there is a simple React component that renders first name from a Person class.
1. using forceUpdate
In constructor
JavaScriptrender()
JavaScript this.Person.firstNameonClick()
JavaScript this.person = new Person('Paul'); this.forceUpdate()
2. using State
In constructor
JavaScript this.state = new Person('John');render()
JavaScript this.state.person.firstNameonClick()
JavaScript var newPerson = new Person('Paul'); this.setState(newPerson);
Note: if you use an array, you cannot assign it directly to state, you have to use
JavaScript this.state = { "people" : people }
Note: Redux is using the same principle, just requires more code.
3. Using @observable in MobX, please refer to the MobX section
9. Routing
Simple routing
You have to display contact page is user types http://mypages.com/#contact
JavaScript var React = require('react'); var AboutPage = require('./components/aboutPage'); var Contact = require('./components/contact'); var AboutPage = React.createClass({ render: function() { var Page; switch (this.props.route) { case 'contact' : Page = Contact; break; default: Page = AboutPage; } return ( <div> <Page/> </div> ); } }); function render() { var myRoute = window.location.hash.substr(1); React.render(<App route = {myRoute} /> document.getElementById('root')); } window.addEventListener('hashchange', render); // listens to URL changes and renders new content render(); // renders page first time
Router
JavaScript var React = require('react'); // includes React package var myRoutes = ( <Route name="app" path="/" handler={require('./main')}> <DefaultRoute handler=={require('./components/aboutPage'}> // if path is not set, name is used <Route name="people" path="allPeople" handler=={require('./components/people'}> </Route> ); } }); module.Export = myRoutes;Update main.js to use myRoutes
JavaScript var React = require('react'); var RouteHandler = require('react-router'); var myRoutes = require('./myRoutes'); var App = React.createClass({ render: function() { return ( <div> <RouteHandler/> </div> ); } }); Router.run(myRoutes, function(Handler) { React.render(<handler>, document.getElementById('root')); });
10. Display a list
Lifecycle: componentWillMount - runs before initial renderning componentDidMount - runs after render (DOM exists) componentWillReceiveProps - run before new properties are received. Does not run on initial render. shouldComponentUpdate - runs before render. If returns falls, render will be ignored. componentWillUpdate - after rendering (when new props are states changed). SetState cannot be called here componentDidUpdate - after componts updates propageated to the DOM componentWillUnmount - before component is unmounted from the DOM
JavaScript var People = React.createClass({ // initial state is an empty array getInitialState: function() { return { people: [] }; }, // loads data in json format and assigns them to people componentWillMount: function() { this.setState({ people: peopleService.getPeople() }); }, render: funtion() { var createPersonText = function(person) { return ( // id is necessary for the framework <li key={author.id}>{person.firstName} {person.lastName}</li> ); } return ( <div> <ul> {this.state.people.map(createPersonText, this)} </ul> </div> ); } }); module.exports = People;
11. PropTypes
React.PropTypes.object.isRequired React.PropTypes.func.isRequired React.PropTypes.bool React.PropTypes.number React.PropTypes.string React.PropTypes.array React.PropTypes.funcDo not run in minified version!
JavaScript var PeopleList = React.createClass({ propTypes: { people: ReactPropTypes.array.isRequired }, render: funtion() { ...If people array has not been passed then browser will show a warning.
12. Redux
Dank Abramov - Twitter
Dan Abramov - You might not need redux
10 Steps tutorial in (full)VS
React-redux environment setup
Single immutable store The only way to change a state is to emit an action States are changed by pure functions (called reducers) Onedirectional data flow Flux - has mutliple stores - stores owns the logic to change the data Action Creators export function setName(name) { return [ type: SET_NAME, name: name } }
Store
let store = createStore(reducer) store.dispatch(action) store.subscribe(listener) replaceReducer(nextReducer) Immutability: changing a state means returning new object Reasons for immutable state: Clarity - change can occur only in one place and it is reducer Performance - redux does not need to iterate over all propeties to figure out if any of them changed. If reference is different, it changed. Time-travel debugging Already in JS: Number, String, Boolean, Undefined, Null Mutable: Objects.Arrays, Functions Changing a state by creating new object: in ES6 Object.assign({}, state, {name: 'John'}); // {} empty object ES5 - Lodash Ways to enforce immutability redux-immutable-state-invariant Immutable.jsCreate store Configure store configureStore.js
JavaScript import {createStore} from 'redux'; import rootReducer from './myReducers'; export default function configureStore(initialState) { return createStore( rootReducer, initialState }Instantiate store for the app in src/index.js
JavaScript import configureStore from './store/configureStore' import {Provider} from 'react-redux' const store = configureStore(); render( <Provider store={store}> <Router history={browseHistory routes={routes} /> </Provider>, document.getElementById('root') );
Actions (events)
JavaScript export function createPerson(person) { return { type: 'CREATE_PERSON', person}; }if the action should be reused, definig the action as a const helps avoiding type error
JavaScript // consts defined in an independent file export const CREATE_PERSON = 'CREATE_PERSON';
JavaScript import * as types from './actiontypes'; export function createPerson(person) { return { type: types.CREATE_PERSON, person}; }
Reducers (event handlers)
JavaScript function myReducer(state, action) { switch (action.type) { case 'SET_NAME': return (Object.assign({}, state, { name: 'John' }); default: return state; } } reducers/index.js function arrayReducer(state =[], action) { switch (action.type) { case types.CREATE_PERSON: return [...state, Object.assign({}, action.person)]; default: return state; } }Using Root reducer simplifies multi case statements
JavaScript import {combineReducers} from 'redux'; import myReducer from './myReducerFile'; const rootReducer = combineReducers({ myReducer : myReducer }); export default rootReducer;
All reducers are called when an action is dispatched containers - logic - are aware of Redux - subscribe to redux state - dispatch Redux actions - generated from react-redux Presentational - view - read data from Props - invoke callbacks on props - written by hand
Enrich page
JavaScript import React,{PropTypes} from 'redux'; class People extends React.Component{ constructor(props) { super(props, contect); this.state = { person:{ name:""} }; this.OnNameChanged = this.OnNameChanged.bind(this); this.OnClickCreate = this.OnClickCreate.bind(this); } onNameChanged(event) { const person = this.state.person; person.name = event.target.value; setState({person:person}); } onClickCreate() { alert('${this.state.person.name} would be created here'); } render() { return ( <div> <input type="text" onChange={this.onNameChanged} value{this.state.course.title} /> <input type="submit" onClick={this.onClickCreate} value="Create" /> </div> ); } } export default PeoplePage;If you start using Redux it will change like this:
JavaScript import React,{PropTypes} from 'redux'; import {connect} from 'react-redux'; import * as personAction from './personActions'; class PeoplePage extends React.Component{ constructor(props) { ... } onClickCreate() { // props.dispatch is injected by connect(), if connect uses only 1 argument this.props.dispatch(personAction.createPerson(this.state.person)); } personDetail(person, index) { return <div key={index}>{person.name}</div> } render() { return ( <div> {this.props.people.map(this.personDetail)} //map() iterates over list - this line displays list ... </div> ); } } function mapStateToProps(state, ownProps) { return { people: state.people }; } export default connect(mapStateToProps)(PeoplePage);
Flow: REACT - user clicks "Create" button - personPage.js onClickCreate() calls this.props.dispatch(personAction.createPerson(this.state.person)) ACTION - personAction.js createPerson(aPerson) returns { type='CREATE_PERSON', person=aPerson } - dispatch() calls all reducers REDUCERS - personReducer.js personReducer() returns new array with the person passed addedd into the array REACT - personPage.js mapStateToProps(state. ownProps) returns { people: state.people} - personPage.js render() draws array in this.props.peopleSimplify the code by using mapDispatchToProps()
JavaScript import React,{PropTypes} from 'redux'; import {connect} from 'react-redux'; import * as personAction from './personActions'; class PeoplePage extends React.Component{ constructor(props) { ... } PeoplePage.PropTypes = { people: PropTypes.array.isRequired, createPerson: PropTypes.func.isRequired } onClickCreate() { this.props.createPerson(this.state.person); // before was: this.props.dispatch(personAction.createPerson(this.state.person)); } personDetail(person, index) { return <div key={index}>{person.name}</div> } render() { return ( <div> {this.props.people.map(this.personDetail)} //map() iterates over list - this line displays list ... </div> ); } } function mapStateToProps(state, ownProps) { return { people: state.people }; } function mapDispatchToProps(dispatch) { // "dispatch" gets injected by the "connect()" function return { createPerson: person => dispatch(personActions.createPerson(person)) }; } export default connect(mapStateToProps, mapDispatchToProps)(PeoplePage);Simplify the code by using bindActionCreators
JavaScript import React,{PropTypes} from 'redux'; import {connect} from 'react-redux'; import * as personAction from './personActions'; import {bindActionCreators} from 'redux'; class PeoplePage extends React.Component{ constructor(props) { } PeoplePage.PropTypes = { people: PropTypes.array.isRequired, actions: PropTypes.object.isRequired, } onClickCreate() { this.props.actions.createPerson(this.state.person); } render() { return ( ... ); } } function mapStateToProps(state, ownProps) { return { people: state.people }; } function mapDispatchToProps(dispatch) { // "dispatch" gets injected by the "connect()" function return { actions: bindActionCreators(courseActions, dispatch); mapping all actions in personActions.js // before was: createPerson: person => dispatch(personActions.createPerson(person)); }; } export default connect(mapStateToProps, mapDispatchToProps)(PeoplePage);
Responding to events
13. MobX
Installation
CMD npm install mobx --save npm install mobx-react --saveif using TypeScript, set experimentalDecorators to true
JSON tsconfig.json { "compilerOptions": { "experimentalDecorators": true ...
Refreshing data
JavaScript @observer class PersonPanel { @observable person = new Person('John') ... }render()
JavaScript this.Person.firstName
JavaScript onClick() { this.person = new Person('Paul') }
14. TypeScript
Resources
Quick Start
Typescript in 30 minutes
The idea
TypeScript // http://www.typescriptlang.org/docs/handbook/classes.html export class Person { lastName: string; firstName: string; age: number; member : boolean; constructor(aFirstName : string) { this.firstName = aFirstName; } };will be compiled to JavaScript
JavaScript var Person = (function () { function Person(aFirstName) { this.firstName = aFirstName; } return Person; }()); exports.Person = Person;that will be deployed to browser.
Importing code from other file
TypeScript // import a function or a class import {add} from './src/mylib'; import {Person} from './src/mylib'; // import all exported objects import * as myLib from './src/mylib';
Installing
or
it can installed (globally) by NPM as:
CMD npm install -g typescript
Compiling and running in Visual Studio
In VS 2015 File > Create new project > Other languages > TypeScript. Hit F5.
Compiling from command line
CMD tsc myApp.tstsc.exe is usually located under C:\Program Files (x86)\Microsoft SDKs\TypeScript\1.5\ or C:\Users\<userName>\AppData\Roaming\npm\
TypeScript vs. ES6
typescript vs ecmascript-6/
es5 es2015 typescript/
TypScript in Node.js
Add following to packages.json
JSON { "devDependencies": { ... "typescript": "2.1.1", "ts-loader": "^1.2.0" } }and run
CMD npm installAdd following to webpack.config.dev.js
JSON module: { loaders: [ { test: /\.ts?$/, loaders: ['ts']} // 'ts' means use 'ts-loader' ], }Run
CMD tsc --initto create tsconfig.json. The file contains tsc options then can listed by
CMD tsc -hbe aware that there are different sets of options for Micrososft tsc and Node.js tsc.
Update the file appropriatelly
JSON { "compilerOptions": { "outDir": "./dist/", "noImplicitAny": true, "module": "commonjs", "target": "es5" }, "include": [ "./src/**/*", "./index.ts" ], "exclude": [ "node_modules" ] }Create index.ts with some TypeScript code
TypeScript document.getElementById("root").innerHTML = "Replaced in TypeScript - pure JavaScript still works"; class Person { firstName: string; age: number; constructor(firstName : string) { this.firstName = firstName; } } var p = new PersonF("John"); document.write("FirtsName=" + p.firstName);run
CMD npm startto see the code on the website
Debugging
JSON package.json "source-map-loader": "^0.1.5"
JSON tsconfig.json "compilerOptions": { "sourceMap": true,
JSON webpack.config.dev.js // Enable sourcemaps for debugging webpack's output. devtool: "source-map", module: { preLoaders: [ // All output '.js' files will have any sourcemaps re-processed by 'source-map-loader'. { test: /\.js$/, loader: "source-map-loader" } ] },
15. Using external libraries with TypeScript
jQuery
Codeplex
Github
Add to the top of your .ts file
TypeScript /// <reference path="jquery.d.ts" />use jQuery in code
TypeScript var el1 = document.getElementById('content1'); el1.innerHTML = $("<div>ABC</div>").html();JavaScript 2 does not require to reference .d.ts in the code file. It uses a reference in package.json
To search if a types are available for a package go to npmjs.com or http://microsoft.github.io/TypeSearch/
If yes, then run
JSON npm install --save @types/<package>or update package.json"
TypeScript "devDependencies": { "@types/<package>": "<version>" }and run
JSON npm installto download the types. The types will be downloaded under node_module\@types\<package>
React
Rect .ts
Tutorial how to install and compile react component with NPM and Webpack
Rect and Webpack
Download by npm
CMD npm install --save react react-dom @types/react @types/react-domTutorial TODO app in react
16. Typescript - Advanced features
17. Testing
Test runner - Mocha
JSON package.json { "devDependencies": { ... "@types/mocha": "^2.2.34", "mocha": "2.4.5", ... } }In order to run Mocha, update package.json
JSON package.json { "scripts": { "pretest": "tsc", "test": "mocha \"dist/**/*.test.js\"", ... },The "pretest" step compiles all typescript files (based on tsconfig.js) into dist folder and the "test" steps will execute the test based on *.test.js file pattern
Let's write a test. A dummy test to start to show the basics. 1 pass, 1 fail
TypeScript export class TestException { message: string; constructor(aMessage : string) { this.message = aMessage; } }; describe('Dummy test demo', () => { it('should PASS', () => { // does nothing }); it('should FAIL', () => { throw new TestException("My failed test message"); }); });Run the tests from command line by executing
CMD npm testNow something more real. Assuming there is a method add that needs to be tested
TypeScript import * as myLib from './myLib'; describe('MyLib', () => { it('WHEN add() called THEN correct value returned', () => { // act var result = myLib.add(1, 2); // assert var expected = 3; if (result != expected) throw new TestException(result + " != " + expected); }); });
Test setup/tear down
TypeScript export class TestException { message: string; constructor(aMessage : string) { this.message = aMessage; } }; // Setup / Tear down before(function(){ console.log(' Runs one time before the first test in the file') }) after(function(){ console.log(' Runs one time after all tests in the file were executed') }) beforeEach(function(){ console.log(' Runs before each test in the file') }) afterEach(function(){ console.log(' Runs after each test in the file') }) describe('Dummy test demo with setup/tear down', () => { it('should PASS', () => { // does nothing }); it('should FAIL', () => { throw new TestException("My failed test message"); }); });
Custom test suite
TypeScript function makeTestSuite(name:string, tests:any) { describe(name, function () { before(function () { console.log(" executed before each test in the tst suite"); }); tests(); after(function () { console.log(" executed before each test in the test suite"); }); }); } makeTestSuite('Suite 1 - with setup', function(){ it('Test 1', function(done){ done(); }); it('Test 2', function(done){ done(); }); }); describe('Test Suite2 - without setup', function(){ it('Test 3', function(done){ done(); }); });
Setup environment
and all tests under src ending *.test.js
Create testSetup.js as configuration for Mocha you can use "--reporter progress" to get just limited output (how many tests passed/failed).
JSON process.env.NODE_ENV = 'test'; // disables hot reloading require('babel-register')(); // will transpile tests so that they could be written in TypeScript require.extensions['.css'] = function () {return null;}; // disable features the Mocha does not understand var jsdom = require('jsdom').jsdom; // simulate browser var exposedProperties = ['window', 'navigator', 'document']; // helps to simulate browser environment global.window = ...if you use TypeScript, you need to compile the ts(x) files to js and copy .css or .less files that are imported in compiled .js files
JSON package.json "scripts": { "pretest": "tsc & xcopy .\\src\\*.less .\\dist\\src\\ /Y /S /D /R", "test": "mocha --reporter progress testSetup.js \"dist/test/**/*.test.js\"", ... }
Simple Test
Include "expect" library that provides assert (expect) functionality.
JavaScript // import expect from 'expect'; ES6 - requires babel var expect = require('expect'); describe('test group', () => { it('dummy test - pass', () => { expect(true).toEqual(true); }); });Start tests with
CMD npm test
Running test upon save
JSON "scripts:"{ "start": "npm-run-all --parallel test:watch open:src", "open:src": "node devServer.js", "test:watch": "npm run test -- --watch" ... }When multiple nodes are found(e.g. 2 buttons) Error: This method is only meant to be run on single node. 2 found instead. Testing components: export class Import in {} Video Testing Connected React Compinents 5:00-6:00
Assertion library - Expect
To install Expect, modify package.json
JSON package.json "devDependencies": { "@types/expect": "^1.13.31", "expect": "1.19.0", ... }
TypeScript import * as myLib from './myLib'; import * as expect from 'expect'; describe('MyLib:', () => { it('WHEN add() called THEN correct value returned', () => { // act var result = myLib.add(1, 2); // assert expect(result).toEqual(3); }); });
Assertion library - Chai
To install Expect, modify package.json
JSON package.json "devDependencies": { "@types/chai": "3.4.30", "chai": "^3.5.0" ... }
TypeScript import * as myLib from './myLib'; import * as chai from 'chai'; chai.should(); var expect = chai.expect; var assert = chai.assert; describe('MyLib:', () => { it('WHEN add() called THEN correct value returned', () => { // act var result = myLib.add(1, 2); // assert // Option 1 expect(result).to.equals(3); // expect(result).toEqual(3); 'expect' library style // Option 2 result.should.equal(3); // Option 3 assert.equal(result, 3); }); });
Mocking (with expect)
TypeScript import * as expect from 'expect'; // method to be mocked var myService = { getValue: function () {} } describe('MyService:', () => { // bypass call and return value it('WHEN getValue() called THEN 3 returned', () => { // arrange var spy = expect.spyOn(myService, 'getValue') var dice = spy.andReturn(3) // act var result = myService.getValue(); // assert spy.restore() expect(result).toEqual(3); }); // fail if not called it('WHEN test runs THEN FAIL if getValue() was not called', () => { // arrange var spy = expect.spyOn(myService, 'getValue') // assert expect(spy).toHaveBeenCalled('not called') spy.restore() }); });
Assertion library comparison
Feature Chai Expect should() syntax x expect() syntax x x assert syntax x integrated mocking x extensions ? xRecommendation to use Expect because:
- It integrates mocking and stubbing functionality, which limits configuration problem issues if these were in 2 separated libraries
- It limits syntax options how to write asserts, which will lean to cleaner
- Recommended by Cory House (author of Pluralsite React-Redux course - assessed with 5 stars)
Testing React Code (React Utils)
JSON package.json { "devDependencies": { "react-addons-test-utils": "15.0.2", ... }and write a test like this
TypeScript import * as React from 'react'; import * as TestUtils from 'react-addons-test-utils'; import * as expect from 'expect'; import {HelloMessage} from './HelloMessage'; describe('Testing with React utils', () => { it('WHEN page gets rendered THEN elements are correct', () => { // arrange let renderer = TestUtils.createRenderer(); renderer.render('<HelloMessage name="John" />'); let output = renderer.getRenderOutput(); // assert const h1Element = output.props.children[0]; expect(h1Element.props.value).toBe('Hello John'); }); });
Testing React Code (React Utils + Enzyme)
JSON package.json { "devDependencies": { "@types/enzyme": "2.4.30", "enzyme": "2.2.0", "jsdom": "8.5.0", ... }which allows to write markup test like this
TypeScript ... import * as Enzyme from 'enzyme'; describe('Testing with Enzyme', () => { it('WHEN page gets rendered THEN elements are correct', () => { // act var wrapper = Enzyme.shallow(<HelloMessage name="John" />); // assert expect(wrapper.find('h1').text()).toEqual('Counter:'); // if you have e.g. 2 buttons, you can finsd the first one like this // var incrementButton = wrapper.find('button').first(); // expect(incrementButton.text()).toEqual('INCREMENT'); // multiple control types - use the first // expect(incrementButton.prop('id')).toEqual('3'); // type of the control }); });
Adding TypeScript support1
/// <reference path="../mocha.d.ts" /> Expect https://www.npmjs.com/package/expect https://github.com/mjackson/expect npm http://stackoverflow.com/questions/26977722/how-to-run-mocha-tests-written-in-typescript $ npm install -g ts-node $ mocha test.ts --require ts-node/register src/**/*.spec.ts In test code: http://stackoverflow.com/questions/35812221/typescript-default-import-failing import * as _expect from 'expect'; const expect = _expect as any as typeof _expect.default; babel-core babel-preset-es2015 babel-register add .babelrc { "preset": ["es2015"] } "test": "mocha --compilers js:babel-register"
JEST test framework
Jest is a test framework used by Facebook. It integrates test runner, assertion library, mocking and code coverage.
The runner syntax is the same as for Mocha, syntax for expect() is similar, but not the same.
Code coverage can be executed as "npm test -- --coverage", watch as "npm test -- --watch"
To configure Jest for project, use the package.json below.
JSON package.json "scripts": { "test": "jest", ... }, "jest": { "transform": { ".(ts|tsx)": "<rootDir>/node_modules/ts-jest/preprocessor.js" }, "testRegex": "(/tests/.*|\\.(test|spec))\\.(ts|tsx|js)$", "moduleFileExtensions": [ "ts", "tsx", "js" ], "testResultsProcessor": "<rootDir>/node_modules/ts-jest/coverageprocessor.js", "moduleNameMapper": { "^.+\\.(css|less)$": "<rootDir>/src/tools/styleMock.js", "^.+\\.(gif|ttf|eot|svg)$": "<rootDir>/src/tools/fileMock.js" } }, "devDependencies": { "@types/enzyme": "^2.7.1", "@types/jest": "^16.0.3", "enzyme": "^2.7.0", "jest": "^18.1.0", "jest-cli": "^18.1.0", "react-addons-test-utils": "15.4.2", "ts-jest": "^18.0.1", ... }To write a React test, you can use the sample below
TypeScript import * as React from 'react' import { mount, shallow } from 'enzyme' import { MyComponent } from './MyComponent' describe('<MyComponent />', () => { it('should render component', () => { var store = { subscribe: jest.fn(), dispatch: jest.fn(), getState: jest.fn(), errors: [] } var props = { headerText : 'My header', store: store } var wrapper = mount(<MyComponent headerText='My header' store={ store } errors={ [] } />) expect(wrapper.find('.header').text()).toBe(props.headerText) }); });If your code imports .less file, you need update configuration
JSON package.json "jest": { ... "moduleNameMapper": { "^.+\\.(css|less)$": "<rootDir>/src/fakeStyle.js", "^.+\\.(gif|ttf|eot|svg)$": "<rootDir>/src/fakeFile.js" }, ... }
JavaScript // test/fileMock.js module.exports = '';
JavaScript // test/styleMock.js module.exports = {};