What is Karma?
Karma is a test runner tool created by the AngularJS team (now part of the broader Angular ecosystem) that executes JavaScript tests in real browsers. The name comes from the idea of bringing "karma" back to your code — a playful take on the concept of cause and effect in testing. Karma itself is not a testing framework; rather, it provides a complete environment for running tests written with frameworks like Jasmine, Mocha, QUnit, or any other testing library of your choice.
At its core, Karma spawns a web server that loads your application code alongside your test code into one or more browser instances. It then captures the test results from each browser, aggregates them, and reports them back to you — either in the terminal, via a reporter, or through continuous integration systems.
Why Karma Matters
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →Testing JavaScript code presents unique challenges compared to server-side languages. JavaScript runs in browsers with different engines, varying DOM implementations, and distinct runtime behaviors. A test that passes in Chrome might fail in Safari due to subtle differences in how the engines handle certain operations. Karma solves this by allowing you to run the same test suite across multiple browsers simultaneously.
Key benefits include:
- Real browser environments: Tests execute in actual browsers, not simulated environments like jsdom
- Cross-browser validation: Run tests against Chrome, Firefox, Safari, Edge, and even mobile browsers
- Continuous integration ready: Headless browser support and CI-friendly reporters
- Framework agnostic: Works with Jasmine, Mocha, QUnit, Jest (via adapters), and more
- Live reloading: Watch mode automatically re-runs tests when files change
- Debugging support: Step through tests directly in the browser's developer tools
Getting Started with Karma
Installation
Karma is distributed as a Node.js package. You'll need Node.js and npm installed on your system before proceeding. The recommended approach is to install Karma locally within your project along with the necessary plugins.
# Initialize a new project (if not already initialized)
npm init -y
# Install Karma and its CLI globally is optional, but local is preferred
npm install --save-dev karma
# Install the CLI runner locally
npm install --save-dev karma-cli
# Install browser launchers (choose based on your needs)
npm install --save-dev karma-chrome-launcher
npm install --save-dev karma-firefox-launcher
npm install --save-dev karma-safari-launcher
# Install a testing framework adapter — Jasmine is a common choice
npm install --save-dev karma-jasmine
npm install --save-dev jasmine-core
If you prefer a guided setup, Karma ships with an interactive initialization command:
npx karma init
This wizard will ask you a series of questions about your preferred testing framework, browsers, and file patterns, then generate a karma.conf.js file for you.
Configuration
The heart of Karma is its configuration file. Whether you generate it automatically or write it by hand, understanding each section is essential. Here is a comprehensive configuration file with annotations:
// karma.conf.js
module.exports = function(config) {
config.set({
// Base path for resolving all relative paths
// This should be set to the root of your project
basePath: '.',
// Testing framework adapters to use
// Order matters: framework plugins must be listed
frameworks: ['jasmine'],
// List of files/patterns to load in the browser
// The order within each array group is respected
files: [
// Source files — your application code
'src/**/*.js',
// Dependencies (like Angular, React, etc.) loaded before tests
// These can be CDN files or local vendor files
'node_modules/angular/angular.js',
'node_modules/angular-mocks/angular-mocks.js',
// Test files — usually follow a naming convention
'test/**/*.spec.js',
// Individual fixture or helper files
{ pattern: 'test/fixtures/**/*.html', included: false, served: true }
],
// Patterns to exclude from the file list
exclude: [
'src/vendor/**/*.min.js',
'test/legacy/**/*.spec.js'
],
// Preprocessors transform files before serving them
// TypeScript, ES6 transpilation, code coverage instrumentation
preprocessors: {
'src/**/*.js': ['coverage'],
'src/**/*.ts': ['typescript']
},
// Reporters produce test output in various formats
// 'progress' shows dots in terminal; 'coverage' generates coverage data
reporters: ['progress', 'coverage', 'kjhtml'],
// Where to store coverage reports
coverageReporter: {
type: 'html',
dir: 'coverage/'
},
// Web server port (default 9876)
port: 9876,
// Enable colors in reporters and logs
colors: true,
// Logging level
// Possible values: LOG_DISABLE, LOG_ERROR, LOG_WARN, LOG_INFO, LOG_DEBUG
logLevel: config.LOG_INFO,
// Enable/disable watching files and re-running tests on changes
autoWatch: true,
// Browsers to launch and capture
// Available browser launchers must be installed separately
browsers: ['Chrome', 'Firefox', 'Safari'],
// Custom browser launchers (e.g., headless Chrome for CI)
customLaunchers: {
ChromeHeadless: {
base: 'Chrome',
flags: ['--headless', '--disable-gpu', '--no-sandbox']
}
},
// If true, Karma will start a browser, run tests, then exit
singleRun: false,
// How many browsers can run simultaneously
concurrency: Infinity,
// Plugins that extend Karma's functionality
plugins: [
'karma-jasmine',
'karma-chrome-launcher',
'karma-firefox-launcher',
'karma-coverage'
]
});
};
Writing Your First Test
With Karma configured, you can write tests using your chosen framework. Below is an example using Jasmine, which is the most commonly paired framework with Karma.
First, create a simple source file to test:
// src/calculator.js
var Calculator = (function() {
function Calculator() {
this.result = 0;
}
Calculator.prototype.add = function(a, b) {
return a + b;
};
Calculator.prototype.subtract = function(a, b) {
return a - b;
};
Calculator.prototype.divide = function(a, b) {
if (b === 0) {
throw new Error('Division by zero is not allowed');
}
return a / b;
};
Calculator.prototype.multiply = function(a, b) {
return a * b;
};
return Calculator;
})();
Then write the corresponding test file:
// test/calculator.spec.js
describe('Calculator', function() {
var calculator;
// Runs before each individual test
beforeEach(function() {
calculator = new Calculator();
});
// Test suite for the add method
describe('add()', function() {
it('should add two positive numbers correctly', function() {
expect(calculator.add(2, 3)).toBe(5);
});
it('should handle negative numbers', function() {
expect(calculator.add(-5, 3)).toBe(-2);
});
it('should handle zero values', function() {
expect(calculator.add(0, 0)).toBe(0);
expect(calculator.add(5, 0)).toBe(5);
});
});
// Test suite for the subtract method
describe('subtract()', function() {
it('should subtract two numbers correctly', function() {
expect(calculator.subtract(10, 4)).toBe(6);
});
it('should return negative when subtracting larger from smaller', function() {
expect(calculator.subtract(3, 8)).toBe(-5);
});
});
// Test suite for the divide method
describe('divide()', function() {
it('should divide two numbers correctly', function() {
expect(calculator.divide(15, 3)).toBe(5);
});
it('should throw an error when dividing by zero', function() {
expect(function() {
calculator.divide(10, 0);
}).toThrow(new Error('Division by zero is not allowed'));
});
it('should handle floating point division', function() {
expect(calculator.divide(7, 2)).toBe(3.5);
});
});
// Test suite for the multiply method
describe('multiply()', function() {
it('should multiply two positive numbers', function() {
expect(calculator.multiply(4, 5)).toBe(20);
});
it('should return zero when multiplying by zero', function() {
expect(calculator.multiply(100, 0)).toBe(0);
});
it('should multiply negative numbers correctly', function() {
expect(calculator.multiply(-3, -3)).toBe(9);
expect(calculator.multiply(-3, 4)).toBe(-12);
});
});
// Demonstrate setup and teardown hooks
describe('with persistent state', function() {
var counter;
beforeEach(function() {
counter = 0;
});
afterEach(function() {
// Cleanup logic here — reset DOM changes, clear timers, etc.
counter = null;
});
it('should increment counter', function() {
counter++;
expect(counter).toBe(1);
});
// beforeAll and afterAll run once per describe block
beforeAll(function() {
// Expensive setup: create test fixtures, mock servers, etc.
console.log('Setting up expensive resources...');
});
afterAll(function() {
// Tear down expensive resources
console.log('Cleaning up expensive resources...');
});
it('should maintain independent state', function() {
expect(counter).toBe(0);
});
});
});
Run the tests with:
npx karma start karma.conf.js
Or add a script to your package.json for convenience:
{
"scripts": {
"test": "karma start karma.conf.js",
"test:ci": "karma start karma.conf.js --single-run --browsers=ChromeHeadless"
}
}
Integrating with Testing Frameworks
Karma's adapter architecture means you can swap testing frameworks without changing your test runner setup. Each framework requires its own adapter plugin.
Jasmine Integration
npm install --save-dev karma-jasmine jasmine-core
Configure in karma.conf.js:
frameworks: ['jasmine'],
plugins: ['karma-jasmine']
Mocha Integration
Mocha gives you more flexibility with assertion libraries like Chai or should.js:
npm install --save-dev karma-mocha mocha
npm install --save-dev karma-chai chai
frameworks: ['mocha', 'chai'],
plugins: ['karma-mocha', 'karma-chai']
Example Mocha test with Chai assertions:
// test/mocha-example.spec.js
describe('Array operations with Mocha and Chai', function() {
describe('indexOf()', function() {
it('should return -1 when value is not present', function() {
var arr = [1, 2, 3];
expect(arr.indexOf(4)).to.equal(-1);
});
it('should return the correct index when value exists', function() {
var arr = ['apple', 'banana', 'cherry'];
expect(arr.indexOf('banana')).to.equal(1);
});
});
});
Jest Integration
While Jest has its own runner, you can use Karma as the browser executor with jest adapter:
npm install --save-dev karma-jest jest
Working with Browsers
Installing Browser Launchers
Each browser requires its own launcher plugin. Karma maintains official plugins for all major browsers:
# Chrome (includes headless support)
npm install --save-dev karma-chrome-launcher
# Firefox
npm install --save-dev karma-firefox-launcher
# Safari (macOS only)
npm install --save-dev karma-safari-launcher
# Microsoft Edge
npm install --save-dev karma-edge-launcher
# Internet Explorer (legacy support)
npm install --save-dev karma-ie-launcher
# Opera
npm install --save-dev karma-opera-launcher
Headless Browser Configuration for CI
Continuous integration servers don't have graphical displays, so headless modes are essential. Here's how to configure headless Chrome and Firefox:
// karma.conf.js — CI configuration
module.exports = function(config) {
config.set({
// ... base configuration ...
customLaunchers: {
// Headless Chrome with sandbox disabled for CI containers
ChromeHeadlessNoSandbox: {
base: 'ChromeHeadless',
flags: [
'--no-sandbox',
'--disable-gpu',
'--disable-dev-shm-usage',
'--remote-debugging-port=9222'
]
},
// Headless Firefox
FirefoxHeadless: {
base: 'Firefox',
flags: ['-headless']
}
},
// For CI, use single-run and headless browsers
browsers: ['ChromeHeadlessNoSandbox', 'FirefoxHeadless'],
singleRun: true,
// CI-friendly reporters (JUnit XML for Jenkins, etc.)
reporters: ['progress', 'junit', 'coverage'],
junitReporter: {
outputDir: 'test-results/',
outputFile: 'junit-results.xml',
suite: 'unit-tests',
useBrowserName: true
}
});
};
Capturing Additional Browsers Manually
Karma can connect to any browser on your network by navigating to the Karma server URL. This is useful for testing on mobile devices or browsers without dedicated launcher plugins:
# Start Karma without launching any browser
npx karma start karma.conf.js --no-browsers
Then open http://localhost:9876 (or your configured port) on any device. The browser will be automatically captured and will run all tests. You can capture as many browsers as you need simultaneously.
Advanced Configuration
Code Coverage with Istanbul
Measuring code coverage helps you identify untested parts of your codebase. Karma integrates with Istanbul via the karma-coverage plugin:
npm install --save-dev karma-coverage istanbul
// In karma.conf.js
preprocessors: {
'src/**/*.js': ['coverage']
},
coverageReporter: {
// Generate HTML report for humans
reporters: [
{ type: 'html', dir: 'coverage/', subdir: 'html' },
{ type: 'lcov', dir: 'coverage/', subdir: 'lcov' },
{ type: 'text-summary' } // Summary in terminal
],
// Watermarks for coverage thresholds
watermarks: {
statements: [50, 80],
branches: [50, 80],
functions: [50, 80],
lines: [50, 80]
},
// Include/exclude patterns
includeAllSources: true
},
reporters: ['progress', 'coverage']
TypeScript Support
For TypeScript projects, use karma-typescript to transpile on-the-fly:
npm install --save-dev karma-typescript typescript @types/jasmine
// karma.conf.js with TypeScript
frameworks: ['jasmine', 'karma-typescript'],
files: [
'src/**/*.ts',
'test/**/*.spec.ts'
],
preprocessors: {
'src/**/*.ts': ['karma-typescript'],
'test/**/*.ts': ['karma-typescript']
},
karmaTypescriptConfig: {
compilerOptions: {
module: 'commonjs',
target: 'ES5',
sourceMap: true,
strict: true
},
include: ['src/', 'test/'],
reports: {
html: 'coverage',
text: ''
}
},
plugins: [
'karma-jasmine',
'karma-chrome-launcher',
'karma-typescript'
]
Webpack Integration
Modern front-end projects using module bundlers need Karma to work with Webpack for proper module resolution:
npm install --save-dev karma-webpack webpack
// karma.conf.js with Webpack
module.exports = function(config) {
config.set({
basePath: '.',
frameworks: ['jasmine'],
files: [
// Single entry point that requires all tests
'test/entrypoint.js'
],
preprocessors: {
'test/entrypoint.js': ['webpack', 'sourcemap']
},
webpack: {
mode: 'development',
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
},
resolve: {
modules: ['node_modules', 'src']
}
},
webpackMiddleware: {
stats: 'errors-only',
logLevel: 'warn'
},
browsers: ['Chrome'],
plugins: [
'karma-jasmine',
'karma-chrome-launcher',
'karma-webpack',
'karma-sourcemap-loader'
]
});
};
The entrypoint file gathers all test files:
// test/entrypoint.js
// Use require.context to dynamically load all spec files
var testsContext = require.context('./', true, /\.spec\.js$/);
testsContext.keys().forEach(testsContext);
// Alternatively, manually require each test
// require('./unit/calculator.spec.js');
// require('./unit/formatter.spec.js');
// require('./integration/api.spec.js');
Custom Reporters
Karma's output can be tailored for different workflows:
npm install --save-dev karma-junit-reporter karma-html-reporter karma-spec-reporter
reporters: ['spec', 'junit', 'html'],
specReporter: {
maxLogLines: 5,
suppressErrorSummary: false,
suppressFailed: false,
suppressPassed: false,
suppressSkipped: true
},
htmlReporter: {
outputDir: 'reports/html/',
templatePath: null,
reportName: 'test-report'
},
junitReporter: {
outputDir: 'reports/junit/',
outputFile: 'test-results.xml',
suite: 'unit',
useBrowserName: true
}
Best Practices
File Organization
A well-structured project makes test configuration straightforward. Keep your source and test files in separate but mirrored directory trees:
project/
├── src/
│ ├── components/
│ │ └── button.js
│ ├── services/
│ │ └── api.js
│ └── utils/
│ └── formatter.js
├── test/
│ ├── unit/
│ │ ├── components/
│ │ │ └── button.spec.js
│ │ ├── services/
│ │ │ └── api.spec.js
│ │ └── utils/
│ │ └── formatter.spec.js
│ ├── integration/
│ │ └── user-flow.spec.js
│ └── fixtures/
│ ├── mock-data.json
│ └── sample-response.xml
├── karma.conf.js
└── package.json
Test Isolation
Every test should be independent and not rely on the execution order of other tests. Karma loads files in the order specified, but test frameworks may run specs in any order. Use beforeEach and afterEach hooks to reset state:
describe('ShoppingCart', function() {
var cart;
var productRepository;
beforeEach(function() {
// Create fresh instances before each test
productRepository = new MockProductRepository();
cart = new ShoppingCart(productRepository);
// Reset any global state or DOM elements
document.getElementById('cart-container').innerHTML = '';
});
afterEach(function() {
// Clean up timers, intervals, subscriptions
jasmine.clock().uninstall();
cart.destroy();
});
it('should add items correctly', function() {
cart.addItem({ id: 1, name: 'Book', price: 29.99 });
expect(cart.itemCount()).toBe(1);
expect(cart.total()).toBe(29.99);
});
it('should start with empty cart', function() {
expect(cart.itemCount()).toBe(0);
expect(cart.total()).toBe(0);
});
});
Mocking and Spies
Use framework-provided spies and mocks instead of writing custom stubs. Jasmine's built-in spy functionality integrates seamlessly:
describe('UserService', function() {
var userService;
var httpBackend;
beforeEach(function() {
// Jasmine spies on existing methods
spyOn(window, 'fetch').and.callThrough();
// Create spy objects
var mockLogger = jasmine.createSpyObj('Logger', ['info', 'error', 'warn']);
mockLogger.info.and.callFake(function(msg) {
console.log('[TEST]', msg);
});
userService = new UserService(mockLogger);
});
it('should log successful user fetch', function() {
// Arrange
var mockResponse = { id: 1, name: 'John' };
spyOn(window, 'fetch').and.returnValue(Promise.resolve({
json: function() { return Promise.resolve(mockResponse); }
}));
// Act
userService.fetchUser(1);
// Assert
expect(window.fetch).toHaveBeenCalledWith('/api/users/1');
});
it('should handle network errors gracefully', function() {
spyOn(window, 'fetch').and.returnValue(Promise.reject(new Error('Network error')));
var errorHandler = jasmine.createSpy('errorHandler');
userService.fetchUser(1).catch(errorHandler);
// Wait for promises to resolve in async tests
// Use done() callback or async/await
});
});
Async Testing
Handling asynchronous code correctly prevents flaky tests. Jasmine provides multiple mechanisms:
describe('Async API Client', function() {
var apiClient;
beforeEach(function() {
apiClient = new ApiClient('https://api.example.com');
});
// Using the done() callback
it('should fetch data using done callback', function(done) {
apiClient.getData('/items', function(err, response) {
expect(err).toBeNull();
expect(response.items).toBeDefined();
expect(response.items.length).toBeGreaterThan(0);
done(); // Signal test completion
});
});
// Using promises with done
it('should handle promises', function(done) {
apiClient.fetchAsync('/items')
.then(function(data) {
expect(data.status).toBe('success');
done();
})
.catch(function(error) {
// Fail the test explicitly
done.fail('Promise rejected with: ' + error);
});
});
// Using async/await (requires ES2017+ support)
it('should work with async/await', async function() {
var result = await apiClient.fetchAsync('/users');
expect(result.users).toBeInstanceOf(Array);
});
});
Performance Optimization
As your test suite grows, execution time becomes critical. Here are strategies to keep tests fast:
// Split karma configurations for different test types
// karma.unit.conf.js — fast, isolated unit tests
// karma.integration.conf.js — slower integration tests
// karma.e2e.conf.js — end-to-end tests (may use separate tool like Protractor)
// Unit test configuration — optimized for speed
module.exports = function(config) {
config.set({
frameworks: ['jasmine'],
files: [
'test/unit/**/*.spec.js'
],
browsers: ['ChromeHeadless'],
singleRun: true,
concurrency: 4, // Parallel execution where possible
// Minimize logging overhead
logLevel: config.LOG_ERROR,
reporters: ['progress']
});
};
// In package.json scripts
{
"scripts": {
"test:unit": "karma start karma.unit.conf.js",
"test:integration": "karma start karma.integration.conf.js",
"test:all": "npm run test:unit && npm run test:integration"
}
}
Debugging Tests
When a test fails unexpectedly, Karma provides several debugging approaches:
// Enable verbose logging
// In karma.conf.js
logLevel: config.LOG_DEBUG
// Use browser developer tools
// Start Karma without singleRun and with autoWatch
// Navigate to http://localhost:9876/debug.html
// Open DevTools, set breakpoints, and re-run tests
// Isolate failing test with fdescribe/fit (Jasmine)
fdescribe('ProblemComponent', function() {
fit('should handle edge case X', function() {
// Only this test runs, making debugging faster
expect(someComplexCalculation()).toBe(expectedValue);
});
});
// Use pause and inspect
it('debugging test', function() {
var intermediate = someFunction();
// Add debugger statement for browser DevTools
debugger;
expect(intermediate).toBeTruthy();
});
Common Issues and Troubleshooting
Browser Not Launching
This is the most frequent issue. Check that the browser binary exists and the launcher plugin is installed:
# Verify Chrome is installed
which google-chrome
# or on macOS
ls /Applications/Google\ Chrome.app/
# Set custom binary path if needed
browsers: ['Chrome'],
customLaunchers: {
ChromeCustom: {
base: 'Chrome',
command: '/usr/bin/google-chrome'
}
}
File Loading Order Problems
If dependencies load after the code that uses them, you'll get undefined reference errors. Control ordering in the files array:
files: [
// Load vendor libraries first
'node_modules/jquery/dist/jquery.min.js',
'node_modules/angular/angular.min.js',
// Then application modules in dependency order
'src/app.module.js',
'src/services/config.service.js',
'src/controllers/*.js',
'src/directives/*.js',
// Test helpers and mocks
'test/mocks/**/*.js',
// Finally, test specs
'test/**/*.spec.js'
]
Memory Leaks and Timeouts
Long-running test suites can exhaust browser memory or hit timeout limits:
// Increase timeout for slow tests
// Jasmine: override default 5-second timeout
describe('Slow integration tests', function() {
var originalTimeout;
beforeEach(function() {
originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL;
jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000; // 30 seconds
});
afterEach(function() {
jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout;
});
it('long-running operation', function(done) {
// This test now has 30 seconds to complete
performSlowOperation().then(done);
});
});
// Karma-level timeouts
// In karma.conf.js
captureTimeout: 60000, // Time to capture a browser (ms)
browserDisconnectTimeout: 10000, // Time before disconnecting
browserNoActivityTimeout: 30000, // Timeout for inactive browser
PhantomJS Deprecation
PhantomJS was once the standard headless browser for Karma, but it's now deprecated. Migrate to headless Chrome or Firefox:
# Remove PhantomJS
npm uninstall karma-phantomjs-launcher phantomjs
# Replace with headless Chrome
npm install --save-dev karma-chrome-launcher
# Update karma.conf.js
browsers: ['ChromeHeadless'],
customLaunchers: {
ChromeHeadless: {
base: 'Chrome',
flags: ['--headless', '--disable-gpu', '--no-sandbox']
}
}
Conclusion
Karma remains a powerful and flexible test runner that bridges the gap between your JavaScript code and real browser environments. By executing tests across multiple browsers simultaneously, it catches cross-browser issues early in the development cycle. Its plugin architecture lets you adapt to virtually any testing framework, assertion library, or reporting format your project requires.
The key to mastering Karma lies in thoughtful configuration: structuring your file patterns correctly, selecting appropriate browser launchers for both development and CI environments, integrating coverage and reporting tools, and maintaining test isolation through proper setup and teardown hooks. Whether you're working on a small library or a large enterprise application, the practices outlined in this guide will help you build a reliable, fast, and maintainable test suite. Start with a simple configuration, add complexity as your project grows, and always keep your tests running against real browsers — that's the Karma way.