Welcome to the Treehouse Community

Want to collaborate on code errors? Have bugs you need feedback on? Looking for an extra set of eyes on your latest project? Get support with fellow developers, designers, and programmers of all backgrounds and skill levels here with the Treehouse Community! While you're at it, check out some resources Treehouse students have shared here.

Looking to learn something new?

Treehouse offers a seven day free trial for new students. Get access to thousands of hours of content and join thousands of Treehouse students and alumni in the community today.

Start your free trial

JavaScript

Mocking service when testing controller in Karma for angularjs

I am working on the Single Page Application with AngularJS project for the 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.

controllers.js:

(function() {
  'use strict';
  angular.module('app')
  .controller('RecipesController', function(dataService,$location) {
    const vm = this;

    vm.init = () => {
      vm.hidden = true;
      dataService.getAllRecipes(function(response) {
        vm.recipes = response.data;
        vm.getCategories(response.data);
      });
    }

    vm.selectCategory = (category) => {
      if (category === null) {
        vm.init();
      } else {
        dataService.getCategory(category,function(response) {
          vm.recipes = response.data;
        });
      }
    };

    vm.getCategories = (data) => {
      let categories = new Set();
      for (let item of data) {
        categories.add(item.category);
      }
      vm.categories = Array.from(categories);
    };

    vm.addRecipe = () => {
      $location.path('/add');
    }

    vm.deleteRecipe = (recipe,$index) => {
      vm.toDelete = recipe.name;
      vm.hidden = false;
      vm.deleteIt = () => {
        vm.hidden = true;
        dataService.deleteRecipe(recipe._id,function(response) {
          vm.init();
        });
      }
    }

    vm.init();
  })
  .controller('RecipeDetailController', function($scope,dataService,$location) {
    const vm = this;
    const init = () => {
      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 => 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) => {
      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) => {
      if (item === 'ingredient') {
        vm.recipe.ingredients.splice($index,1);
      } else if (item === 'step') {
        vm.recipe.steps.splice($index,1);
      }

    }

    vm.saveChanges = (recipe) => {

      vm.errors = [];

      const buildErrorArray = (errorArray) => {
        for (let item of errorArray) {
          vm.errors.push(item.userMessage);
        }
      }

      const collectErrors = (response) => {
        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 = () => {
      $location.path('/');
    }

    init();

  });
}());

services.js:

(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());
    };

  });
}());

controllersSpec.js:

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');
  }));

});

This is the result I get when I run the tests:

  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.<anonymous> (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.<anonymous> (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)