title: Mocking service when testing controller in Karma for angularjs
body: <p>I am working on the Single Page Application with AngularJS project for the Treehouse Full Stack JavaScript TechDegree and I am trying to do unit tests on the controllers.  To test the controllers that make api calls to the dataService I have to mock the dataService and I can not figure out how to do this correctly.  I have read article after article on unit testing angular and I am so lost that I have no idea what to do next.</p>
  
  <p>controllers.js:</p>
  
  <pre><code>(function() {
    'use strict';
    angular.module('app')
    .controller('RecipesController', function(dataService,$location) {
      const vm = this;
  
      vm.init = () =&gt; {
        vm.hidden = true;
        dataService.getAllRecipes(function(response) {
          vm.recipes = response.data;
          vm.getCategories(response.data);
        });
      }
  
      vm.selectCategory = (category) =&gt; {
        if (category === null) {
          vm.init();
        } else {
          dataService.getCategory(category,function(response) {
            vm.recipes = response.data;
          });
        }
      };
  
      vm.getCategories = (data) =&gt; {
        let categories = new Set();
        for (let item of data) {
          categories.add(item.category);
        }
        vm.categories = Array.from(categories);
      };
  
      vm.addRecipe = () =&gt; {
        $location.path('/add');
      }
  
      vm.deleteRecipe = (recipe,$index) =&gt; {
        vm.toDelete = recipe.name;
        vm.hidden = false;
        vm.deleteIt = () =&gt; {
          vm.hidden = true;
          dataService.deleteRecipe(recipe._id,function(response) {
            vm.init();
          });
        }
      }
  
      vm.init();
    })
    .controller('RecipeDetailController', function($scope,dataService,$location) {
      const vm = this;
      const init = () =&gt; {
        const path = $location.path();
        if (path.includes("edit")) {
          let id = path.slice(6);
          dataService.getID(id,function(response) {
            vm.recipe = response.data;
            vm.title = response.data.name;
            vm.editCategory = response.data.category;
          });
        } else if (path.includes("add")) {
          vm.recipe = {
            name: "",
            description: "",
            category: "",
            prepTime: 0,
            cookTime: 0,
            ingredients: [
              {
                foodItem: "",
                condition: "",
                amount: ""
              }
            ],
            steps: [
              {
                description: ""
              }
            ]
          }
          vm.title = 'Add New Recipe.'
        }
  
        dataService.getAllCategories(function (response) {
          vm.categories = response.data;
          let index = response.data.findIndex(item =&gt; item.name === $scope.editCategory);
          if (index === -1) {
            vm.initial = {"name": "Choose a Category"};
          } else {
            vm.initial = $scope.categories[index];
          }
        });
  
        dataService.getAllFoodItems(function (response) {
          vm.foods = response.data;
        });
      }
  
      vm.addItem = (item) =&gt; {
        if (item === 'ingredient') {
          vm.recipe.ingredients.push({amount: "amount", condition: "condition", foodItem: ""});
        } else if (item === 'step') {
          vm.recipe.steps.push({description: "description"});
        }
      };
  
      vm.deleteItem = (item,$index) =&gt; {
        if (item === 'ingredient') {
          vm.recipe.ingredients.splice($index,1);
        } else if (item === 'step') {
          vm.recipe.steps.splice($index,1);
        }
  
      }
  
      vm.saveChanges = (recipe) =&gt; {
  
        vm.errors = [];
  
        const buildErrorArray = (errorArray) =&gt; {
          for (let item of errorArray) {
            vm.errors.push(item.userMessage);
          }
        }
  
        const collectErrors = (response) =&gt; {
          if (response.data.errors.category) { buildErrorArray(response.data.errors.category) }
          if (response.data.errors.ingredients) { buildErrorArray(response.data.errors.ingredients) }
          if (response.data.errors.name) { buildErrorArray(response.data.errors.name) }
          if (response.data.errors.steps) { buildErrorArray(response.data.errors.steps) }
        }
  
        if (recipe._id) {
          dataService.updateID(recipe,function(response) {
            $location.path('/');
            }, function(response) {
              collectErrors(response)
          });
        } else {
          dataService.addRecipe(recipe,function(response) {
            $location.path('/');
            }, function(response) {
              collectErrors(response)
          });
        }
  
      }
  
      vm.cancelChanges = () =&gt; {
        $location.path('/');
      }
  
      init();
  
    });
  }());
  </code></pre>
  
  <p>services.js:</p>
  
  <pre><code>(function() {
    'use strict';
    angular.module('app')
    .service('dataService', function($http,errors,httpErrors) {
  
      this.getAllRecipes = function (callback) {
        $http.get('http://localhost:5000/api/recipes')
        .then(callback,httpErrors.display('HTTP Error'))
        .catch(errors.catch());
      };
  
      this.getAllCategories = function (callback) {
        $http.get('http://localhost:5000/api/categories')
        .then(callback,httpErrors.display('HTTP Error'))
        .catch(errors.catch());
      };
  
      this.getAllFoodItems = function (callback) {
        $http.get('http://localhost:5000/api/fooditems')
        .then(callback,httpErrors.display('HTTP Error'))
        .catch(errors.catch());
      };
  
      this.getCategory = function(category,callback) {
        $http.get('http://localhost:5000/api/recipes?category=' + category)
        .then(callback,httpErrors.display('HTTP Error'))
        .catch(errors.catch());
      };
  
      this.getID = function (id,callback) {
        $http.get('http://localhost:5000/api/recipes/' + id)
        .then(callback,httpErrors.display('HTTP Error'))
        .catch(errors.catch());
      };
  
      this.updateID = function (data,success,error) {
        $http.put('http://localhost:5000/api/recipes/' + data._id, data)
        .then(success,error).catch(errors.catch());
      };
  
      this.addRecipe = function (data,success,error) {
        $http.post('http://localhost:5000/api/recipes', data)
        .then(success,error).catch(errors.catch());
      };
  
      this.deleteRecipe = function (id,callback) {
        $http.delete('http://localhost:5000/api/recipes/' + id)
        .then(callback,httpErrors.display('HTTP Error'))
        .catch(errors.catch());
      };
  
    });
  }());
  </code></pre>
  
  <p>controllersSpec.js:</p>
  
  <pre><code>describe("Unit Testing Controllers", function() {
  
    beforeEach(angular.mock.module('app'));
  
    let $scope;
    let getAllRecipesMock;
  
    beforeEach(inject(function(_$controller_,_$rootScope_,$q) {
      $controller = _$controller_;
      $scope = _$rootScope_.$new();
  
      getAllRecipesMock = {
        getAllRecipes: function() {
          var deferred = $q.defer();
          deferred.resolve([{name: "recipename"}]);
          return deferred.promise;
        }            
      }
    }));
  
    it('has a test to test that tests are testing', function() {
      expect(2 + 2).toEqual(4);
    });
  
    it('should have a RecipesController', function() {
      const controller = $controller('RecipesController',{$scope:$scope});
      expect(controller).toBeDefined();
    });
  
    it('should have a RecipeDetailController', function() {
      const controller = $controller('RecipeDetailController',{$scope:$scope});
      expect(controller).toBeDefined();
    });
  
    it('should call the getAllRecipes service and return response', inject(function() {
      const controller = $controller('RecipesController',{$scope:$scope,dataService:getAllRecipesMock});
      $scope.$digest();
      expect(controller.recipes).toBe([{name: "recipename"}]);
    }));
  
    it('should remove duplicate categories', function() {
      const controller = $controller('RecipesController',{$scope:$scope});
      let data = [{'category':'dog'},{'category':'cat'},{'category':'horse'},{'category':'dog'},{'category':'cow'}];
      controller.getCategories(data);
      expect(controller.categories).toEqual(['dog','cat','horse','cow']);
    });
  
    it('should take you to the /add route when the addRecipe method is called', inject(function($location) {
      const controller = $controller('RecipesController',{$scope:$scope});
      controller.addRecipe();
      expect($location.path()).toEqual('/add');
    }));
  
  });
  </code></pre>
  
  <p>This is the result I get when I run the tests:</p>
  
  <pre><code> Unit Testing Controllers
      √has a test to test that tests are testing
      √should have a RecipesController
      √should have a RecipeDetailController
      ×should call the getAllRecipes service and return response
          Expected undefined to be [ Object({ name: 'recipename' }) ].
              at Object.&lt;anonymous&gt; (test/controllersSpec.js:38:32)
              at Object.invoke (node_modules/angular/angular.js:4839:19)
              at Object.WorkFn (node_modules/angular-mocks/angular-mocks.js:3155:20)
  
      √should remove duplicate categories
      √should take you to the /add route when the addRecipe method is called
  
  Chrome 55.0.2883 (Windows 10 0.0.0): Executed 6 of 6 (1 FAILED) (0.235 secs / 0.084 secs)
  TOTAL: 1 FAILED, 5 SUCCESS
  
  
  1) should call the getAllRecipes service and return response
       Unit Testing Controllers
       Expected undefined to be [ Object({ name: 'recipename' }) ].
      at Object.&lt;anonymous&gt; (test/controllersSpec.js:38:32)
      at Object.invoke (node_modules/angular/angular.js:4839:19)
      at Object.WorkFn (node_modules/angular-mocks/angular-mocks.js:3155:20)
  </code></pre>
  
  <p><strong>EDIT</strong></p>
  
  <p>I decided to change the service to return a promise instead of a callback:</p>
  
  <pre><code>this.getAllRecipes = function () {
       return $http.get('http://localhost:5000/api/recipes');
      };
  </code></pre>
  
  <p>Then I changed the corresponding function in the controller:</p>
  
  <pre><code>vm.init = () =&gt; {
        vm.hidden = true;
        let allRecipes = dataService.getAllRecipes();
        allRecipes.then(function(response) {
          vm.recipes = response.data;
          vm.getCategories(response.data);
        },httpErrors.display('HTTP Error'))
        .catch(errors.catch());
      }
  </code></pre>
  
  <p>but I'm still getting</p>
  
  <p><code>Expected undefined to be [ Object({ name: 'recipename' }) ].</code></p>
  
  <p>Am I not implementing the promise correctly?  Is there still something in my test that I'm missing?</p>