Table of contents 1. Environment 2. Starter code 3. Add Karma 3.1. Update dependencies 3.2. Update export 3.3. Update test 3.4. Add Karma configuration 4. Add Webpack 4.1. Update dependencies 4.2. Add Webpack configuration 4.3. Update Karma configuration 4.4. Update imports 5. Add Babel 5.1. Update dependencies 5.2. Add Babel configuration 5.3. Update webpack configuration 6. Add TypeScript 6.1. Update dependencies 6.2. Add TypeScript configuration 6.3. Update webpack configuration 6.4. Update Babel configuration 6.5. Migrate test code 6.6. Update Karma configuration 7. Migrate source code 7.1. Update export 7.2. Update import 7.3. Adjust start script 8. Add code coverage 8.1. Update dependencies 8.2. Update Karma configuration 8.3. Update webpack configuration 8.4. Update Karma configuration 9. Bonus: TypeScript everything! Configuring code coverage with TypeScript and Karma to get coverage reports for code running in web browsers.
Environment Tested with:
Node.js v10.9.0 yarn v1.15.2 Starter code package.json 1 2 3 4 5 6 7 8 9 10 11 12 { "devDependencies" : { "jasmine" : "3.4.0" } , "main" : "src/main.js" , "name" : "karma-webpack-babel-typescript-istanbul" , "scripts" : { "start" : "node index.js" , "test" : "jasmine --config=jasmine.json" } , "version" : "0.0.0" }
jasmine.json 1 2 3 4 5 6 7 8 { "random" : true , "spec_dir" : "src" , "spec_files" : [ "**/*test.js" ] , "stopSpecOnExpectationFailure" : true }
index.js 1 2 3 4 5 const afterTwoSeconds = require ('./src/main' );afterTwoSeconds (() => { console .log ('I will be called after 2 seconds.' ); });
src/main.js 1 2 3 4 5 6 7 8 module .exports = function afterTwoSeconds (callback ) { return new Promise ((resolve ) => { setTimeout (() => { callback (); resolve (); }, 2000 ); }); };
src/main.test.js 1 2 3 4 5 6 7 8 9 const afterTwoSeconds = require ('./main' );describe ('afterTwoSeconds' , () => { it ('resolves after 2 seconds' , async () => { const myCallbackSpy = jasmine.createSpy ('myCallbackSpy' ); await afterTwoSeconds (myCallbackSpy); expect (myCallbackSpy).toHaveBeenCalled (); }); });
Add Karma Update dependencies Karma needs an adapter to know about the Jasmine testing framework. It also needs a browser launcher to run the tests within a browser environment.
package.json 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 { "devDependencies" : { "jasmine" : "3.4.0" , "karma" : "4.1.0" , "karma-chrome-launcher" : "2.2.0" , "karma-jasmine" : "2.0.1" } , "main" : "src/main.js" , "name" : "karma-webpack-babel-typescript-istanbul" , "scripts" : { "start" : "node index.js" , "test" : "karma start" } , "version" : "0.0.0" }
Update export Karma will run the tests inside the operating system’s Chrome browser which was in my case Chrome v74. The browser environment does not know about module.exports
, so we will use the window
namespace to export our afterTwoSeconds
function:
src/main.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 function afterTwoSeconds (callback ) { return new Promise ((resolve ) => { setTimeout (() => { callback (); resolve (); }, 2000 ); }); }if (typeof module !== 'undefined' && typeof module .exports !== 'undefined' ) { module .exports = afterTwoSeconds; } else { window .afterTwoSeconds = afterTwoSeconds; }
Note: We still export our code for Node.js environments, to make our code work in both worlds. That’s why we keep setTimeout because it is available in Node.js and browser environments. If we would write window.setTimeout
it would only work in browsers but fail in Node.js.
Update test Our tests will run in the browser so we cannot import code with a require
statement (CommonJS syntax) anymore and need to use the window
namespace:
src/main.test.js 1 2 3 4 5 6 7 describe ('afterTwoSeconds' , () => { it ('resolves after 2 seconds' , async () => { const myCallbackSpy = jasmine.createSpy ('myCallbackSpy' ); await window .afterTwoSeconds (myCallbackSpy); expect (myCallbackSpy).toHaveBeenCalled (); }); });
Add Karma configuration A Karma configuration can be interactively created by running npx karma init
. In our case we reuse the file paths from our Jasmine configuration.
For a successful Karma test run it is important to declare the source code and test code within the files
property.
Karma can be equipped with a custom test reporter but for now we are good with the standard progress
reporter:
karma.conf.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 const jasmineConfig = require ('./jasmine.json' );module .exports = function (config ) { config.set ({ autoWatch : false , basePath : jasmineConfig.spec_dir , browsers : ['Chrome' ], colors : true , concurrency : Infinity , exclude : [], files : [ 'main.js' , ...jasmineConfig.spec_files ], frameworks : ['jasmine' ], logLevel : config.LOG_INFO , port : 9876 , preprocessors : {}, reporters : ['progress' ], singleRun : true , }); };
Add Webpack Update dependencies We need to add webpack-karma
so that Karma can use webpack
to preprocess files. This also requires us to include webpack
in our list of dependencies as it is a peer dependency of webpack-karma
:
package.json 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 { "devDependencies" : { "jasmine" : "3.4.0" , "karma" : "4.1.0" , "karma-chrome-launcher" : "2.2.0" , "karma-jasmine" : "2.0.1" , "karma-webpack" : "3.0.5" , "webpack" : "4.30.0" } , "main" : "src/main.js" , "name" : "karma-webpack-babel-typescript-istanbul" , "scripts" : { "start" : "node index.js" , "test" : "karma start" } , "version" : "0.0.0" }
Common mistake
ERROR [preprocess]: Can not load “webpack”, it is not registered! Perhaps you are missing some plugin?
This happens when you run karma start
and you forgot to install webpack
.
Add Webpack configuration Thanks to webpack’s zero configuration mode and its default settings, we don’t need to specify much. All we do is defining a “development” mode to get detailed messages in case of preprocessing errors:
webpack.config.js 1 2 3 module .exports = { mode : 'development' };
Using a “development” mode will decrypt error messages like TypeError: r is not a function
.
Update Karma configuration In the previous Karma setup, our test code was relying that our business logic is exposed to the window
namespace (window.afterTwoSeconds
). Having webpack in place we will now load our business logic through our test code. That’s why we don’t need to declare our business logic anymore within Karma’s files
pattern. It’s sufficient if we just point Karma to our test code because the tests will import the main source code for us. We can also reuse our webpack configuration by requiring it. To activate webpack, we need to declare it as a preprocessor for our test code. We also need to add the webpack configuration to our Karma configuration:
karma.conf.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 const testCode = 'src/**/*test.js' ;const webpackConfig = require ('./webpack.config.js' );module .exports = function (config ) { config.set ({ autoWatch : false , basePath : '' , browsers : ['Chrome' ], colors : true , concurrency : Infinity , exclude : [], files : [ {pattern : testCode, watched : false } ], frameworks : ['jasmine' , 'webpack], logLevel: config.LOG_INFO, port: 9876, preprocessors: { [testCode]: [' webpack'] }, reporters: [' progress'], singleRun: true, webpack: webpackConfig }); };
Update imports Our test code will now be preprocessed by webpack which means that we can use Node.js features like require
statements to import code. Webpack will make sure that the require
statements get processed into something that can be understood by our Browser environment:
karma.conf.js 1 2 3 4 5 6 7 8 9 const afterTwoSeconds = require ('./main' );describe ('afterTwoSeconds' , () => { it ('resolves after 2 seconds' , async () => { const myCallbackSpy = jasmine.createSpy ('myCallbackSpy' ); await afterTwoSeconds (myCallbackSpy); expect (myCallbackSpy).toHaveBeenCalled (); }); });
Add Babel Babel 7 ships with TypeScript support and can be used to preprocess code with TypeScript’s compiler. You might not need Babel to compile your code with TypeScript but using Babel’s ecosystem (with presets like @babel/preset-env ) can bring enormous benefits if you want to ship code for various environments. That’s why it is the preferred setup in this tutorial, so let’s get started with a Babel setup:
Update dependencies 1 yarn add @babel/core babel-loader --dev
package.json 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 { "devDependencies" : { "@babel/core" : "7.4.4" , "babel-loader" : "8.0.5" , "jasmine" : "3.4.0" , "karma" : "4.1.0" , "karma-chrome-launcher" : "2.2.0" , "karma-jasmine" : "2.0.1" , "karma-webpack" : "3.0.5" , "webpack" : "4.30.0" } , "main" : "src/main.js" , "name" : "karma-webpack-babel-typescript-istanbul" , "scripts" : { "start" : "node index.js" , "test" : "karma start" } , "version" : "0.0.0" }
Add Babel configuration We will start with a very basic Babel configuration which does not define any plugin our sets of plugins (called presets). Without plugins Babel won’t do much which is okay for now and will be changed once we add TypeScript to our Babel toolchain.
babel.config.js 1 2 3 4 module .exports = { plugins : [], presets : [], };
Update webpack configuration With the babel-loader
we are telling webpack
to process files ending on .js
or .jsx
(/\.jsx?$/
) through Babel:
webpack.config.js 1 2 3 4 5 6 7 8 9 10 11 12 module .exports = { mode : 'development' , module : { rules : [ { exclude : /(node_modules)/ , loader : 'babel-loader' , test : /\.jsx?$/ , }, ], }, };
Add TypeScript Update dependencies 1 yarn add @babel/preset-typescript @types/jasmine @types/node typescript --dev
package.json 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 { "devDependencies" : { "@babel/core" : "7.4.4" , "@babel/preset-typescript" : "7.3.3" , "@types/jasmine" : "3.3.12" , "@types/node" : "12.0.0" , "babel-loader" : "8.0.5" , "jasmine" : "3.4.0" , "karma" : "4.1.0" , "karma-chrome-launcher" : "2.2.0" , "karma-jasmine" : "2.0.1" , "karma-webpack" : "3.0.5" , "typescript" : "3.4.5" , "webpack" : "4.30.0" } , "main" : "src/main.js" , "name" : "karma-webpack-babel-typescript-istanbul" , "scripts" : { "start" : "node index.js" , "test" : "karma start" } , "version" : "0.0.0" }
Add TypeScript configuration tsconfig.json 1 2 3 4 5 6 7 8 9 10 11 { "compilerOptions" : { "esModuleInterop" : true , "lib" : [ "es6" ] , "module" : "commonjs" , "outDir" : "dist" , "rootDir" : "src" , "strict" : true , "target" : "es6" } }
Update webpack configuration webpack.config.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 module .exports = { mode : 'development' , module : { rules : [ { exclude : /(node_modules)/ , loader : 'babel-loader' , test : /\.[tj]sx?$/ , }, ], }, resolve : { extensions : ['.js' , '.jsx' , '.ts' , '.tsx' ], }, };
Update Babel configuration babel.config.js 1 2 3 4 module .exports = { plugins : [], presets : ['@babel/preset-typescript' ], };
Migrate test code We need to rename main.test.js
to main.test.ts
. Thanks to the allowJs
TypeScript compiler option we can still import our JavaScript business logic within our test code:
src/main.test.ts 1 2 3 4 5 6 7 8 9 const afterTwoSeconds = require ('./main' );describe ('afterTwoSeconds' , () => { it ('resolves after 2 seconds' , async () => { const myCallbackSpy = jasmine.createSpy ('myCallbackSpy' ); await afterTwoSeconds (myCallbackSpy); expect (myCallbackSpy).toHaveBeenCalled (); }); });
Update Karma configuration Our Karma setup now needs to load our migrated test code:
karma.conf.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 const testCode = 'src/**/*test.ts' ;const webpackConfig = require ('./webpack.config.js' );module .exports = function (config ) { config.set ({ autoWatch : false , basePath : '' , browsers : ['Chrome' ], colors : true , concurrency : Infinity , exclude : [], files : [ {pattern : testCode, watched : false } ], frameworks : ['jasmine' ], logLevel : config.LOG_INFO , port : 9876 , preprocessors : { [testCode]: ['webpack' ] }, reporters : ['progress' ], singleRun : true , webpack : webpackConfig }); };
Migrate source code Update export src/main.ts 1 2 3 4 5 6 7 8 export function afterTwoSeconds (callback: Function ) { return new Promise ((resolve ) => { setTimeout (() => { callback (); resolve (); }, 2000 ); }); }
Update import src/main.test.ts 1 2 3 4 5 6 7 8 9 import {afterTwoSeconds} from './main' ;describe ('afterTwoSeconds' , () => { it ('resolves after 2 seconds' , async () => { const myCallbackSpy = jasmine.createSpy ('myCallbackSpy' ); await afterTwoSeconds (myCallbackSpy); expect (myCallbackSpy).toHaveBeenCalled (); }); });
Adjust start script package.json 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 { "devDependencies" : { "@babel/core" : "7.4.4" , "@babel/preset-typescript" : "7.3.3" , "@types/jasmine" : "3.3.12" , "@types/node" : "12.0.0" , "babel-loader" : "8.0.5" , "jasmine" : "3.4.0" , "karma" : "4.1.0" , "karma-chrome-launcher" : "2.2.0" , "karma-jasmine" : "2.0.1" , "karma-webpack" : "3.0.5" , "typescript" : "3.4.5" , "webpack" : "4.30.0" } , "main" : "dist/main.js" , "name" : "karma-webpack-babel-typescript-istanbul" , "scripts" : { "start" : "tsc && node dist/main.js" , "test" : "karma start" } , "version" : "0.0.0" }
Add code coverage Note: Every package prefixed with karma-
will be automatically added to Karma’s plugin section , so no need to define it.
Update dependencies 1 yarn add istanbul-instrumenter-loader karma-coverage-istanbul-reporter --dev
Update Karma configuration 1 yarn add istanbul-instrumenter-loader karma-coverage-istanbul-reporter --dev
package.json 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 { "devDependencies" : { "@babel/core" : "7.4.4" , "@babel/preset-typescript" : "7.3.3" , "@types/jasmine" : "3.3.12" , "@types/node" : "12.0.0" , "babel-loader" : "8.0.5" , "istanbul-instrumenter-loader" : "3.0.1" , "jasmine" : "3.4.0" , "karma" : "4.1.0" , "karma-chrome-launcher" : "2.2.0" , "karma-coverage-istanbul-reporter" : "2.0.5" , "karma-jasmine" : "2.0.1" , "karma-webpack" : "3.0.5" , "typescript" : "3.4.5" , "webpack" : "4.30.0" } , "main" : "dist/main.js" , "name" : "karma-webpack-babel-typescript-istanbul" , "scripts" : { "start" : "tsc && node dist/main.js" , "test" : "karma start" } , "version" : "0.0.0" }
Update webpack configuration webpack.config.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 module .exports = { mode : 'development' , module : { rules : [ { exclude : /(node_modules)/ , loader : 'babel-loader' , test : /\.[tj]sx?$/ , }, { enforce : 'post' , exclude : /(node_modules|\.test\.[tj]sx?$)/ , test : /\.[tj]s$/ , use : { loader : 'istanbul-instrumenter-loader' , options : {esModules : true } }, } ], }, resolve : { extensions : ['.js' , '.jsx' , '.ts' , '.tsx' ], }, };
Update Karma configuration karma.conf.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 const testCode = 'src/**/*test.ts' ;const webpackConfig = require ('./webpack.config.js' );module .exports = function (config ) { config.set ({ autoWatch : false , basePath : '' , browsers : ['Chrome' ], colors : true , concurrency : Infinity , coverageIstanbulReporter : { fixWebpackSourcePaths : true , reports : ['html' ] }, exclude : [], files : [ {pattern : testCode, watched : false } ], frameworks : ['jasmine' ], logLevel : config.LOG_INFO , port : 9876 , preprocessors : { [testCode]: ['webpack' ] }, reporters : ['progress' , 'coverage-istanbul' ], singleRun : true , webpack : webpackConfig }); };
Bonus: TypeScript everything! Since Webpack 5 you can turn the webpack.config.js
into a TypeScript file called webpack.config.ts
. If you are using Webpack 4, you will have to add @types/webpack
to get type definitions for Webpack’s configuration file.
Learn TypeScript For Free Whether you're a beginner or an experienced programmer, our free TypeScript course offers tremendous value for all skill levels. Presented by Benny , an experienced instructor and contributor to Visual Studio Code, our course covers everything from the fundamentals of the TypeScript programming language to in-depth explorations for advanced programmers.
You'll gain a solid understanding of the language and its features, setting a strong foundation for your programming journey. Subscribe now!