Tutorials

$httpBackend | Quick Tutorial With Example

Key Takeaways

  • Use the ngMock version of $httpBackend to test AngularJS code that calls $http without contacting an API server.
  • expectGET() requires a matching request; whenGET() supplies a reusable response without requiring a request.
  • Call flush() when you need to deliver pending mocked responses, then assert the resulting application state.
  • Verify both outstanding expectations and outstanding requests after each test.
  • AngularJS is end-of-life. These techniques are for maintaining existing applications, not choosing a framework for a new project.

AngularJS $httpBackend unit testing lets you exercise code that uses $http while replacing the network with controlled responses. You define the expected request, invoke your application code, deliver the response, and check the result—without starting an API server locally or depending on one in CI.

What $httpBackend Mocks

Production AngularJS already has a service named $httpBackend: it performs the underlying requests for $http. The test dependency angular-mocks provides the ngMock module, which replaces that backend with a mock supporting expectations, response definitions, flushing, and verification.

You keep the real $http service, including its promise-based API and response transformations. Your test controls the backend response rather than replacing $http.get() with a hand-written stub. The official ngMock implementation documents this distinction.

A Complete AngularJS $httpBackend Unit Testing Example

1. Install and Configure the Jasmine Browser Runner

This standalone fixture uses AngularJS 1.8.3, matching angular-mocks, and Jasmine through jasmine-browser-runner. Start in a directory containing package.json, with Node.js/npm compatible with the runner and Chrome installed.

npm install --save-dev angular@1.8.3 angular-mocks@1.8.3 jasmine-core jasmine-browser-runner
npx jasmine-browser-runner init

Following the official Jasmine browser setup, replace spec/support/jasmine-browser.mjs with:

export default {
  srcDir: '.',
  srcFiles: [
    'node_modules/angular/angular.js',
    'node_modules/angular-mocks/angular-mocks.js',
    'app.js'
  ],
  specDir: 'spec',
  specFiles: ['**/*Spec.js'],
  helpers: [],
  browser: 'headlessChrome'
};

The runner configuration reference explains these file paths and browser settings. Load AngularJS before angular-mocks, and load the application before its specs; Jasmine must also be initialized before angular-mocks registers its test helpers.

If your application already runs Jasmine through Karma, keep that working setup and add the files to its existing configuration. Our AngularJS testing with Jasmine and Karma tutorial covers that arrangement.

2. Register the Application Module and Controller

Create app.js in the project root. This preserves the original controller example while adding an error path and an optional request configuration for the timeout test later.

angular.module('myApp', [])
  .controller('myCtrl', ['$scope', '$http', function($scope, $http) {
    $scope.response = null;
    $scope.error = null;

    $scope.getResponse = function(config) {
      $scope.error = null;
      $scope.response = null;

      return $http.get('/api/data', config).then(
        function(result) {
          $scope.response = result.data;
          return result.data;
        },
        function(error) {
          $scope.error = {
            status: error.status,
            xhrStatus: error.xhrStatus
          };
        }
      );
    };
  }]);

The error callback deliberately handles the rejection by recording it on the scope. In a larger application, put reusable HTTP logic in a service rather than expanding the controller; see AngularJS controller responsibilities.

3. Load the Module, Make the Request, and Verify Cleanup

Create spec/appSpec.js:

describe('myCtrl HTTP behavior', function() {
  let $httpBackend;
  let $scope;

  beforeEach(angular.mock.module('myApp'));

  beforeEach(angular.mock.inject(function(
    $controller, $rootScope, _$httpBackend_
  ) {
    $httpBackend = _$httpBackend_;
    $scope = $rootScope.$new();
    $controller('myCtrl', { $scope: $scope });
  }));

  afterEach(function() {
    $httpBackend.verifyNoOutstandingExpectation();
    $httpBackend.verifyNoOutstandingRequest();
  });

  it('populates the response with the returned body', function() {
    $httpBackend.expectGET('/api/data')
      .respond(200, 'mock response');

    $scope.getResponse();
    $httpBackend.flush();

    expect($scope.response).toBe('mock response');
    expect($scope.error).toBeNull();
  });
});

angular.mock.module() registers the application module for the test injector and automatically includes ng and ngMock. Loading the application script alone does not register its module with that injector.

Run the fixture:

npx jasmine-browser-runner runSpecs

The mocked body is the string 'mock response', so result.data and $scope.response are that string. If you instead use .respond(200, { data: 'mock response' }), result.data is the object { data: 'mock response' }. AngularJS does not unwrap that nested property; its $http response implementation assigns the response body to data.

Difference Between $httpBackend.expect and $httpBackend.when

Choose expect() when making the request is part of the behavior you are testing. Choose when() when you only need a response available if a request happens. The method-specific helpers, such as expectGET() and whenGET(), use the same respective semantics.

Behaviorexpect / expectGETwhen / whenGET
Request required?Yes; verification fails if it never occursNo
Matching requests per registrationOneReusable
Request orderingExpectations are consumed in declaration orderDoes not require a request sequence
Typical useRequired fetch, save, or deleteOptional request or shared fixture response

The successful test above demonstrates a required call. Add this optional-call counterpart inside the same describe block:

// Fragment: add inside the existing describe block.
it('allows a response definition to remain unused', function() {
  $httpBackend.whenGET('/api/data')
    .respond(200, 'mock response');

  // No request is made, so there is nothing to flush.
  expect($scope.response).toBeNull();
});

The shared verification passes because an unused whenGET() definition is not an outstanding expectation. However, if you do make a request handled by that definition, its response still needs processing.

Expectations take priority over backend definitions. An expectation without its own response can use a matching when() response. A request with no matching expectation or definition fails with an unexpected-request error; the unit-test mock does not fall back to the real server. These behaviors are also exercised in AngularJS's official ngMock tests.

Match Query Parameters, Request Data, and Headers

Match the request contract, not just its endpoint. URLs can be matched with strings, regular expressions, or predicate functions; include the expected query parameters. Request bodies can be matched structurally with an object for JSON data, and header predicates let you check selected headers without matching the entire header collection.

// Fragment: add inside the existing describe block.
it('matches query parameters, JSON data, and a header',
  angular.mock.inject(function($http) {
    let saved;

    $httpBackend.expectPOST(
      '/api/data?page=2',
      { name: 'Example' },
      function(headers) {
        return headers['X-Client'] === 'legacy-test';
      }
    ).respond(201, { id: 7 });

    $http.post('/api/data', { name: 'Example' }, {
      params: { page: 2 },
      headers: { 'X-Client': 'legacy-test' }
    }).then(function(result) {
      saved = result.data;
    });

    $httpBackend.flush();
    expect(saved).toEqual({ id: 7 });
  })
);

AngularJS normally serializes object request bodies as JSON. A header predicate is useful here because $http also supplies default headers. Avoid broad URL patterns that accidentally accept the wrong resource or missing query parameters.

Flush Responses and Verify Outstanding Work

$httpBackend.flush() delivers queued mocked responses at a point you control, allowing the AngularJS promise callbacks to run before your assertions. It does not make a real network request synchronous. The ordinary success test needs neither Jasmine's done callback nor a real timer.

Do not call flush() automatically after every test. It throws when there are no pending requests, and delivering responses in cleanup can hide that the test never asserted their effects. No-request tests need no flush; timeout tests can complete through the timeout path instead.

  • verifyNoOutstandingExpectation() detects required requests that were never made.
  • verifyNoOutstandingRequest() detects requests still waiting for mocked responses.

Keep both checks. A request can satisfy its expectation while its response remains unprocessed, so checking expectations alone is insufficient.

Mock HTTP Errors, Network Failures, and Timeouts

HTTP Error Responses

A server error response rejects the $http promise. Add this test inside the existing suite to check the controller's failure state:

// Fragment: add inside the existing describe block.
it('records an HTTP failure', function() {
  $httpBackend.expectGET('/api/data')
    .respond(503, { message: 'Unavailable' });

  $scope.getResponse();
  $httpBackend.flush();

  expect($scope.response).toBeNull();
  expect($scope.error).toEqual({
    status: 503,
    xhrStatus: 'complete'
  });
});

Here, xhrStatus: 'complete' means the transport completed, not that the HTTP status was successful.

Network Failures

For a transport failure, return a response tuple from a callback. Its fifth entry supplies the XMLHttpRequest status; it is not a fifth positional argument to the static respond() overload.

// Fragment: add inside the existing describe block.
it('records a network failure', function() {
  $httpBackend.expectGET('/api/data').respond(function() {
    return [0, null, {}, '', 'error'];
  });

  $scope.getResponse();
  $httpBackend.flush();

  expect($scope.error).toEqual({
    status: 0,
    xhrStatus: 'error'
  });
});

Request Timeouts

To test timeout behavior rather than a manufactured response, configure a request timeout and advance the mock scheduler:

// Fragment: add inside the existing describe block.
it('records a request timeout', angular.mock.inject(function(
  $rootScope, $flushPendingTasks
) {
  $httpBackend.expectGET('/api/data')
    .respond(200, 'too late');

  $scope.getResponse({ timeout: 1000 });
  $rootScope.$digest(); // Start the request and its timeout.
  $flushPendingTasks(1000);
  $rootScope.$digest();

  expect($scope.error).toEqual({
    status: -1,
    xhrStatus: 'timeout'
  });
}));

$flushPendingTasks(1000) advances the mock task queue by 1,000 milliseconds without waiting in real time. The timeout removes the pending response, so this test does not call $httpBackend.flush(); the shared verification still applies.

Common Mistakes and a Practical Checklist

  • Missing registration: load the application file and call angular.mock.module('myApp') before injection.
  • Wrong response shape: assert the body you actually returned, including any nested properties.
  • Overly permissive mocks: use expectations for required calls and narrow matchers for important request fields.
  • Unexpected template requests: account for templateUrl dependencies rather than accepting every GET. See AngularJS template testing with ng-html2js.
  • Missing failure coverage: test application state after server errors, network failures, and timeouts—not merely whether a request matched.
  • CI surprises: commit your dependency lockfile, provision the browser and driver, and keep these unit tests independent of deployed API services.

The repeatable sequence is: load the module, configure the mock, invoke the code, process the intended outcome, assert application behavior, and verify no HTTP work remains. Keep separate integration tests for behavior that requires a real server.

FAQ

What is AngularJS $httpBackend used for?

The ngMock version of $httpBackend replaces AngularJS's HTTP transport during unit tests. It lets you verify requests and supply controlled responses without contacting an API server.

What is the difference between $httpBackend.expect() and $httpBackend.when()?

expect() registers a required request that is checked during verification. when() registers a reusable response definition but does not require a request to occur.

When should I call $httpBackend.flush()?

Call flush() after invoking code that creates a mocked request whose response you need to process. It delivers the response and allows AngularJS promise callbacks to run; tests with no pending request should not call it.

How do I verify that no AngularJS HTTP requests are outstanding?

Call verifyNoOutstandingExpectation() and verifyNoOutstandingRequest() in afterEach. The first detects required requests that never occurred; the second detects requests still awaiting mocked responses.

How do I mock an HTTP error response with $httpBackend?

Configure a response such as expectGET('/api/data').respond(503, { message: 'Unavailable' }), invoke the application code, and call flush(). Then assert the error-handling behavior, because the non-success status rejects the $http promise.

Further Reading

Written by Sam Erickson

Software engineer. I write every article on StackChief myself. Author of Mastering the Tech Interview. More about me