• angularJS测试一 Karma Jasmine Mock


    AngularJS测试
    一 测试工具
    1.NodeJS领域:Jasmine做单元测试,Karma自动化完成单元测试,Grunt启动Karma统一项目管理,Yeoman最后封装成一个项目原型模板,npm做nodejs的包依赖管理,bower做javascript的包依赖管理。Java领域:JUnit做单元测试, Maven自动化单元测试,统一项目管理,构建项目原型模板,包依赖管理。
    Nodejs让组合变得更丰富,却又在加重我们的学习门槛。唉......
    2.Karma
    Karma是一个测试工具,它从头开始构建,免去了设置测试方面的负担,这样我们就可以将主要精力放在构建核心应用逻辑上。
    Karma产生一个浏览器实例(或者多个不同的浏览器实例),针对不同的浏览器实例运行测试,检测在不同浏览器环境下测试是否通过。Karma与浏览器通过socket.io来联系,这能让Karma保持持续通信。因此Karma提供了关于哪些测试正在运行的实时反馈,提供一份适合人类阅读的输出,告诉我们哪些测试通过、哪些失败或者超时。
    Karma测试运行器同时支持单元测试和端到端测试。
    3.Karma安装
    如果你已经安装了NodeJS和npm,就可以通过npm命令来安装karma
    安装命令:npm install -g karma
    测试是否安装成功:karma start
    如果成功安装karma,则可以通过浏览器看到karma界面
    4.karma+jasmine配置
    初始化karma配置文件karma.conf.js:karma init
    安装集成包karma-jasmine:npm install karma-jasmine
    karma安装好后,后面就是具体的jasmine测试了。
    二 jasmine测试
    1.尽管Karma支持多种测试框架,但默认的选项是Jasmine。Jasmine是一个用于测试JavaScript代码的行为驱动开发框架。
    下载发布的安装包:jasmine-standalone-2.2.0.zip
    下载后的目录结构:
    (1)lib:存放了运行测试案例所必须的文件,其内包含jasmine-2.2.0文件夹。可以将不同版本的Jasmine放在lib下,以便使用时切换。
    jasmine.js:整个框架的核心代码。
    jasmine-html.js:用来展示测试结果的js文件。
    boot.js:jasmine框架的的启动脚本。需要注意的是,这个脚本应该放在jasmine.js之后,自己的js测试代码之前加载。
    jasmine.css:用来美化测试结果。
    (2)spec:存放测试脚本。
    PlayerSpec.js:就是针对src文件夹下的Player.js所写的测试用例。
    SpecHelper.js:用来添加自定义的检验规则,如果框架本身提供的规则(诸如toBe,toNotBe等)不适用,就可以额外添加自己的规则(在本文件中添加了自定义的规则toBePlaying)。
    (3)src:存放需要测试的js文件。Jasmine提供了一个Example(Player.js,Song.js)。
    (4)pecRunner.html:运行测试用例的环境。它将上面3个文件夹中一些必要的文件都包含了进来。如果你想将自己的测试添加进来的话,那么就修改相应的路径。
    2.核心概念
    (1)细则套件Suites
    Suite表示一个测试集,以函数describe(string, function)封装。describe函数是Jasmine套件定义的一个全局函数,所以可以在测试中直接调用。
    describe()函数带有两个参数,一个字符串,一个函数。字符串是待建立的细则(spec)套件名称或者描述,函数封装了测试套件。
    可以嵌套这些describe()函数,这样我们可以创建一个测试树来执行那些在测试中设置的不同条件。如:
    describe('Unit test: MainController', function() {
    describe('index method', function() {
    // 细则放这里
    });
    });
    一个Suite(describe)包含多个Specs(it),一个Specs(it)包含多个断言(expect)。
    使用describe()函数把相关的细则分组是个不错的主意。在每个describe()块运行时,这些字符串会沿着细则的名称链接起来。因此,上面这个例子的标题就会变成“Unit test:MainController index method.”然后,这些describe()块的标题就会被追加到细则的标题上。设计这个步骤的目的是让我们以完整句子来阅读细则的,所以把测试命名成可读的英文就很重要了。
    (2)定义一个细则
    Spec表示测试用例,以it(string, function)函数封装。我们通过调用it()函数来定义一个细则。这个函数也是在Jasmine测试套件中定义的全局函数,所以可以从测试中直接调用。
    it()函数带有两个参数:一个字符串,是细则的标题或者描述;一个函数,包含了一个或多个用于测试代码功能的预期。
    这些预期都是函数,执行时评估为true或false。一个所有预期都为true的测试就算是一条通过的细则,一条细则有一个或者多个预期为false的话,就是个失败的测试。
    一个简单的测试可能像这样:
    describe('A spec suite', function() {
    it('contains a passing spec', function() {
    expect(true).toBe(true);
    });
    });
    这个细则的标题,追加到describe()标题之后,就成为了“一个细则套件包含一条已通过的细则”。
    (3)预期
    测试应用时,我们会想要断言条件在应用的不同阶段是符合我们期望的。我们要写的这个测试读起来就像这样:“如果我们点击这个按钮,就期望有这个结果。”例如,“如果我们导航到首页,我们期望欢迎信息会被渲染出来。”
    使用expect()函数来建立预期。expect()函数带有一个单值参数。这个参数被称为真实值。
    要建立一个预期,我们给它串联一个带单值参数的匹配器函数,这个参数就是期望值。
    这些匹配器函数实现了一个在真实值和期望值之间的布尔比较。可以通过在调用匹配器之前调一个not来创建测试的否定式。
    (4)内置匹配器matchers
    常用的Matchers有:
        toBe():相当于===比较。
        toNotBe()
        toBeDefined():检查变量或属性是否已声明且赋值。
        toBeUndefined()
        toBeNull():是否是null。
        toBeTruthy():如果转换为布尔值,是否为true。
        toBeFalsy()
        toBeLessThan():数值比较,小于。
        toBeGreaterThan():数值比较,大于。
        toEqual():相当于==,注意与toBe()的区别。
        toNotEqual()
        toContain():数组中是否包含元素(值)。只能用于数组,不能用于对象。
        toBeCloseTo():数值比较时定义精度,先四舍五入后再比较。
        toHaveBeenCalled()
        toHaveBeenCalledWith()
        toMatch():按正则表达式匹配。
        toNotMatch()
        toThrow():检验一个函数是否会抛出一个错误
    所有的matchers匹配器支持添加 .not反转结果: expect(x).not.toEqual(y);
    举例:
    describe('A spec suite', function() {
    it('contains a passing spec', function() {
    var value = "<h2>Header element: welcome</h2>";
    expect(value).toMatch(/welcome/);
    expect(value).toMatch('welcome');
    expect(value).not.toMatch('goodbye');
    });
    });
     
    describe('A spec suite', function() {
    it('contains a passing spec', function() {
    var arr = [1,2,3,4];
    expect(arr).toContain(4);
    expect(arr).not.toContain(12);
    });
    });
     
    describe('A spec suite', function() {
    it('contains a passing spec', function() {
    expect(function() {
    return a + 10;
    }).toThrow();
    expect(function() {
    return 2 + 10;
    }).not.toThrow();
    });
    });
    (5)自定义matcher匹配器
    自定义Matcher(被称为Matcher Factories)实质上是一个函数(该函数的参数可以为空),该函数返回一个闭包,该闭包的本质是一个compare函数,compare函数接受2个参数:actual value 和 expected value。
    compare函数必须返回一个带pass属性的结果Object,pass属性是一个Boolean值,表示该Matcher的结果(为true表示该Matcher实际值与预期值匹配,为false表示不匹配),也就是说,实际值与预期值具体的比较操作的结果,存放于pass属性中。
    (6)Setup和Teardown操作
    Jasmine的Setup和Teardown操作(Setup在每个测试用例Spec执行之前做一些初始化操作,Teardown在每个Sepc执行完之后做一些清理操作,这两个函数名称来自于JUnit),是由一组全局beforeEach,afterEach, beforeAll,afterAll函数来实现的。
        beforeEach():在describe函数中每个Spec执行之前执行。
        afterEach(): 在describe函数中每个Spec数执行之后执行。
        beforeAll():在describe函数中所有的Specs执行之前执行,但只执行一次,在Sepc之间并不会被执行。
        afterAll(): 在describe函数中所有的Specs执行之后执行,但只执行一次,在Sepc之间并不会被执行。
    beforeAll 和 afterAll适用于执行比较耗时或者耗资源的一些共同的初始化和清理工作。而且在使用时还要注意,它们不会在每个Spec之间执行,所以不适用于每次执行前都需要干净环境的Spec。
    (7)this关键字
    除了在describe函数开始定义变量,用于各it函数共享数据外,还可以通过this关键字来共享数据。
    在每一个Spec的生命周期(beforeEach->it->afterEach)的开始,都将有一个空的this对象(在开始下一个Spec周期时,this会被重置为空对象)。
    3.高级特性
    1.Spy
    Spy能监测任何function的调用和方法参数的调用痕迹。需使用2个特殊的Matcher:
        toHaveBeenCalled:可以检查function是否被调用过,
        toHaveBeenCalledWith: 可以检查传入参数是否被作为参数调用过。
    2.spyOn
    使用spyOn(obj,'function')来为obj的function方法声明一个Spy。不过要注意的一点是,对Spy函数的调用并不会影响真实的值。
    describe("A spy", function() {
      var foo, bar = null;
     
      beforeEach(function() {
        foo = {
          setBar: function(value) {
            bar = value;
          }
        };
     
        spyOn(foo, 'setBar');
     
        foo.setBar(123);
        foo.setBar(456, 'another param');
      });
     
      it("tracks that the spy was called", function() {
        expect(foo.setBar).toHaveBeenCalled();
      });
     
      it("tracks all the arguments of its calls", function() {
        expect(foo.setBar).toHaveBeenCalledWith(123);
        expect(foo.setBar).toHaveBeenCalledWith(456, 'another param');
      });
     
      it("stops all execution on a function", function() {
        // Spy的调用并不会影响真实的值,所以bar仍然是null。
        expect(bar).toBeNull();
      });
    });
    3.and.callThrough
    如果在spyOn之后链式调用and.callThrough,那么Spy除了跟踪所有的函数调用外,还会直接调用函数的真实实现,因此Spy返回的值就是函数调用后实际的值了。
      ...
      spyOn(foo, 'getBar').and.callThrough();
      foo.setBar(123);
      fetchedBar = foo.getBar();
     
      it("tracks that the spy was called", function() {
        expect(foo.getBar).toHaveBeenCalled();
      });
     
      it("should not effect other functions", function() {
        expect(bar).toEqual(123);
      });
     
      it("when called returns the requested value", function() {
        expect(fetchedBar).toEqual(123);
      });
    });
    4.全局匹配谓词
    (1)jasmine.any
    jasmine.any的参数为一个构造函数,用于检测该参数是否与实际值所对应的构造函数相匹配。
    describe("jasmine.any", function() {
      it("matches any value", function() {
        expect({}).toEqual(jasmine.any(Object));
        expect(12).toEqual(jasmine.any(Number));
      });
     
      describe("when used with a spy", function() {
        it("is useful for comparing arguments", function() {
          var foo = jasmine.createSpy('foo');
          foo(12, function() {
            return true;
          });
     
          expect(foo).toHaveBeenCalledWith(jasmine.any(Number), jasmine.any(Function));
        });
      });
    });
    (2)jasmine.anything
    jasmine.anything用于检测实际值是否为nullundefined,如果不为nullundefined,则返回true
    it("matches anything", function() {
        expect(1).toEqual(jasmine.anything());});
    (3)jasmine.objectContaining
    用于检测实际Object值中是否存在特定key/value对。
      var foo;
     
      beforeEach(function() {
        foo = {
          a: 1,
          b: 2,
          bar: "baz"
        };
      });
     
      it("matches objects with the expect key/value pairs", function() {
        expect(foo).toEqual(jasmine.objectContaining({
          bar: "baz"
        }));
        expect(foo).not.toEqual(jasmine.objectContaining({
          c: 37
        }));
      });
    5.Jasmine Clock
    Jasmine Clock用于setTimeout和setInterval的回调控制,它使timer的回调函数同步化,不再依赖于具体的时间,而是将时间离散化,使测试人员能精确控制具体的时间点。
    调用jasmine.clock().install()可以在特定的需要操纵时间的Spec或者Suite中安装Jasmine Clock,注意操作完后要调用jasmine.clock().uninstall()进行卸载。
    var timerCallback;
      beforeEach(function() {
        timerCallback = jasmine.createSpy("timerCallback");
        jasmine.clock().install();
      });
      afterEach(function() {
        jasmine.clock().uninstall();
      });
    6.模拟超时(Mocking Timeout)
    可以调用jasmine.clock().tick(nTime)来模拟计时,一旦tick中设置的时间nTime,其累计设置的值达到setTimeout或setInterval中指定的延时时间,则触发回调函数。
      it("causes an interval to be called synchronously", function() {
        setInterval(function() {
          timerCallback();
        }, 100);
     
        expect(timerCallback).not.toHaveBeenCalled();
     
        jasmine.clock().tick(101);
        expect(timerCallback.calls.count()).toEqual(1);
     
        jasmine.clock().tick(50);
        expect(timerCallback.calls.count()).toEqual(1);
        //tick设置的时间,累计到此201ms,因此会触发setInterval中的毁掉函数被调用2次。
        jasmine.clock().tick(50);
        expect(timerCallback.calls.count()).toEqual(2);
      });
    三 模拟
    1.什么是mock
    在开始写测试之前,我们需要理解测试的一个核心特性:模拟。模拟允许我们在受控环境下定义模拟对象来模仿真实对象的行为。AngularJS提供了自己的模拟库:angular-mocks,位于angular-mock.js文件中,因此如果要在单元测试中建立模拟对象,就必须确保在Karma配置中,即test/karma.conf.js文件的file数组中包含了angular-mock.js。
    2.ng-mock中的一些常用的方法
    (1)angular.mock.module
    此方法非常方便调用,因为angular.mock.module函数被发布在全局作用域的window接口上了。
    module是用来配置inject方法注入的模块信息,参数可以是字符串,函数,对象,它一般用在beforeEach方法里,因为这个可以确保在执行测试任务的时候,inject方法可以获取到模块配置。
    describe('myApp',function(){
    //模拟'myApp'angular模块
    beforeEach(angular.mock.module('myApp'));
    it('....')
    });
    建立了模拟的angular模块之后,可以把连接到这个模块上的任意服务注入到测试代码中。在我们的测试代码中,注入依赖关系很重要,因为我们隔离了想要测试的功能。
    (2)angular.mock.inject
    inject函数也是在window对象上的,为的是全局访问,因此可以直接调用inject。
    inject是用来注入上面配置好的ng模块,方便在it的测试函数里调用。
    describe('myApp',function(){
    var scope;
    beforeEach(angular.mock.module('myApp'));
    beforeEach(angular.mock.inject(function($rootscope){
         scope=$rootscope.$new();
    });
    it('...');
    });
    通常我们会用将引入进测试时使用的名字来保存它。比如说,如果我们在测试一个服务,可以注入这个服务,然后把它的引用用一种稍微不同的命名方案存储起来。在注入的服务名称两端使用下划线,当它被注入时,注入器会忽略它的名称。
    describe('myApp',function(){
    var scope;
    beforeEach(angular.mock.module('myApp'));
    beforeEach(angular.mock.inject(function(_myService_){
         myService=_myService_;
    });
    it('...');
    });
    3.模拟$httpBackend
    angualr内置了$httpBackend模拟库,这样我们可以在应用中模拟任何外部的XHR请求,避免在测试中创建昂贵的$http请求。
    如:
    var app = angular.module('Application', []);
     
    app.controller('MainCtrl', function($scope, $http) {
        $http.get('Users/users.json').success(function(data){
            $scope.users = data;
        });
        $scope.text = 'Hello World!';
    });
    测试:
    describe('MainCtrl', function() {
        //我们会在测试中使用这个scope
        var scope, $httpBackend;
     
        //模拟我们的Application模块并注入我们自己的依赖
        beforeEach(angular.mock.module('Application'));
     
        //模拟Controller,并且包含 $rootScope 和 $controller
        beforeEach(angular.mock.inject(function($rootScope, $controller, _$httpBackend_) {
            //设置$httpBackend冲刷$http请求
            $httpBackend = _$httpBackend_;
            $httpBackend.when('GET', 'Users/users.json').respond([{
                id: 1,
                name: 'Bob'
            }, {
                id: 2,
                name: 'Jane'
            }]);
            //创建一个空的 scope
            scope = $rootScope.$new();
     
            //声明 Controller并且注入已创建的空的 scope
            $controller('MainCtrl', {
                $scope: scope
            });
        }));
     
        // 测试从这里开始
        it('should have variable text = "Hello World!"', function() {
            expect(scope.text).toBe('Hello World!');
        });
        it('should fetch list of users', function() {
            $httpBackend.flush();
            expect(scope.users.length).toBe(2);
            expect(scope.users[0].name).toBe('Bob');
            //输出结果以方便查看
            for(var i=0;i<scope.users.length;i++){
                console.log(scope.users[i].name);
            }
        });
    });
    可以使用$httpBackend.when和$httpBackend.expect提前设置请求的伪数据,最后在请求后执行$httpBackend.flush就会立即执行完成http请求。
    4.$httpBackend常用方法
    (1)when :新建一个后端定义(backend definition)。
    when(method, url, [data], [headers]);
    (2)expect :新建一个请求期望(request expectation)。
    expect(method, url, [data], [headers]);
    method表示http方法注意都需要是大写(GET, PUT…);
    url请求的url可以为正则或者字符串;
    data请求时带的参数,
    headers请求时设置的header。
    如果这些参数都提供了,那只有当这些参数都匹配的时候才会正确的匹配请求。when和expect都会返回一个带respond方法的对象。respond方法有3个参数status,data,headers通过设置这3个参数就可以伪造返回的响应数据了。
    $httpBackend.when与$httpBackend.expect的区别在于:$httpBackend.expect的伪后台只能被调用一次(调用一次后会被清除),第二次调用就会报错,而且$httpBackend.resetExpectations可以移除所有的expect而对when没有影响。
    快捷方法:when和expect都有对应的快捷方法whenGET, whenPOST,whenHEAD, whenJSONP, whenDELETE, whenPUT; expect也一样。
    使用快捷方法进行测试:
    $httpBackend.whenGET('/someUrl').respond({name:'wolf'},{'X-Record-Count':100});   //声明Mock服务,模拟后端服务器行为
    //调用网络接口
    $http.get('/someUrl').success(function(data){
         expect(data.name).toBe('wolf');
    });
    //刷新一次,模拟后端返回请求,在调用这个命令之前,success中的回调函数不会被执行
    $httpBackend.flush();
  • 相关阅读:
    docker-compose 使用
    mysql UNIX时间戳与日期的相互转换 查询表信息
    mysql查看表结构命令
    PostgreSQL新手入门
    ibdata1是?
    ubuntu 12.04 安装 nginx+php+mysql web服务器
    读懂IL代码就这么简单(二)
    读懂IL代码就这么简单(一)
    在Ubuntu Linux下怎样安装QQ
    jQuery 选择器
  • 原文地址:https://www.cnblogs.com/lyy-2016/p/5699032.html
Copyright © 2020-2023  润新知