Key Takeaways
- Jasmine defines your tests; Karma serves the files, connects browsers, and reports results to the terminal.
- Load AngularJS before angular-mocks, then application code, then specs. Match angular-mocks to your AngularJS version.
- Install a browser launcher and provide an actual browser executable. The launcher alone doesn't install Chrome.
- Use watch mode locally and single-run mode in CI. Commit package-lock.json so dependency installation is repeatable.
- This is a legacy-maintenance setup: AngularJS is unsupported upstream, and Karma is deprecated.
To test an existing AngularJS application with Jasmine and Karma, configure Karma to load Jasmine, AngularJS, angular-mocks, your application, and your specs into a browser. Use angular-mocks to configure the test injector, instantiate controllers or retrieve services, and assert their behavior with Jasmine.
AngularJS still appears in legacy applications; that isn't a reason to choose it for new work. This tutorial assumes basic JavaScript, Node.js, and AngularJS knowledge and focuses on repairing or extending an existing test suite.
How Jasmine, Karma, and angular-mocks Fit Together
Karma runs a server under Node.js, serves your test files, and coordinates connected browsers. Jasmine executes the specifications inside those browsers. A launcher automates browser startup; manually opening a browser session is another way to connect.
Terminal: start Karma under Node.js
|
+-- Chrome launcher starts the browser
|
+-- serves files in order:
Jasmine + adapter
AngularJS -> angular-mocks -> app -> specs
|
Browser runs tests
|
Terminal: reporter <----------- Karma <------------------+Unit testing is useful whether or not you follow test-driven development. Start by proving that this execution path works, then replace the smoke test with assertions about application behavior.
Getting Started with Reproducible Dependencies
For an existing repository, begin with its committed lockfile and approved dependency versions. Don't replace a working legacy dependency graph with whatever npm currently resolves. Keep angular-mocks on the same version as AngularJS and validate changes against your application's plugins and browser requirements.
The following pins define an isolated demonstration, not the latest releases or a security-approved dependency set:
| Package | Demo version | Responsibility |
|---|---|---|
| angular | 1.8.3 | Application runtime |
| angular-mocks | 1.8.3 | AngularJS testing helpers |
| karma | 6.4.4 | Browser test runner |
| karma-jasmine | 5.1.0 | Jasmine adapter for Karma |
| jasmine-core | 4.6.1 | Test framework and assertions |
| karma-chrome-launcher | 3.2.0 | Chrome browser launcher |
In an empty demonstration directory, run:
npm init -y
npm install --save-exact angular@1.8.3
npm install --save-dev --save-exact angular-mocks@1.8.3 karma@6.4.4 karma-jasmine@5.1.0 jasmine-core@4.6.1 karma-chrome-launcher@3.2.0npm's installation options let you save test tools as development dependencies and save exact direct versions. Commit package.json and package-lock.json together; direct pins alone don't freeze transitive dependencies.
Install Chrome separately and use a supported Node.js release that you've validated against your locked toolchain. Record the Node.js, npm, and browser versions used by CI.
Create the AngularJS Module and Controller
Create a src directory and save this as src/app.js. The example intentionally retains a small $scope-based controller commonly found in legacy code:
'use strict';
angular.module('myModule', [])
.controller('myCtrl', ['$scope', function ($scope) {
$scope.count = 0;
$scope.add = function () {
$scope.count += 1;
};
}]);The module is created before its controller is registered. The array annotation explicitly names the controller's dependency. For larger applications, keep controllers focused on view behavior; see AngularJS controller responsibilities.
Complete Karma Configuration for AngularJS
Create karma.conf.js in the project root. You can use npx karma init as an optional starting point, but accepting its defaults doesn't establish the application's dependencies or file order. Use this complete configuration for the demonstration:
'use strict';
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine'],
plugins: ['karma-jasmine', 'karma-chrome-launcher'],
files: [
'node_modules/angular/angular.js',
'node_modules/angular-mocks/angular-mocks.js',
'src/app.js',
'*.spec.js'
],
preprocessors: {},
reporters: ['progress'],
browsers: ['ChromeHeadless'],
hostname: '127.0.0.1',
listenAddress: '127.0.0.1',
port: 9876,
autoWatch: false,
singleRun: true,
failOnEmptyTestSuite: true
});
};The Karma configuration reference documents these options. This configuration selects Jasmine, loads the adapter and launcher explicitly, reports progress, and defaults to a single headless run. No preprocessing is needed for these plain browser scripts.
AngularJS must precede angular-mocks; application module definitions and registrations must precede specs. Karma's file patterns control inclusion order, so don't rely on a broad glob to discover dependency order in a larger application.
Write and Run a Jasmine Smoke Test
Create test.spec.js in the project root:
describe('test harness', function () {
it('executes a Jasmine assertion', function () {
expect(0).toEqual(0);
});
});In Jasmine's test vocabulary, describe groups related specifications, it defines an individual specification, and expect starts an assertion. The toEqual matcher compares values for equality. This test proves the harness executes an assertion, not that your controller works.
Separate Watch Mode from CI
Merge this scripts fragment into package.json, preserving the dependency fields created during installation:
{
"scripts": {
"test": "karma start karma.conf.js --single-run --no-auto-watch",
"test:watch": "karma start karma.conf.js --no-single-run --auto-watch --browsers Chrome"
}
}Run the smoke test once:
npm testThe configured launcher starts headless Chrome, and the progress reporter writes results to the terminal. With singleRun enabled, Karma exits with code 0 when all tests pass or code 1 when tests fail.
For interactive development, run:
npm run test:watchThis command opens visible Chrome, enables file watching, and keeps Karma running. autoWatch controls reruns after file changes; singleRun controls whether Karma exits after a run. Setting singleRun to false alone isn't the instruction to watch files.
To connect a browser manually instead of launching one:
npx karma start karma.conf.js --no-browsers --no-single-run --auto-watchOpen the URL Karma prints in a browser. Karma's default port is 9876, but configuration or port availability can change the address. This page connects the browser to the runner; the terminal reporter remains the main results view. The example binds to loopback, so remote devices require deliberate network configuration.
Run the Same Suite in CI
Once package-lock.json is committed, use:
npm ci --include=dev
npm testnpm ci installs from the lockfile and fails if its dependencies don't match package.json; it doesn't rewrite the lockfile. Including development dependencies ensures the test tools are installed. Provision Chrome in the CI environment, and set CHROME_BIN to its executable path if the Chrome launcher can't find it.
Test an AngularJS Controller with Jasmine
Replace the entire smoke test in test.spec.js with:
describe('myCtrl', function () {
let scope;
beforeEach(angular.mock.module('myModule'));
beforeEach(angular.mock.inject(function ($controller, $rootScope) {
scope = $rootScope.$new();
$controller('myCtrl', { $scope: scope });
}));
afterEach(function () {
if (scope) {
scope.$destroy();
}
});
describe('$scope.count', function () {
it('starts at zero', function () {
expect(scope.count).toBe(0);
});
it('increments on every call to add', function () {
scope.add();
expect(scope.count).toBe(1);
scope.add();
expect(scope.count).toBe(2);
});
});
});The setup has three distinct jobs:
- angular.mock.module('myModule') registers the application module for the test injector.
- angular.mock.inject resolves dependencies for the setup function.
- $controller('myCtrl', { $scope: scope }) instantiates the registered controller and supplies this test's scope through its locals object.
The module name doesn't belong in the locals object. Passing $scope explicitly is already the correct pattern. These assertions check initialization and repeated behavior, rather than merely checking that a controller can be constructed.
When You Need a Real Scope or Component Controller
A plain object is sufficient for this controller's property assignments. Using $rootScope.$new() also provides actual scope methods when a controller uses watchers or events; destroy that child scope after the test. No digest is needed for the synchronous assignments above.
If behavior depends on watchers, trigger a digest before asserting their effects. Our guide to AngularJS watchers explains that distinction.
For controllers registered through .component(), use $componentController(componentName, locals, bindings) to test controller logic without rendering a template. It doesn't reproduce template compilation or automatically run lifecycle hooks; invoke hooks such as $onInit explicitly when the test needs them. Use a compiled component test when DOM behavior or binding integration is what you're testing.
Test Services and Filters with angular-mocks
The same module-and-injection setup works beyond controllers. Append these registrations to src/app.js; don't recreate myModule with another dependency array:
// Append to src/app.js.
angular.module('myModule')
.factory('counterMath', function () {
return {
next: function (value) {
return value + 1;
}
};
})
.filter('countLabel', function () {
return function (count) {
return count === 1 ? '1 item' : count + ' items';
};
});Create domain.spec.js alongside test.spec.js. The existing *.spec.js pattern loads both files:
describe('counter helpers', function () {
beforeEach(angular.mock.module('myModule'));
it('calculates the next count', angular.mock.inject(function (counterMath) {
expect(counterMath.next(0)).toBe(1);
expect(counterMath.next(9)).toBe(10);
}));
it('formats singular and plural labels', angular.mock.inject(function ($filter) {
const countLabel = $filter('countLabel');
expect(countLabel(0)).toBe('0 items');
expect(countLabel(1)).toBe('1 item');
expect(countLabel(2)).toBe('2 items');
}));
});A service can be injected by its registered name; $filter('countLabel') retrieves the filter function. For services that make HTTP requests, use ngMock's $httpBackend instead of contacting real servers; see our $httpBackend testing tutorial.
Common Mistakes
- angular.mock.module is undefined: check that Jasmine is configured and angular-mocks loads after AngularJS. These module and injection helpers are defined when angular-mocks detects Jasmine or Mocha.
- Unknown module or controller: check file paths and registration names. Create a module with angular.module('myModule', []) once; retrieve it with angular.module('myModule') elsewhere.
- No browser starts: check the launcher plugin, configured browser name, installed executable, and executable permissions. A package installation isn't a browser installation.
- CI never finishes: don't use the watch command in CI. Keep single-run behavior explicit.
- Template requests fail: controller-only tests don't compile templates. For compiled tests involving templateUrl, see preloading AngularJS templates for Karma.
FAQ
How do I test an AngularJS controller with Jasmine?
Register the application module with angular.mock.module, then use angular.mock.inject to obtain $controller. Instantiate the controller with an explicit $scope local and assert both its initial state and the effects of calling its methods.
What does angular-mocks do in AngularJS tests?
angular-mocks provides ngMock, module-loading and injection helpers, and test-oriented implementations or extensions of AngularJS services. It supports this injector-based setup, but isn't required for testing a standalone JavaScript function with Jasmine.
Why is Karma not launching a browser?
Automatic launching requires a configured browser, its launcher plugin, and an available browser executable. For Chrome, install karma-chrome-launcher, configure Chrome or ChromeHeadless, and set CHROME_BIN if executable discovery fails.
How do I run Karma tests once in CI instead of watch mode?
Set singleRun to true or pass --single-run, and configure a browser launcher. Disable watching for clarity; Karma exits with code 0 when all tests pass or code 1 when tests fail.
Is AngularJS still supported in 2026?
Official AngularJS support ended in January 2022, so the upstream project no longer provides ongoing security or compatibility maintenance. Teams continuing to use it must manage that risk themselves or arrange third-party support; unit tests don't replace security updates.
Further Reading
- AngularJS security policy — official support status.
- Karma project status — deprecation and maintenance scope.
- Karma configuration reference — browsers, reporters, watching, and single-run behavior.
- Jasmine: your first suite — specifications, setup, and matchers.
- AngularJS ngMock reference — testing helpers and service replacements.
- AngularJS component guide — component-controller unit testing.
- Chrome launcher documentation — browser names and executable configuration.
- npm ci documentation — lockfile-based installation for CI.
