← Back to DevBytes

Karma: Complete Testing Guide for Developers

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:

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.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles