New version Angular 9
Now we should complete our CRUD. But we already have too much functionalities we will take a look at...testing! We will add the Create, Update and Delete in the next step!
We will create a folder test, in which we will add the "test/specRunner.html" file that will contain the runner for Jasmine a Javascript unit test framework. Its content will be the following.
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <title>Jasmine Spec Runner</title> <!-- include Jasmine --> <link href="//cdn.jsdelivr.net/jasmine/1.3.1/jasmine.css" type="text/css" rel="stylesheet"> <script src="//cdn.jsdelivr.net/jasmine/1.3.1/jasmine.js" type="text/javascript"></script> <script src="//cdn.jsdelivr.net/jasmine/1.3.1/jasmine-html.js" type="text/javascript"></script> <!-- include angular and all its dependencies --> <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/angularjs/1.0.8/angular.js"></script> <!-- include angular mocks --> <script src="//code.angularjs.org/1.0.8/angular-mocks.js" type="text/javascript"></script> <!-- include angular extra modules --> <!-- include source files here... --> <!-- include spec files here...--> <script type="text/javascript"> (function() { var jasmineEnv = jasmine.getEnv(); jasmineEnv.updateInterval = 1000; var htmlReporter = new jasmine.HtmlReporter(); jasmineEnv.addReporter(htmlReporter); jasmineEnv.specFilter = function(spec) { return htmlReporter.specFilter(spec); }; var currentWindowOnload = window.onload; window.onload = function() { if (currentWindowOnload) { currentWindowOnload(); } execJasmine(); }; function execJasmine() { jasmineEnv.execute(); } })(); </script> </head> <body> </body> </html>
We should then add to the file (for our project)
If we run the "test/specRunner.html" nothing will appear, because we have not any test, called Specs by Jasmine. We create then the file "test/spec/app.test.js" and whe add "spec/app.test.js" to our test runner in the "specs" section.
Then we add a simple tet for the existance of the EmptyController. These are the steps: * describe "Contacts Application": Create a categorization of tests (we are testing the ContactsApp!) * beforeEach module('ContactsApp'): Initialize the module "ContactsApp" * describe "EmptyController": Create a categorization of tests (we will test the EmptyController) We are telling that we are creating a category "ContactsApp" that, before running the module "ContactsApp" will run The tet "EmptyController". * it 'Should be declared': The test outcome * inject( $contoller): Inject the controller factory * $controller('EmptyController'): Instantiate the controller EmptyController
describe("ContactsApp Services", function() { beforeEach(module('ContactsApp')); describe("listController", function() { it('listController should be declared', inject(function(listController) { expect(listController).not.toBe(null); })); } });
Now running the page will show the tests results!
Mock Object: according to Wikipedia, In object-oriented programming, mock objects are simulated objects that mimic the behavior of real objects in controlled ways. A programmer typically creates a mock object to test the behavior of some other object, in much the same way that a car designer uses a crash test dummy to simulate the dynamic behavior of a human in vehicle impacts
We will create a mock for the repository. We want to test our list service in isolation and we want simply to know if it interacts correctly with the rest of the environment. We will add a "test/lib/mock.js" file in wich we will store the mocks. And we add the reference to the specRunner.
function buildMockRepository(dataToReturn = null,returnsFunction = null){ if(returnsFunction==null){ returnsFunction = function(){ return{ success: function(doCallback){ var data = { result:"ok", data:dataToReturn }; doCallback(data); } }; } } return { calledMethod : "", calledObject : null, getAll : function() { this.calledMethod = "getAll"; this.calledObject = null; return returnsFunction(); }, getById : function(id) { this.calledMethod = "getById"; this.calledObject = id; return returnsFunction(); } } }
The Repository Mock takes two parameters, a callback function to get the data, that will return usally an "ok" message without data, as our api does; and a "dataToReturn" field, that will contain the data eventually sent back (just when we need it to get test consistency.
With this, the two functions that will be mocked store the method called and the parameter passed to the mock.
Now to use the mock we should first initialize the mock, and then a listController to use for all our request, we will add after the "listController should be required" the construction of the mock and the initialization of the test environment.
We create a new scope that will be used for subsequent tests and through the $controller service we will retrieve the 'listController' instance
var repositoryService = buildMockRepository(); var listController; var scope; beforeEach(inject(function($rootScope, $controller) { scope = $rootScope.$new(); listController = $controller('listController',{ $scope:scope, repository:repositoryService, identifier:'test' }); }));
First we verify that the controller actually had been declared
it('listController should be declared',function() { expect(listController).toBeDefined(); });
Now we can use the listController just declared and verify that the simple istantiation would be calling "getAll" and that no parameters had been passed!
it('listController should call the repository getAll',function(){ expect(repositoryService.calledMethod).toBe("getAll"); expect(repositoryService.calledObject).toBe(null); });
Finally we check that the variable "scope.test" exists on the scope.
it('listController should set the result variable on the scope',function(){ expect(scope.test).not.toBe(null); });
Now, to test the detailController we must mock the $modal service, this can be done pretty easily (in this context!! search for this on google and the Challenger Deep opens suddenly..). This mock will be placed too in the mock.js. Note the trick on the mockModalInstance to change the variable inside the instance!!
function buildMockModal(){ var modal = { openParam:null, closeParam:null, thenCalled:false, openCalled:false, closeCalled:false, mockModalInstance : { modal:null, close: function(param){ modal.closeCalled = true; modal.closeParam = param; if(modal.callBack!=null){ modal.thenCalled = true; callback(param); } } }, callBack : null, command: null, open: function(param) { this.openCalled = true; this.openParam = param; return { result: { then: function(callback){ this.callBack = callback; } } } } }; modal.mockModalInstance.modal = modal; return modal; };
This mock signal the calling of the open, close functions and their parameters. Other than this it simulate the "then" javscript promise
Now, we could finally test the detail controller. First we should initialize the test. We are using a repository that returns an object with a specified id. We create too the mocked modal.
var id = 'testId'; var repositoryService = buildMockRepository({ id : id }); var detailController; var mockModal = buildMockModal();
We should now instantiate the detailController, and test for its existence
beforeEach(inject(function($rootScope, $controller) { scope = $rootScope.$new(); detailController = $controller('detailController',{ $scope:scope, $modal:mockModal, repository:repositoryService, identifier:'test' }); })); it('detailController should be declared',function() { expect(detailController).toBeDefined(); });
The only function present will be "open", we then verify that it's defined on the scope, that the repository had been called and that the dialog had been opened with the correct parameters.
it("open function should be added to scope", function() { expect(scope.open).toBeDefined(); }); it('open function should call the repository getById',function(){ scope.open(id); expect(repositoryService.calledMethod).toBe("getById"); expect(repositoryService.calledObject).toBe(id); }); it('open function should call the $modal open',function(){ scope.open(id); expect(mockModal.openCalled).toBe(true); }); it('open function should call the $modal with the correct parameters',function(){ scope.open(id); expect(mockModal.openParam).not.toBe(null); expect(mockModal.openParam.templateUrl).toBe("assets/test/detail.html"); expect(mockModal.openParam.controller).toBe("detailControllerModal"); });
We add a new "test/spec/contact.test.js" to the specRunner. And setup a new block of tests. Plus the one for our repository service. Note the usage of the $httpBackend, this is the wrapper that is used by angular-mocks for the $http service
describe("ContactsApp-Contact Module", function() { beforeEach(module('ContactsApp')); describe("ContactsRepositoryService", function() { var contactsRepositoryService; beforeEach(inject(function(ContactsRepositoryService, _$httpBackend_){ contactsRepositoryService = ContactsRepositoryService; $httpBackend = _$httpBackend_; })); ...
We could then do our verifications on the two methods "getAll" and "getById". The response is added to verify that the correct mocked http request had been called.
it("getAll should make an ajax call to api/contacts/", function () { $httpBackend.whenGET("api/contacts/").respond([{ result: "ok", data :{} }]); expect(contactsRepositoryService.getAll()).toBeDefined(); }); it("getById should make an ajax call to api/contacts/?id=objId", function () { $httpBackend.whenGET("api/contacts/?id=objId").respond([{ result: "ok", data :{id:"objId"} }]); expect(contactsRepositoryService.getById('objId')).toBeDefined(); });