资讯专栏INFORMATION COLUMN

AngularJs directive 的单元测试方法

JouyPub / 2641人阅读

摘要:翻译自在这篇文章中,我将详述如何给我们上周开发的做单元测试的过程。单元测试是一种测试你的项目中每个最小单元代码的艺术,是使你的程序思路清晰的基础。

第一次翻译技术文章,肯定很多语句很生疏,有看官的话就见谅,没有的话也没人看的到这句话。。

翻译自:Unit Testing an AngularJS Directive

在这篇文章中,我将详述如何给我们上周开发的stepper directive做单元测试的过程。下周会讲到如何使用Github和Bower进行组件分离。

单元测试是一种测试你的项目中每个最小单元代码的艺术,是使你的程序思路清晰的基础。一旦所有的测试通过,这些零散的单元组合在一起也会运行的很好,因为这些单元的行为已经被独立的验证过了。

单元测试能够避免你的代码出现回归性BUG提高代码的质量和可维护性使你的代码在代码库中是可信赖的,从而提高团队合作的质量,使重构变得简单和快乐: )

单元测试的另一个用处是当你发现了一个新的BUG,你可以为这个BUG写一个单元测试,当你修改了你的代码,使这个测试可以PASS了的时候,就说明这个BUG已经被修复了。

AngularJS最好的小伙伴儿KarmaJS test runner(一个能够在浏览器中运行测试同时生成结果日志的Node.js server)还有 Jasmine(定义了你的测试和断言的语法的库)。我们使用Grunt-karma将karma集成在我们经典且繁重的grunt 工作流中,然后在浏览器中运行测试。这里值得注意的是,karma可以将测试运行在远程的云浏览器中,比如SauceLabs和BrowserStack。

AngularJS是将是经过了严密地测试的,所以赶紧给自己点个赞,现在就开始写测试吧!

术语:

在我们进行下一步之前有一些术语需要说明:

spec: 你想要测试的代码的说明,包括一个或多个测试条件。spec应该覆盖所有预期行为。

test suite: 一组测试的集合,定义在Jasmine提供的describe语句块中,语句块是可以嵌套的。

test: 测试说明,写在Jasmin提供的it语句块中,以一个或者多个期望值结束(译者按:也就是说,一个it语句块中,一定要有一个以上的期望值)。

actual: 在你的期望中要被测试的值。

expected value: 针对测试出的真实值做比较的期望值。(原文:this is the value you test the actual value against.)

matcher: 一个返回值为Boolean类型的函数,用于比较真实值跟期望值。结果返回给jasmine,比如toEqual,toBeGreatherThan,toHaveBeenCalledWith... 你也可以定义你自己的matcher。

expectation: 使用expect函数测试一个值,得到它的返回值,expectation是与一个得到期望值的matcher函数链接的。(原文:Use the expect function to test a value, called the actual. It is chained with a matcher function, which takes the expected value.)

mock: 一种「stubbed」(不会翻译)服务,你可以制造一些假数据或方法来替代程序真正运行时所产生的数据。

这有一个spec文件的例子:


// a test suite (group of tests) //一组测试 describe("sample component test", function() { // a single test //多带带的测试 it("ensure addition is correct", function() { // sample expectation // 简单的期望 expect(1+1).toEqual(2); // `--- the expected value (2) 期望值是2 // `--- the matcher method (equality) toEqual方法就是matcher函数 // `-- the actual value (2) 真实值是2 }); // another test // 另一个测试 it("ensure substraction is correct", function() { expect(1-1).toEqual(0); }); });
测试环境搭建

将grunt-karma添加到你项目的依赖中

npm install grunt-karma --save -dev

创建一个karma-unit.js文件

这里是一个karma-unit文件的例子
这个文件定义了如下内容:
* 将要被加载到浏览器进行测试的JS文件。通常情况下,不仅项目用的库和项目本身的文件需要包含在内,你所要测试的文件和mock文件也要在这里加载。
* 你想将测试运行在哪款浏览器中。
* 怎样接收到测试结果,是命令行里还是在浏览器中...?
* 可选插件。

以下是files这一项的例子:

files: [
  "http://code.angularjs.org/1.2.1/angular.js",       <-- angular sourc
  "http://code.angularjs.org/1.2.1/angular-mocks.js", <-- angular mocks & test utils
  "src/angular-stepper.js",                           <-- our component source code
  "src/angular-stepper.spec.js"                       <-- our component test suite
]

注:这里可以添加jquery在里面,如果你需要它帮助你编写测试代码(更强大的选择器,CSS测试,尺寸计算…)

将karma grunt tasks添加到Gruntfile.js中

karma: {
    unit: {
        configFile: "karma-unit.js",
        // run karma in the background
        background: true,
        // which browsers to run the tests on
        browsers: ["Chrome", "Firefox"]
    }
}

然后创建 angular-stepper.spec.js文件,将上面写的简单的测试代码粘贴进来。这时你就可以轻松运行grunt karma任务去观察你的测试在浏览器中运行并且在命令行中生成测试报告。

....
Chrome 33.0.1712 (Mac OS X 10.9.0): Executed 2 of 2 SUCCESS (1.65 secs / 0.004 secs)
Firefox 25.0.0 (Mac OS X 10.9): Executed 2 of 2 SUCCESS (2.085 secs / 0.006 secs)
TOTAL: 4 SUCCESS

上面有四个点,每个点都代表一个成功的测试,这时你可以看到,两个测试分别运行在我们配置的两个浏览器中了。
哦也~

那么接下来,让我们写一些真正的测试代码吧: )

给directive编写单元测试

为我们的组件所编写的一组单元测试,又叫做spec的东西,不仅应该覆盖我们所要测试的组件的所有预期行为,还要将边缘情况覆盖到(比如不合法的输入、服务器的异常状况)。

下面展示的angular-stepper组件的测试集的精华部分,完整版点这里。我们对这样一个组件的测试非常简单,不需要假数据。唯一比较有技巧性的是,我们将我们的directive包含在了一个form表单下,这样能够在使用ngModelController和更新表单验证正确性的情况下正确的运行测试。(注:此处的内容需要读angular-stepper那个组件的文件才能懂为何要将directive包含在form表单中,如果不想深入了解,可以忽略这句。原文:The only tricky thing is that we wrap our directive inside a form to be able to test that it plays well with ngModelController and updates form validity correctly.)


// the describe keyword is used to define a test suite (group of tests) describe("rnStepper directive", function() { // we declare some global vars to be used in the tests var elm, // our directive jqLite element scope; // the scope where our directive is inserted // load the modules we want to test 在跑测试之前将你要测试的模块引入进来 beforeEach(module("revolunet.stepper")); // before each test, creates a new fresh scope // the inject function interest is to make use of the angularJS // dependency injection to get some other services in our test inject方法的作用是利用angularJS的依赖注入将我们所需要的服务注入进去 // here we need $rootScope to create a new scope 需要用$rootScope新建一个scope beforeEach(inject(function($rootScope, $compile) { scope = $rootScope.$new(); scope.testModel = 42; })); function compileDirective(tpl) { // function to compile a fresh directive with the given template, or a default one // compile the tpl with the $rootScope created above // wrap our directive inside a form to be able to test // that our form integration works well (via ngModelController) // our directive instance is then put in the global "elm" variable for further tests if (!tpl) tpl = "
"; tpl = "
" + tpl + "
"; //原文最后一个标签是感觉是笔误。 // inject allows you to use AngularJS dependency injection // to retrieve and use other services inject(function($compile) { var form = $compile(tpl)(scope); elm = form.find("div"); }); // $digest is necessary to finalize the directive generation //$digest 方法对于生成指令是必要的。 scope.$digest(); } describe("initialisation", function() { // before each test in this block, generates a fresh directive beforeEach(function() { compileDirective(); }); // a single test example, check the produced DOM it("should produce 2 buttons and a div", function() { expect(elm.find("button").length).toEqual(2); expect(elm.find("div").length).toEqual(1); }); it("should check validity on init", function() { expect(scope.form.$valid).toBeTruthy(); }); }); it("should update form validity initialy", function() { // test with a min attribute that is out of bounds // first set the min value scope.testMin = 45; // then produce our directive using it compileDirective("
"); // this should impact the form validity expect(scope.form.$valid).toBeFalsy(); }); it("decrease button should be disabled when min reached", function() { // test the initial button status compileDirective("
"); expect(elm.find("button").attr("disabled")).not.toBeDefined(); // update the scope model value scope.testModel = 40; // force model change propagation scope.$digest(); // validate it has updated the button status expect(elm.find("button").attr("disabled")).toEqual("disabled"); }); // and many others... });

一些需要注意的点:

在要被测试的scope中,一个directive需要被compiled(译者注:也就是上面代码中的$compile(tpl)(scope);这句话在做的事情)。
一个非隔离scope可以通过element.scope()方法访问到。
一个隔离的scope可以通过element.isolateScope()方法访问到。

为啥我在改变一个Model的值的时候需要调用scope.$digest()方法?

在一个真正的angular应用中,$digest方法是angular通过各种事件(click,inputs,requests...)的反应自动调用的。自动化测试不是以真实的用户事件为基础的,所以我们需要手动的调用$digest方法($digest方法负责更新所有数据绑定)。

额外福利 #1: 实时测试

多亏了grunt,当我们的文件改动的时候,可以自动的进行测试。

如果你想在你的代码有任何改动的时候都进行一次测试,只要将一段代码加入到grunt的watch任务中就行。

js: {
    files: ["src/*.js"],
    tasks: ["karma:unit:run", "build"]
},

你也可以将grunt的默认任务设置成这样:

grunt.registerTask("default", ["karma:unit", "connect", "watch"]);

设置完后,运行grunt,就可以实时的在内置的server中跑测试了。

额外福利 #2:添加测试覆盖率报告

作为开发者,我们希望以靠谱的数据作为依据,我们也希望持续的改进自己的代码。"coverage"指的是你的测试代码的覆盖率,它可以提供给你一些指标和详细的信息,无痛的增加你的代码的覆盖率。

下面是一个简易的覆盖率报告:

我们可以详细的看到每个文件夹的每个文件的代码是否被测试覆盖。归功于grunt+karma的集成,这个报告是实时更新的。我们可以在每一个文件中一行一行的检查那一块带按摩没有被测试。这样能使测试变得更加的简单。

100% test coverage 不代表你的代码就没有BUG了,但它代表这代码质量的提高!

karma+grunt的集成特别的简单,karma有一套「插件」系统,它允许我们通过配置karma-unit.js文件来外挂fantastic Istanbul 代码覆盖率检测工具。只要配置一下文件,妈妈就再也不用担心我的代码覆盖率了。

Add coverage to karma
# add the necessary node_modules
npm install karma-coverage --save-dev

现在将新的设置更新到kamar的配置文件中

// here we specify which of the files we want to appear in the coverage report
preprocessors: {
    "src/angular-stepper.js": ["coverage"]
},
// add the coverage plugin
plugins: [ "karma-jasmine", "karma-firefox-launcher", "karma-chrome-launcher", "karma-coverage"],
// add coverage to reporters
reporters: ["dots", "coverage"],
// tell karma how you want the coverage results
coverageReporter: {
  type : "html",
  // where to store the report
  dir : "coverage/"
}

更多覆盖率的设置请看这里:https://github.com/karma-runner/karma-coverage

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/78078.html

相关文章

  • AngularJS技术使用经验-达观数据

    摘要:达观数据陈高星本文主要涵盖模型,和数据双向绑定模块路由过滤器和自定义过滤器服务和自定义服务,指令和自定义指令依赖注入继承。负责响应于用户输入并执行交互数据模型对象。七指令和自定义指令指令使我们用来扩展浏览器能力的技术之一。表示提取整个元素。 AngularJs诞生于2009年,最初由Misko Hevery和Adam Abrons开发,后来成为Google的项目。AngularJS 是...

    lansheng228 评论0 收藏0
  • angular 1.x多项目共享子项目实践之路

    摘要:可发布这一部分会在下一章管理对子项目引用中详细说明。总结本文总结了多项目共享子项目工程化方面的一些实践,并不涉及到复杂的代码,主要涉及到的概念,使用进行包管理,使用作为自动化工具等工程化的知识。 背景 公司的产品线涵盖多个产品,这些产品中会有一些相同的功能,如登录,认证等,为了保持这些功能在各个产品中的一致性,我们在各个产品中维护一份相同的代码。这带来了很大的不便:当出现新的需求时,不...

    mist14 评论0 收藏0
  • angular2.0 笔记 - 01

    angular2.0 学习笔记 ### 1.angular-cli 常用命令记录 详细教程 angular cli官网 有,这里不详细说明,感兴趣的可以自行到官网看,一下仅记录本人到学习过程常用到的命令 1.创建项目 ng new ng new project-name exp: ng new my-app 2.启动项目 ng serve 参数名 类型 默认值 作用 exp ...

    AnthonyHan 评论0 收藏0
  • Angularjs学习笔记指令

    摘要:自定义指令中有很多内置指令,一般都是以开头的比如等等。本文介绍的自定义指令的用法。该参数的意思是替换指令的内容,更改上面的例子。将属性绑定到父控制器的域中学习概念多指令中的参数中增加了的值和的点击函数。 自定义指令 angularjs中有很多内置指令,一般都是以ng开头的;比如:ng-app,ng-click,ng-repeat等等。本文介绍angularjs的自定义指令的用法。 指令...

    LeexMuller 评论0 收藏0
  • Angularjs学习笔记指令

    摘要:自定义指令中有很多内置指令,一般都是以开头的比如等等。本文介绍的自定义指令的用法。该参数的意思是替换指令的内容,更改上面的例子。将属性绑定到父控制器的域中学习概念多指令中的参数中增加了的值和的点击函数。 自定义指令 angularjs中有很多内置指令,一般都是以ng开头的;比如:ng-app,ng-click,ng-repeat等等。本文介绍angularjs的自定义指令的用法。 指令...

    Cristic 评论0 收藏0

发表评论

0条评论

JouyPub

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<