这里有新鲜出炉的 Javascript 教程,程序狗速度看过来!
Javascript 是一种由 Netscape 的 LiveScript 发展而来的原型化继承的基于对象的动态类型的区分大小写的客户端脚本语言,主要目的是为了解决服务器端语言,比如 Perl,遗留的速度问题,为客户提供更流畅的浏览效果。
这篇文章主要介绍了在 JavaScript 的 AngularJS 库中进行单元测试的方法, 主要针对 AngularJS 中的控制器相关, 需要的朋友可以参考下
开发者们都一致认为单元测试在开发项目中十分有好处。它们帮助你保证代码的质量,从而确保更稳定的研发,即使需要重构时也更有信心。
测试驱动开发流程图
AngularJS 的代码声称其较高的可测性确实是合理的。单单文档中列出端对端的测试实例就能说明。就像 AngularJS 这样的项目虽然都说单元测试很简单但真正做好却不容易。即使官方文档中以提供了详尽的实例,但在我的实际应用中却还是很有挑战。这里我就简单示范一下我是怎么操作的吧.
Instant Karma
Karma 是来 Angular 团队针对 JavaScript 开发的一个测试运行框架。它很方便的实现了自动执行测试任务从而替代了繁琐的手工操作(好比回归测试集或是加载目标测试的依赖关系)Karma 和 Angular 的协作就好比花生酱和果冻.
只需要在 Karma 中定义好配置文件启动它,接下来它就会在预期的测试环境下的自动执行测试用例。你可以在配置文件中制定相关的测试环境。angular-seed, 是我强烈推荐的可以快速实施的方案。在我近期的项目中 Karma 的配置如下:
- 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'
- }
- })
- }
这个跟 angular-seed 的默认配置类似只不过有以下几点不同:
autoWatch 确实是个很酷的设置,它会让 Karma 在有文件更改时自动回归你的测试用例。你可以这样安装 Karma:
- npm install karma
angular-seed 提供了一个简单的脚本 inscripts/test.sh 去触发 Karma 的测试。
用 Jasmine 设计测试用例
当使用 Jasmine---- 一种行为驱动开发模式的 JavaScript 测试框架为 Angular 设计单元测试用例时大部分的资源都已可获取。
这也就是我接下来要说的话题。
如果你要对 AngularJS controller 做单元测试可以利用 Angular 的依赖注入 dependency injection 功能导入测试场景中 controller 需要的服务版本还能同时检查预期的结果是否正确。例如,我定义了这个 controller 去高亮需要导航去的那个页签:
- app.controller('NavCtrl', function($scope, $location) {
- $scope.isActive = function(route) {
- return route === $location.path();
- };
- })
如果想要测试 isActive 方法,我会怎么做呢?我将检查 $locationservice 变量是否返回了预期值,方法返回的是否预期值。因此在我们的测试说明中我们会定义好局部变量保存测试过程中需要的 controlled 版本并在需要时注入到对应的 controller 当中。然后在实际的测试用例中我们会加入断言来验证实际的结果是否正确。整个过程如下:
- 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);
- });
- });
使用整个基本的结构,你就能设计各种类型的测试。由于我们的测试场景使用了本地的环境来调用 controller, 你也可以多加上一些属性接着执行一个方法清除这些属性,然后再验证一下属性到底有没有被清除。
$httpBackendIs Cool
那么要是你在调用 $httpservice 请求或是发送数据到服务端呢?还好,Angular 提供了一种
$httpBackend 的 mock 方法。这样的话,你就能自定义服务端的响应内容,又或是确保服务端的响应结果能和单元测试中的预期保持一致。
具体细节如下:
- 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://success.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);
- });
- });
正如你所见,beforeEach call 其实都很类似,唯一不同的是我们是从 injector 获取 $httpBackend 而并非直接获取。即使如此,创建不同的测试时还会有一些明显的不同之处。对初学者来说,会有一个 afterEachcall 方法来确保 $httpBackend 在每次用例执行后不会有明显的异常请求。如果你观察一下测试场景的设置和 $httpBackend 方法的应用就会会发现有那么几点不是那么直观的。
实际上调用 $httpBackend 的方法也算是简单明了但还不够——我们还得在传值给 $scope.$apply 的方法中把调用封装到实际测试中的 $scope.runTest 方法上。这样在 $digest 被触发后才能处理 HTTP 请求。而如你所见直到我们调用 $httpBackend.flush() 方法后 $httpBackend 才会被解析,这也就保证了我们能在调用过程中去验证返回的结果是否正确(在上面的示例中,controller 的 $scope.parseOriginalUrlStatusproperty 属性将被传递给调用者,我们也因此能实时监控)
接下来的几行代码都是在调用过程中检测 $scopethat 属性的断言。很酷吧?
提示:在某些单元测试中,用户习惯把没有 $ 的范围标记为变量。这个在 Angular 文档中并没有强制要求或是过分强调,只是我在使用中为了提高可读性和一致性才使用 $scopelike 这种方式。
结论
也许这就是我做起来对其他人而言只是自然而然能做到的事情之一,但是学习使用 Angular 编写单元测试一开始对我而言确实是相当痛苦的。我发现自己对如何开始的理解大多来自互联网上各种博客文章和资源的拼拼凑凑,没有真正一致或明确的最佳实践,而是通过自然而然随意的选择。我想针对我最终得到的成果提供一些文档,以帮助那些也许还在坑里面挣扎的其他人,毕竟他们只是想要编写代码而已,而非不得不去了解 Angular 和 Jasmine 中所有的怪异特性和独特用法。因此我希望这篇文章能对你有些许帮助。
来源: http://www.phperz.com/article/17/0421/270727.html