Search code examples
angularjskarma-runnerkarma-jasminehttpbackend

Angular test using $httpBackend fails with "400 thrown" error


For hours I've been trying to test my NewPostController with $httpBackend. The problem is whenever I set non-2xx status code in the response, the test fails.

NewPostController has the following method:

$scope.submit = function () {
  var newPost = $scope.newPost;
    PostService.insertPost(newPost).then(function (post) {
      $location.path("/my-posts");
    }, function (status) {
        $scope.form.$setPristine();
        $scope.error = status;
    });
};

I have a problem testing the failure path:

it(...) {
  ...
  $scope.post.text = "";
  $httpBackend.expectPOST("/create-post", {"post":$scope.post}).respond(400);
  $scope.submit();
  $httpBackend.flush();
  expect($scope.error).toBeDefined();

  $scope.post.text = "This is a valid text.";
  $httpBackend.expectPOST("/create-post", {"post": $scope.post}).respond(200);
  $scope.submit();
  $httpBackend.flush();
  expect($location.path()).toBe("/my-posts");
});

The test fails with a message "400 thrown" (no callstack). I tried to change the order of subtests, use whenPOST instead of expectPOST and combine the methods as they do in Angular docs (https://docs.angularjs.org/api/ngMock/service/$httpBackend) but without success.

Please help.

EDIT:

Now when I look at PostService, it makes sense where the "400 thrown" comes from but I expected the error to be handled by angular. I threw it because of the section "Handling problems in nested service calls" of this article. It is supposed to be a shorter version of deferred.resolve/reject mechanism.

this.insertPost = function (newPost) {
  return $http({
    method: "post",
    url: "/create-post",
    data: {
      post: newPost
    }
  }).then(function (res) {
      return (res.data);
  }, function (res) {
      throw res.status;
  });
};

Solution

  • This is indeed strange, and is perhaps something the angular team didn't consider.

    When a promise is rejected by throwing (as you're doing), the angular $exceptionHandler service is called with the thrown exception. By default, this service just logs the exception in the browser console.

    But when using ngMocks, this service is replaced by a mock implementation that can either log or rethrow the exception. The default mode is to rethrow, in order to make a test fail when an exception is thrown.

    My advice would be to avoid using throw to simply reject a promise, and thus replace

    function (res) {
      throw res.status;
    }
    

    by

    function (res) {
      return $q.reject(res.status);
    }
    

    But if you really want to keep using throw, you can also configure the mock exceptionHandler to log instead of rethrowing:

    beforeEach(module(function($exceptionHandlerProvider) {
      $exceptionHandlerProvider.mode('log');
    }));