Developers all agree that unit testing is very beneficial in development projects. They help you ensure the quality of your code, thereby ensuring more stable development and greater confidence even when refactoring is needed.
Test-driven development flow chart
AngularJS’s code claim of higher testability is indeed reasonable. Just the end-to-end test examples listed in the document can illustrate this. For projects like AngularJS, although unit testing is said to be easy, it is not easy to do it well. Even though the official documentation provides detailed examples, it is still very challenging in my actual application. Here I will simply demonstrate how I operate it.
Instant Karma
Karma is a test running framework developed by the Angular team for JavaScript. It easily automates testing tasks and replaces tedious manual operations (such as regression test sets or loading target test dependencies). The collaboration between Karma and Angular is like peanut butter and jelly.
You only need to define the configuration file in Karma to start it, and then it will automatically execute test cases in the expected test environment. You can specify the relevant test environment in the configuration file. angular-seed is a solution that I highly recommend and can be implemented quickly. The configuration of Karma in my recent project is as follows:
module.exports = function(config) { config.set({ basePath: '../', files: [ 'app/lib/angular/angular.js', 'app/lib/angular/angular-*.js', 'app/js/**/*.js', 'test/lib/recaptcha/recaptcha_ajax.js', 'test/lib/angular/angular-mocks.js', 'test/unit/**/*.js' ], exclude: [ 'app/lib/angular/angular-loader.js', 'app/lib/angular/*.min.js', 'app/lib/angular/angular-scenario.js' ], autoWatch: true, frameworks: ['jasmine'], browsers: ['PhantomJS'], plugins: [ 'karma-junit-reporter', 'karma-chrome-launcher', 'karma-firefox-launcher', 'karma-jasmine', 'karma-phantomjs-launcher' ], junitReporter: { outputFile: 'test_out/unit.xml', suite: 'unit' } }) }
This is similar to the default configuration of angular-seed but has the following differences:
autoWatch is a really cool setting, it will let Karma automatically revert to your test cases when there are file changes. You can install Karma like this:
npm install karma
angular-seed provides a simple script inscripts/test.sh to trigger Karma tests.
Design test cases with Jasmine
Most resources are already available when designing unit test cases for Angular using Jasmine - a JavaScript testing framework with a behavior-driven development model.
This is what I want to talk about next.
If you want to unit test the AngularJS controller, you can use Angular's dependency injection dependency injection The function imports the service version required by the controller in the test scenario and also checks whether the expected results are correct. For example, I defined this controller to highlight the tab that needs to be navigated to:
app.controller('NavCtrl', function($scope, $location) { $scope.isActive = function(route) { return route === $location.path(); }; })
What would I do if I wanted to test the isActive method? I'll check if the $locationservice variable returns the expected value and the method returns the expected value. Therefore, in our test description, we will define local variables to save the controlled version required during the test and inject it into the corresponding controller when needed. Then in the actual test case we will add assertions to verify whether the actual results are correct. The whole process is as follows:
describe('NavCtrl', function() { var $scope, $location, $rootScope, createController; beforeEach(inject(function($injector) { $location = $injector.get('$location'); $rootScope = $injector.get('$rootScope'); $scope = $rootScope.$new(); var $controller = $injector.get('$controller'); createController = function() { return $controller('NavCtrl', { '$scope': $scope }); }; })); it('should have a method to check if the path is active', function() { var controller = createController(); $location.path('/about'); expect($location.path()).toBe('/about'); expect($scope.isActive('/about')).toBe(true); expect($scope.isActive('/contact')).toBe(false); }); });
Using the entire basic structure, you can design various types of tests. Since our test scenario uses the local environment to call the controller, you can also add some more attributes and then execute a method to clear these attributes, and then verify whether the attributes have been cleared.
$httpBackendIs Cool
So what if you are calling $httpservice to request or send data to the server? Fortunately, Angular provides a
Mock method of $httpBackend. In this way, you can customize the response content of the server, or ensure that the response results of the server are consistent with the expectations in the unit test.
The specific details are as follows:
describe('MainCtrl', function() { var $scope, $rootScope, $httpBackend, $timeout, createController; beforeEach(inject(function($injector) { $timeout = $injector.get('$timeout'); $httpBackend = $injector.get('$httpBackend'); $rootScope = $injector.get('$rootScope'); $scope = $rootScope.$new(); var $controller = $injector.get('$controller'); createController = function() { return $controller('MainCtrl', { '$scope': $scope }); }; })); afterEach(function() { $httpBackend.verifyNoOutstandingExpectation(); $httpBackend.verifyNoOutstandingRequest(); }); it('should run the Test to get the link data from the go backend', function() { var controller = createController(); $scope.urlToScrape = 'success.com'; $httpBackend.expect('GET', '/slurp?urlToScrape=http:%2F%2Fsuccess.com') .respond({ "success": true, "links": ["http://www.google.com", "http://angularjs.org", "http://amazon.com"] }); // have to use $apply to trigger the $digest which will // take care of the HTTP request $scope.$apply(function() { $scope.runTest(); }); expect($scope.parseOriginalUrlStatus).toEqual('calling'); $httpBackend.flush(); expect($scope.retrievedUrls).toEqual(["http://www.google.com", "http://angularjs.org", "http://amazon.com"]); expect($scope.parseOriginalUrlStatus).toEqual('waiting'); expect($scope.doneScrapingOriginalUrl).toEqual(true); }); });
As you can see, beforeEach call is actually very similar. The only difference is that we get $httpBackend from the injector instead of getting it directly. Even so, there are some obvious differences when creating different tests. For starters, there will be an afterEachcall method to ensure that $httpBackend does not have obvious abnormal requests after each use case execution. If you look at the settings of the test scenario and the application of the $httpBackend method, you will find that there are a few things that are not so intuitive.
In fact, the method of calling $httpBackend is simple and clear, but it is not enough - we have to encapsulate the call into the $scope.runTest method in the actual test in the method of passing the value to $scope.$apply. This way the HTTP request can be processed only after $digest is triggered. As you can see, $httpBackend will not be parsed until we call the $httpBackend.flush() method, which ensures that we can verify whether the returned result is correct during the call (in the above example, the controller's The $scope.parseOriginalUrlStatusproperty property will be passed to the caller, so we can monitor it in real time)
The next few lines of code are assertions that detect the $scopethat attribute during the call. Cool right?
Tip: In some unit tests, users are accustomed to marking scopes without $ as variables. This is not mandatory or overemphasized in the Angular documentation, but I use $scopelike in order to improve readability and consistency.
Conclusion
Maybe this is one of those things that just comes naturally to me, but learning to write unit tests in Angular was definitely quite painful for me at first. I found that much of my understanding of how to get started came from a patchwork of various blog posts and resources on the internet, with no really consistent or clear best practices, but rather through random choices that came naturally. I wanted to provide some documentation of what I ended up with to help others who may be struggling because they just want to write code rather than having to understand all the weird and unique features of Angular and Jasmine. usage. So I hope this article can be of some help to you.