2017-01-08 4 views
0

Ich arbeite an der Single-Page-Anwendung mit AngularJS-Projekt für den Treehouse Full Stack JavaScript TechDegree und ich versuche Unit-Tests auf den Controllern zu tun. Um die Controller zu testen, die API-Aufrufe an den DataService machen, muss ich den DataService überspielen und ich kann nicht herausfinden, wie man das richtig macht. Ich habe Artikel für Artikel über die Winkelprüfung gelesen und bin so verloren, dass ich keine Ahnung habe, was ich als nächstes tun soll.Mocking-Service beim Testen des Controllers in Karma für angularjs

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

}); 

Dies ist das Ergebnis, das ich erhalte, wenn ich die Tests ausführen:

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) 

EDIT

ich beschlossen, den Dienst zu ändern, um ein Versprechen anstelle eines Rückruf zurückzukehren:

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

Dann änderte ich die entsprechende Funktion in der Steuerung:

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

aber Ich bekomme immer noch

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

Werde ich das Versprechen nicht korrekt umsetzen? Gibt es noch etwas in meinem Test, dass ich vermisse?

Antwort

0

Sie mischen derzeit Rückrufe und Versprechen.

Die Methode getAllRecipes in der realen Service-Implementierung nimmt einen Rückruf als Argument und führt ihn aus, wenn der interne Ajax-Aufruf erfolgt ist. Der Konsument der Methode hat keine Ahnung, dass die Implementierung Versprechungen intern verwendet.

Die Pseudo-Implementierung von getAllRecipes nimmt jedoch keine Callback-Funktion oder verwendet sie, sondern gibt stattdessen ein Versprechen zurück.

Sie in Ihrem Controller haben:

dataService.getAllRecipes(function(response) { 

    vm.recipes = response.data; 
    vm.getCategories(response.data); 
}); 

Aber mit der Mock Implementierung würden Sie brauchen:

dataService.getAllRecipes(function(response) { 

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

    // Code 
}); 

Mit Ihrer aktuellen Implementierung von getAllRecipes Ihre Mock könnte wie folgt aussehen:

getAllRecipesMock = { 
    getAllRecipes: function(callback) { 

    var response = { 
     data: [{ 
     name: "recipename" 
     }] 
    }; 

    callback(response); 
    } 
}; 

Beachten Sie auch, dass, wenn Sie als Referenz equ vergleichen möchten nalität, verwendet toEqual statt toBe:

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

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

Eine Alternative ist es, die Service-Implementierung zu ändern, um ein Versprechen statt mit Rückrufen zurückzukehren.

+0

Ich habe die Frage mit einigen Änderungen aktualisiert, aber ich bekomme immer noch den undefinierten Fehler. – SmellydogCoding

+0

@SmellydogCoding Bitte aktualisieren Sie nicht und ändern Sie die Frage, nachdem sie beantwortet und gelöst wurde :) Es macht mir nichts aus zu helfen. Scheint zu arbeiten: http://plnkr.co/edit/UnloQivYZmOJ3WgZanye?p=preview – tasseKATT

+0

Vielen Dank für Ihre Hilfe! Ich wollte nicht unhöflich sein, indem ich die Frage ändere, nachdem du sie beantwortet hast. Nachdem ich etwas über JavaScript-Versprechungen gelesen hatte, entschied ich mich, Ihren Rat zu befolgen und die Service-Implementierung zu ändern, um ein Versprechen anstelle eines Rückrufs zurückzugeben, weil das für mich mehr Sinn ergab. Ich versuche immer noch, mich daran zu erinnern, warum die Lösung funktioniert, und um die Dienste im Allgemeinen zu verspotten. Ich bin froh, dass es funktioniert hat. Danke noch einmal. – SmellydogCoding

Verwandte Themen