TDD
TDD的好处:
npm install karma --save-devnpm install karma-junit-reporter --save-dev然后一个典型的运行框架通常都需要一个配置文件,在karma里可以是一个karma.conf.js,里面的代码是一个nodejs风格的,一个普通的例子如下:
module.exports = function(config){ config.set({ // 下面files里的基础目录 basePath : "../", // 测试环境需要加载的JS信息 files : [ "app/bower_components/angular/angular.js", "app/bower_components/angular-route/angular-route.js", "app/bower_components/angular-mocks/angular-mocks.js", "app/js/**/*.js", "test/unit/**/*.js" ], // 是否自动监听上面文件的改变自动运行测试 autoWatch : true, // 应用的测试框架 frameworks: ["jasmine"], // 用什么环境测试代码,这里是chrome` browsers : ["Chrome"], // 用到的插件,比如chrome浏览器与jasmine插件 plugins : ["karma-chrome-launcher","karma-firefox-launcher","karma-jasmine","karma-junit-reporter"], // 测试内容的输出以及导出用的模块名 reporters: ["progress", "junit"], // 设置输出测试内容文件的信息 junitReporter : { outputFile: "test_out/unit.xml", suite: "unit" } });};运行时输入:
karma start test/karma.conf.jsjasmine
describe("A spec (with setup and tear-down)", function() { var foo; beforeEach(function() { foo = 0; foo += 1; }); afterEach(function() { foo = 0; }); it("is just a function, so it can contain any code", function() { expect(foo).toEqual(1); }); it("can have more than one expectation", function() { expect(foo).toEqual(1); expect(true).toEqual(true); });});
angular.mock.module: module
用来加载已有的模块,以及配置inject方法注入的模块信息。具体使用如下:beforeEach(module("myApp.filters"));beforeEach(module(function($provide) { $provide.value("version", "TEST_VER");}));该方法一般在beforeEach中使用,在执行测试用例之前可以获得模块的配置。
angular.mock.inject: inject
用来注入配置好的ng模块,来供测试用例里进行调用。具体使用如下:it("should provide a version", inject(function(mode, version) { expect(version).toEqual("v1.0.1"); expect(mode).toEqual("app"); }));其实inject里面就是利用angular.inject方法创建的一个内置的依赖注入实例,然后里面的模块和普通的ng模块的依赖处理是一样的。
createNote()
函数会被调用。下面是app.js的代码部分。var todoApp = angular.module("todoApp",[]);todoApp.controller("TodoController",function($scope,notesFactory){ $scope.notes = notesFactory.get(); $scope.createNote = function(){ notesFactory.put($scope.note); $scope.note=""; $scope.notes = notesFactory.get(); }});todoApp.factory("notesFactory",function(){ return { put: function(note){localStorage.setItem("todo" + (Object.keys(localStorage).length + 1), note); }, get: function(){ var notes = []; var keys = Object.keys(localStorage); for(var i = 0; i < keys.length; i++){notes.push(localStorage.getItem(keys[i])); } return notes; }};});在todoController中用了个叫做notesFactory的服务来存储和提取笔记。当createNote()被调用时,会使用这个服务将一条信息存入LocalStorage中,然后清空当前的note。因此,在编写测试模块是,应该保证控制器初始化,scope中有一定数量的笔记,在调用
createNote()
之后,笔记的数量应该加一。describe("TodoController Test", function() { beforeEach(module("todoApp")); // 将会在所有的it()之前运行 // 我们在这里不需要真正的factory。因此我们使用一个假的factory。 var mockService = { notes: ["note1", "note2"], //仅仅初始化两个项目 get: function() { return this.notes; }, put: function(content) { this.notes.push(content); } }; // 现在是真正的东西,测试spec it("should return notes array with two elements initially and then add one", inject(function($rootScope, $controller) { //注入依赖项目 var scope = $rootScope.$new(); // 在创建控制器的时候,我们也要注入依赖项目 var ctrl = $controller("TodoController", {$scope: scope, notesFactory:mockService}); // 初始化的技术应该是2 expect(scope.notes.length).toBe(2); // 输入一个新项目 scope.note = "test3"; // now run the function that adds a new note (the result of hitting the button in HTML) // 现在运行这个函数,它将会增加一个新的笔记项目 scope.createNote(); // 期待现在的笔记数目是3 expect(scope.notes.length).toBe(3); }) );});在beforeEach中,每一个测试用例被执行之前,都需要加载模块
module("todoApp")
。get()
和put()
。这个假的factory从数组中加载数据代替localStorage的操作。$rootScope
和$controller
,都可以由Angular自动注入,其中$rootScope
用来获得根作用域,$controller
用作创建新的控制器。$controller
服务需要两个参数。第一个参数是将要创建的控制器的名称。第二个参数是一个代表控制器依赖项目的对象,$rootScope.$new()
方法将会返回一个新的作用域,它用来注入控制器。同时我们传入mockService作为假factory。createNote()
函数之后,会改变数组的长度,因此可以写出两个测试用例。describe("notesFactory tests", function() { var factory; // 在所有it()函数之前运行 beforeEach(function() { // 载入模块 module("todoApp"); // 注入你的factory服务 inject(function(notesFactory) { factory = notesFactory; }); var store = { todo1: "test1", todo2: "test2", todo3: "test3" }; spyOn(localStorage, "getItem").andCallFake(function(key) { return store[key]; }); spyOn(localStorage, "setItem").andCallFake(function(key, value) { return store[key] = value + ""; }); spyOn(localStorage, "clear").andCallFake(function() { store = {}; }); spyOn(Object, "keys").andCallFake(function(value) { var keys=[]; for(var key in store) { keys.push(key); } return keys; }); }); // 检查是否有我们想要的函数 it("should have a get function", function() { expect(angular.isFunction(factory.get)).toBe(true); expect(angular.isFunction(factory.put)).toBe(true); }); // 检查是否返回3条记录 it("should return three todo notes initially", function() { var result = factory.get(); expect(result.length).toBe(3); }); // 检查是否添加了一条新纪录 it("should return four todo notes after adding one more", function() { factory.put("Angular is awesome"); var result = factory.get(); expect(result.length).toBe(4); });});在TodoController模块中,实际上的factory会调用localStorage来存储和提取笔记的项目,但由于我们单元测试中,不需要依赖外部服务去获取和存储数据,因此我们要对
localStorage.getItem()
和localStorage.setItem()
进行spy操作,也就是利用假函数来代替这两个部分。spyOn(localStorage,"setItem")andCallFake()
是用来用假函数进行监听的。第一个参数指定需要监听的对象,第二个参数指定需要监听的函数,然后andCallfake这个API可以编写自己的函数。因此,测试中完成了对localStorage和Object的改写,使函数可以返回我们自己数组中的值。get()
和put()
这两个方法,,然后进行factory.put()
操作后断言笔记的数量。todoApp.filter("truncate",function(){ return function(input,length){ return (input.length > length ? input.substring(0,length) : input); }});所以在单元测试中,可以根据传入字符串的情况断言生成子串的长度。
describe("filter test",function(){ beforeEach(module("todoApp")); it("should truncate the input to 1o characters",inject(function(truncateFilter){ expect(truncateFilter("abcdefghijkl",10).length).toBe(10); }); );});之前已经对断言进行讨论了,值得注意的一点是我们需要在调用过滤器的时候在名称后面加入Filter,然后正常调用即可。
todoApp.directive("customColor", function() { return { restrict: "A", link: function(scope, elem, attrs) { elem.css({"background-color": attrs.customColor}); } };});由于指令必须编译之后才能生成相关的模板,因此我们要引入
$compile
服务来完成实际的编译,然后再测试我们想要进行测试的元素。angular.element()
会创建一个jqLite元素,然后我们将其编译到一个新生成的自作用域中,就可以被测试了。具体测试用例如下:describe("directive tests",function(){ beforeEach(module("todoApp")); it("should set background to rgb(128, 128, 128)", inject(function($compile,$rootScope) { scope = $rootScope.$new(); // 获得一个元素 elem = angular.element("<span custom-color="rgb(128, 128, 128)">sample</span>"); // 创建一个新的自作用域 scope = $rootScope.$new(); // 最后编译HTML $compile(elem)(scope); // 希望元素的背景色和我们所想的一样 expect(elem.css("background-color")).toEqual("rgb(128, 128, 128)"); }) );});开始端到端测试
describe("my app", function() { beforeEach(function() { browser().navigateTo("../../app/notes.html"); }); var oldCount = -1; it("entering note and performing click", function() { element("ul").query(function($el, done) { oldCount = $el.children().length; done(); }); input("note").enter("test data"); element("button").query(function($el, done) { $el.click(); done(); }); }); it("should add one more element now", function() { expect(repeater("ul li").count()).toBe(oldCount + 1); }); });我们在端到端测试过程中,首先导航到我们的主html页面app/notes.html,可以通过
browser.navigateTo()
来完成,element.query()
函数选择了ul元素并记录其中有多少个初始化的项目,存放在oldCount变量中。input("note").enter()
来键入一个新的笔记,然后模拟一下点击操作来检查是否增加了一个新的笔记(li元素)。然后通过断言可以将新旧的笔记数进行对比。