【问题标题】:Mocking service when testing controller in Karma for angularjs在 Karma 中为 angularjs 测试控制器时的模拟服务
【发布时间】:2023-04-01 22:53:01
【问题描述】:

我正在为 Treehouse Full Stack JavaScript TechDegree 开发带有 AngularJS 的单页应用程序项目,并且我正在尝试对控制器进行单元测试。要测试对 dataService 进行 api 调用的控制器,我必须模拟 dataService,但我无法弄清楚如何正确执行此操作。我已经阅读了一篇又一篇关于 Angular 单元测试的文章,但我很迷茫,不知道下一步该做什么。

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

});

这是我运行测试时得到的结果:

 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)

编辑

我决定更改服务以返回承诺而不是回调:

this.getAllRecipes = function () {
     return $http.get('http://localhost:5000/api/recipes');
    };

然后我在控制器中更改了相应的功能:

vm.init = () => {
      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());
    }

但我还是得到了

Expected undefined to be [ Object({ name: 'recipename' }) ].

我没有正确执行承诺吗?我的测试中是否还有一些我遗漏的东西?

【问题讨论】:

    标签: javascript angularjs unit-testing


    【解决方案1】:

    您目前正在混合使用回调和承诺。

    实际服务实现中的方法getAllRecipes 将回调作为参数,并在内部ajax 调用完成时执行。该方法的使用者不知道该实现在内部使用了 Promise。

    getAllRecipes 的模拟实现不采用或使用回调函数,而是返回一个承诺。

    您的控制器中有:

    dataService.getAllRecipes(function(response) {
    
      vm.recipes = response.data;
      vm.getCategories(response.data);
    });
    

    但是你需要模拟实现:

    dataService.getAllRecipes(function(response) {
    
      vm.recipes = response.data;
      vm.getCategories(response.data);
    }).then(function (response) {
    
      // Code
    });
    

    使用您当前的getAllRecipes 实现,您的模拟可能如下所示:

    getAllRecipesMock = {
      getAllRecipes: function(callback) {
    
        var response = {
          data: [{
            name: "recipename"
          }]
        };
    
        callback(response);
      }
    };
    

    还请注意,除非您想比较引用是否相等,否则请使用 toEqual 而不是 toBe

    expect(controller.recipes).toEqual([{
      name: "recipename"
    }]);
    

    演示http://plnkr.co/edit/5BQBt4tTxohXEN0Drq3f?p=preview

    另一种方法是更改​​服务实现以返回承诺而不是使用回调。

    【讨论】:

    • 我对问题进行了一些更改,但我仍然收到未定义的错误。
    • @SmellydogCoding 请不要在问题得到回答和解决后更新和更改问题:) 不过我不介意帮忙。似乎工作:plnkr.co/edit/UnloQivYZmOJ3WgZanye?p=preview
    • 感谢您的所有帮助!我不是故意在你回答问题后改变问题的粗鲁。在阅读了一些关于 JavaScript Promise 的内容后,我决定采纳您的建议并更改服务实现以返回 Promise 而不是回调,因为这对我来说更有意义。我仍在努力思考解决方案为何有效,以及一般的模拟服务。我很高兴它确实有效。再次感谢。
    • @SmellydogCoding 没问题,只是一个友好的建议 :) 如果您需要更多帮助,我不介意提供帮助。我的个人资料中有我的电子邮件是有原因的:)
    猜你喜欢
    • 1970-01-01
    • 2017-06-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-12-23
    • 1970-01-01
    • 2014-11-22
    相关资源
    最近更新 更多