单元测试既模块测试

javascript单位测试


Admin
2012年8月3日名人名言:从智慧的土壤中生出三片绿芽:好的思想,好的语言,好的行动。——希腊谚语

前端测试一向是困扰大师的一个困难,本文参考了能找到的一些牛博,列举了几种常见的javascript单位测试框架,迎接批驳领导。


 


1.      什么是单位测试


在策画机编程中,单位测试(又称为模块测试)是针对法度模块(软件设计的最小单位)来进行正确性查验的测试工作。法度单位是应用的最小可测试部件。在过程化编程中,一个单位就是单个法度、函数、过程等;对于面向对象编程,最小单位就是办法,包含基类(超类)、抽象类、或者派生类(子类)中的办法。


每个幻想的测试案例自力于其它案例;为测试时隔离模块,经常应用stubs、mock 或fake等测试马甲法度。单位测试凡是由软件开辟人员编写,用于确保他们所写的代码合适软件需乞降遵守开辟目标。


单位测试的目标是隔离法度模块并证实这些单个模块是正确的。单位测试能确保在开辟过程的早期就能发明题目,是为了让法度“死得更早”。我们应当从开辟的早期就为所有函数和办法编写单位测试,可读性强的单位测试可以使法度员便利地搜检代码片段是否依然正常工作。杰出设计的单位测试案例覆盖法度单位分支和轮回前提的所有路径。采取这种自底向上的测试路径,先测试法度模块再测试模块的凑集,一旦变革导致错误产生,借助于单位测试可以快速定位并修复错误。


2.      JavaScript单位测试近况


单位测试在后台开辟中很是风行和普及,比如JAVA开辟者的JUnit等,而在前端开辟中则应用的很是少。究其原因,主如果单位测试更实用于逻辑代码的测试,这对于JAVA等后台编程说话来说测试起来很是便利,然则前端开辟很多时辰要要UI打交道,UI相干的代码不是不成以进行单位测试,但的确很麻烦,比起逻辑代码来说艰苦多了,这就导致了单位测试在前端开辟没有普及起来。


然则跟着单位测试的普及,尤其是急迅开辟的推动,出现了很多优良的JavaScript单位测试框架,如QUnit、Jasmine等。所有的这些框架根蒂根基上都能对Javascript代码进行很好的测试,当然UI项目组的代码测试一样斗劲麻烦,然则我们可以经由过程精心计表情关我们的测试代码来测试项目组UI代码。然则每个框架都不是全能的,它们都有各自善于的范畴,下面拔取了几个具有代表性的框架进行介绍。


 


1.      单位测试常用框架


l   QUnit框架


a)        简介


QUnit是jQuery团队开辟的JavaScript单位测试对象,功能强大且应用简单。今朝所有的JQuery代码都应用QUnit进行测试,原生的JavaScript也可以应用QUnit。


最初,John Resig将QUnit设计为jQuery的一项目组。2008年,QUnit才有了本身的名字、主页和API文档,也开端容许其他人用它来做单位测试。但当时QUnit还是基于jQuery的。直到2009年,QUnit才可以完全的自力运行。


b)       长处


应用起来很是便利,有摩登的外观和完全的测试功能(包含异步测试);


很是简单,轻易上手,今朝公开的API只有19个;


不须要依附其它任何软件包或框架,只要能运行JS的处所就可以,QUnit本身只有一个JS文件和CSS文件,当然若是须要可以和jQuery等其它框架集成;


不仅支撑在浏览器中测试,还支撑在Rhino和node.js等后端测试。


c)        不足


对主动化支撑不好,很难和Ant、Maven或主动构建等对象集成,首要用在浏览器中进行测试。


d)       API


QUnit所有的API可以分为三类:Setup,Assertions,Asynchronous Testing,下面就分别对这些API做些介绍:


Setup:


test( name, [expected], testFun )   代表QUnit中的一个测试


name:要测试的名称,比如“加法函数”或“add”等


expected:可选参数,用来默示该测试函数的断言的数量,是个正整数


testFun:一个函数,所有的测试代码都应当包含在该函数里,凡是这是一个匿名函数。


例:



test(“add function”, 1, function() {

    equal(add(1, 2), 3);

});



asyncTest( name, [expected], testFun )   代表QUnit中的一个异步测试,参数同test


expect( amount )   用在测试函数中,用于声明测试断言的数量,这个函数和test中的expected参数的感化是一样的。首要感化就是搜检你声明的个数和你写的断言的实际个数是否一致。


module( name, [lifecycle] )   首要用于测试函数的分组,一个module函数为一个分组,比如module(“validate”)默示后面的测试用例都是validate相干的代码,或者module(“common.js”),注解后面的测试用例都是common.js里面的代码。一个测试文件可以写多个module。


name:分组或者模块的名称


lifecycle:可选参数,它是一个对象,可以设置setup和teardown回调函数


例:



module(“common.js”, 

    {

        setup:function(){},

        teardown: function() {} 

    }

);



setup:在module开端之前履行,可认为该module下面的测试代码做一些筹办工作


teardown:将会在该module的所有测试代码履行后履行,比如做一些清理还原工作等。


QUnit.init( )   用于初始化QUnit测试框架,凡是这个函数是不须要我们手工调用的。


QUnit.reset( )   重设函数,凡是是在每个test函数履行后由QUnit本身调用来重设全部QUnit测试景象,当然须要时我们本身也可以调用它来答复复兴,不常用。


 


Assertions:


ok( state, [message] )   断言。state值为true时默示经由过程,不然失败。


equal( actual, expected, [message] )   斗劲参数actual和expected是否相等,相当于 ==


notEqual( actual, expected, [message] )   斗劲两个参数是否不相等,相当于 !=


deepEqual( actual, expected, [message] )   首要用于数组和对象等类型的值是否相等,会递归遍历它们所包含的值是否相等。


notDeepEqual( actual, expected, [message] )   首要用于数组和对象等类型的值是否不相等,会递归遍历它们所包含的值是否不相等。


strictEqual( actual, expected, [message] )   斗劲两个参数是否严格相等,相当于 ===


notStrictEqual( actual, expected, [message] )   斗劲两个参数是否不严格相等,相当于 !==


throws( block, expected, [message] )   测试block函数是否抛出一个异常,抛出则经由过程,不抛则失败。


block:我们要测试的函数


expected:可选参数,是一个类型,用来验证第一个函数抛出的异常是否是我们预期的类型。


例:



function CustomError( message ) {

    this.message = message;

}



CustomError.prototype.toString = function() {

    return this.message;

};

throws(

    function() {

        throw new CustomError(“some error description”);

    },

    CustomError,

    "raised error is an instance of CustomError"

);



 


Asynchronous Testing:


stop( [increment] )   停止测试的运行,用于异步测试。在异步测试时一般先把QUnit的test runner停下来。


increment:增长停止的时候。


start( [decrement] )   当异步调用成功后就应当把停止的test runner启动起来让它接着往前跑


decrement:用来削减停止的时候。


例:



test( "a test", function() {

    stop();

    var result = null;

    ¥.ajax(

        url,

        {},

        function(data){

            result = data;

        }

    );

    setTimeout(function() {

        equals(result, "success" );

        start();

    }, 150 );

});



 


e)        应用


test.html


导入qunit.css,qunit.js


依次导入被测试文件src.js和测试文件test.js



src.js里是我们要测试的一些函数



test.js里放我们的测试



打开test.html,显示:



若是期望值与函数履行的成果不一致,会报错:


test.js



test.html显示:



期望值与成果不符,测试不经由过程。


 


与浏览器主动化测试对象集成的接口:


都是QUnit主动调用的一些函数,一般不消改,也可以本身定制


QUnit.log(Function({ result, actual, expected, message }))   这个接口会在每个断言履行后被主动调用


result:断言是否经由过程


message:断言里的message参数


例:



QUnit.log(function(details){

	alert(“Log: ” + details.result + “ ” + details.message);

})



QUnit.testStart(Function({ name }))   在每个测试函数履行前被主动调用


name:测试函数中的name参数值


QUnit.testDone(Function({ name, failed, passed, total }))   在每个测试函数停止后履行被主动调用


name:同上


failed:指失败断言的个数


passed:指成功断言的个数


total:所有断言的个数


QUnit.moduleStart(Function({ name }))   在每个module所有的测试代码履行前被主动调用


name:module函数中name参数的值


QUnit.moduleDone(Function({ name, failed, passed, total }))   在每个module所有的测试代码履行完之后被主动履行


failed:指失败断言的个数


passed:指成功断言的个数


total:指所有断言的个数


QUnit.begin(Function())   在所有的测试代码调用之前运行


QUnit.done(Function({ failed, passed, total, runtime }))   在所有的测试代码调用之后运行


failed:指失败断言的个数


passed:指成功断言的个数


total:指所有断言的个数


runtime:所有代码的履行时候


 


API及下载:http://api.qunitjs.com/


参考:http://www.weakweb.com/articles/255.html


http://www.iteye.com/topic/981253


 


l  Jasmine框架


a)        简介


Jasmine是一个有名的JavaScript单位测试框架,它是自力的行动驱动开辟框架,语法清楚易懂。


行动驱动开辟(BDD):是一种急迅软件开辟的技巧,它激劝软件项目中的开辟者、QA和非技巧人员或贸易参与者之间的协作。BDD最初是由Dan North在2003年定名,它包含验收和客户测试驱动等的极限编程的实践,作为对测试驱动开辟的回应。在畴昔的数年里,获得了极大的成长。


BDD的重点是经由过程与好处相干者的评论辩论取得对预期的软件行动的复苏熟悉。它经由过程用天然说话书写不法度员可读的测试用例扩大了测试驱动开辟办法。行动驱动开辟人员应用混淆了范畴中同一的说话的母语说话来描述他们的代码的目标。这让开辟者得以把精力集中在代码应当怎么写,而不是技巧细节上,并且也最大程度的削减了将代码编写者的技巧说话与贸易客户、用户、好处相干者、项目经管者等的范畴说话之间往返翻译的价格。


 


BDD的做法包含:


l  确立不合好处相干者要实现的前景目标


l  应用特点注入办法绘制出达到这些目标所须要的特点


l  经由过程由外及内的软件开辟办法,把涉及到的好处相干者融入到实现的过程中


l  应用例子来描述应用法度的行动或代码的每个单位


l  经由过程主动运行这些例子,供给快速反馈,进行回归测试


l  应用“该当(should)”来描述软件的行动,以帮助申明代码的职责,以及答复对该软件的功能性的质疑


l  应用“确保(ensure)”来描述软件的职责,以把代码本身的效用与其他单位(element)代码带来的边际效用中区分出来。


l  应用mock作为还未编写的相干代码模块的替人


 


BDD特点注入:一个公司可能有多个会带来贸易好处的不合愿景,凡是包含盈利、省钱或保护钱。一旦某个愿景被开辟小组断定为当前前提下的最佳愿景,他们将须要更多的帮助来成功实现这个前景。


然后断定该愿景的首要好处相干者,会带入其他的好处相干者。每个相干者要定义为了实现该愿景他们须要完成的目标。例如,法务项目组可能请求某些监管要获得满足。市场营销负责人可能要参加将应用该软件的用户的社区。安然专家须要确保该软件不会受到SQL注入的进击。


经由过程这些目标,会定义出要实现这些目标所须要的可能的题目或者特点凑集。例如,“容许用户排序供献值”或“交易审计”。从这些主题,可以断定用户功能以及用户界面的第一批细节。


 


b)       长处


它是基于行动驱动开辟实现的测试框架,它的语法很是切近天然说话,简单了然,轻易懂得。


能很便利的和Ant、Maven等进行集成进行主动化测试,也可以便利和Jekins等连气儿集成对象进行集成,可以生成测试成果的XMl文档。


它有雄厚的API,同时用户也支撑用户扩大它的API,这一点很少有其它框架可以或许做到。


应用便利简单,只须要引入两个js文件即可


不仅支撑在浏览器中测试,还支撑在Rhino和node.js等后端测试。


对于Ruby说话有特此外支撑,可以或许很是便利的集成到Ruby项目中去


c)        不足


在浏览器中的测试界面不如QUnit美观、具体。


d)       API


it(string, function)   一个测试Spec


string:测试名称


function:测试函数


describe (string, function)    一个测试组开端于全局函数describe,一个describe是一个it的凑集。describe包含n个it,一个it包含n个断定断言  Suite


string:测试组名称


function:测试组函数



describe("测试add()函数", function() {

    it("1 + 1 = 2", function(){

        expect(add(1, 1)).toBe(2);

    });

});



beforeEach(function)   定义在一个describe的所有it履行前做的操纵


afterEach(function)   定义在一个describe的所有it履行后做的操纵


expect(a).matchFunction(b)


expect(a).not.matchFunction(b)   期望a和b满足匹配体式格式matchFunction


matchFunctions:


 


toBe   相当于===,处理惩罚简单字面值和变量



    it("toBe相当于===", function(){

        var a = 12;

        var b = a;



        expect(a).toBe(b);

        expect(a).not.toBe(null);

        expect(false == 0).toBe(true);

    });

    it("toBe不克不及当==用", function(){

        expect(false).toBe(0);

    });




toEqual   处理惩罚简单字面值和变量,并且可以处理惩罚对象,数组



    it("toEqual可以处理惩罚字面值,变量和对象", function(){

        var a = 12;

        expect(a).toEqual(12);



        var foo = {key : "key"};

        var bar = {key : "key"};

        expect(foo).toEqual(bar);



        var arr1 = [];

        arr1["p1"] = "string1";

        var arr2 = [];

        arr2["p1"] = "string1";

        var obj = {};

        obj["p1"] = "string1";

        expect(arr1).toEqual(arr2);

        expect(arr1).toEqual(obj);

    });



toMatch   按正则式检索。



    it("toMatch匹配正则式", function(){

        var message = "foo bar baz";

        expect(message).toMatch(/bar/);

        expect(message).toMatch("bar");

        expect(message).not.toMatch(/quux/);

        expect(message).toMatch(/^f/);

        expect(message).not.toMatch(/f¥/);

    });



toBeDefined   是否已声明且赋值



    it("toBeDefined检测变量非undefined", function(){

        var a = { key : "key"};



        expect(a.key).toBeDefined();

        expect(a.foo).not.toBeDefined();



        //expect(c).not.toBeDefined();  //未声明失足

        var b;

        expect(b).not.toBeDefined();

    });



       对象.未声明属性.not.toBeDefined();   经由过程


       未声明变量.not.toBeDefined();       报错


toBeUndefined      是否undefined


toBeNull   是否null


toBeTruthy   若是转换为布尔值,是否为true


toBeFalsy    若是转换为布尔值,是否为false


toContain   数组中是否包含元素(值)。只能用于数组,不克不及用于对象



    it("toContain查验数组中是否包含元素(值)", function(){

        var a = ["foo", "bar", "baz"];



        expect(a).toContain("bar");

    });



toBeLessThan   数值斗劲,小于


toBeGreaterThan   数值斗劲,大于


toBeCloseTo   数值斗劲时定义精度,先四舍五入后再斗劲



    it("toBeCloseTo数值斗劲,指定精度,先四舍五入再斗劲", function() {

        var pi = 3.1415926, e = 2.78;



        expect(pi).toBeCloseTo(e, 0);

        expect(pi).not.toBeCloseTo(e, 0.1);

    });



 


toThrow    查验一个函数是否会抛出一个错误



    it("toThrow查验一个函数是否会抛出一个错误", function() {

        var foo = function() {

          return 1 + 2;

        };

        var bar = function() {

          return a + 1;

        };



        expect(foo).not.toThrow();

        expect(bar).toThrow();

    });



 


注:describe可嵌套


       xdescribe 和 xit:路过不履行,成果不显示。像display:none。点把握栏中skipped显示


      


Spy   存储函数的被调用景象和参数(函数把守器,记录被调用景象,但函数并不真履行)



describe("对spy函数的测试", function() {

    var foo, bar = null;



    beforeEach(function() {

        foo = {

            setBar: function(value) {

                bar = value;

            }

        };



        spyOn(foo, ""setBar"");  //foo为spy函数



        foo.setBar(123);

        foo.setBar(456, ""another param"");

    });



    it("测试foo函数是否被调用过", function() {

        expect(foo.setBar).toHaveBeenCalled();

    });



    it("测试foo函数被调用的次数", function() {

        expect(foo.setBar.calls.length).toEqual(2);

    });



    it("测试foo函数被调用时传入的参数", function() {

        expect(foo.setBar).toHaveBeenCalledWith(123);

        expect(foo.setBar).toHaveBeenCalledWith(456, ""another param"");

    });



    it("上一次被调用的参数", function() {

        expect(foo.setBar.mostRecentCall.args[0]).toEqual(456);

    });



    it("所有被调用的景象存在一个数组里", function() {

        expect(foo.setBar.calls[0].args[0]).toEqual(123);

    });



    it("函数并未真的履行", function() {  

        expect(bar).toBeNull();  

    });

});



Spy addCallThrough  函数把守器,但函数真的履行



describe("对spy函数的测试,函数真的履行", function() {

    var foo, bar, fetchedBar;



    beforeEach(function() {

        foo = {

            setBar: function(value) {

                bar = value;

            },

            getBar: function() {

                return bar;

            }

        };



        //spyOn(foo, "setBar");    //若是加上这句,setBar不真的履行,后两个spec不经由过程

        spyOn(foo, ""getBar"").andCallThrough();



        foo.setBar(123);

        fetchedBar = foo.getBar();

    });



    it("测试foo中getBar函数是否被调用过", function() {

        expect(foo.getBar).toHaveBeenCalled();

    });



    it("foo中setBar函数真的履行了", function() {

        expect(bar).toEqual(123);

    });



    it("foo中getBar函数真的履行了", function() {

        expect(fetchedBar).toEqual(123);

    });

});



Spy andReturn  函数把守器,函数不真的履行。指定把守的函数的返回值



describe("A spy, when faking a return value", function() {

    var foo, bar, fetchedBar;



    beforeEach(function() {

        foo = {

            setBar: function(value) {

                bar = value;

            },

            getBar: function() {

                return bar;

            }

        };



        spyOn(foo, ""getBar"").andReturn(745);  //指定getBar函数返回745



        foo.setBar(123);

        fetchedBar = foo.getBar();

    });



    it("测试foo中getBar函数是否被调用过", function() {

        expect(foo.getBar).toHaveBeenCalled();

    });



    it("不影响未被把守的其它函数", function() {

        expect(bar).toEqual(123);

    });



    it("指定的返回值745", function() {

        expect(fetchedBar).toEqual(745);

    });

});



Spy addCallFake  调换被把守的函数,原函数不履行



describe("调换被把守的函数,原函数不履行", function() {

    var foo, bar, fetchedBar;



    beforeEach(function() {

        foo = {

            setBar: function(value) {

                bar = value;

            },

            getBar: function() {

                alert("frostbelt");

                return bar;

            }

        };



        spyOn(foo, ""getBar"").andCallFake(function() {

            return 1001;

        });



        foo.setBar(123);

        fetchedBar = foo.getBar();

    });



    it("测试foo中getBar函数是否被调用过", function() {

        expect(foo.getBar).toHaveBeenCalled();

    });



    it("不影响未被把守的其它函数", function() {

        expect(bar).toEqual(123);

    });



    it("getBar被addCallFake指定的匿名函数庖代,getBar不履行", function() {

        expect(fetchedBar).toEqual(1001);

    });

});



若是你没有什么可把守的又其实想把守一下,该咋办?本身create一个被把守函数。。


jasmine.createSpy(functionId)



describe("本身造一个被把守函数。啊,我纷乱了。。", function() {

    var whatAmI;



    beforeEach(function() {

        whatAmI = jasmine.createSpy(""whatAmI"");



        whatAmI("I", "am", "a", "spy");

    });



    it("有个id,是createSpy的传入函数,用于报错", function() {

        expect(whatAmI.identity).toEqual(""whatAmI"")

    });



    it("是否被调用", function() {

        expect(whatAmI).toHaveBeenCalled();

    });



    it("被调用的次数", function() {

        expect(whatAmI.calls.length).toEqual(1);

    });



    it("被调用的参数", function() {

        expect(whatAmI).toHaveBeenCalledWith("I", "am", "a", "spy");

    });



    it("比来一次被调用", function() {

        expect(whatAmI.mostRecentCall.args[0]).toEqual("I");

    });

});



有时须要把守一个对象的很多办法,用createSpyObj添加办法数组


jasmine.createSpyObj(obj, methodArray)



describe("有时须要把守一个对象的很多个办法,用createSpyObj添加数组", function() {

    var tape;



    beforeEach(function() {

        tape = jasmine.createSpyObj(""tape"", [""play"", ""pause"", ""stop"", ""rewind""]);



        tape.play();

        tape.pause();

        tape.rewind(0);

    });



    it("tape对象的这四个办法已被定义", function() {

        expect(tape.play).toBeDefined();

        expect(tape.pause).toBeDefined();

        expect(tape.stop).toBeDefined();

        expect(tape.rewind).toBeDefined();

    });



    it("四个办法是否被调用", function() {

        expect(tape.play).toHaveBeenCalled();

        expect(tape.pause).toHaveBeenCalled();

        expect(tape.rewind).toHaveBeenCalled();

        expect(tape.stop).not.toHaveBeenCalled();

    });



    it("被调用时传入的参数", function() {

        expect(tape.rewind).toHaveBeenCalledWith(0);

    });

});



 


jasmine.any   类型断定。instanceof



describe("类型匹配", function() {

    it("相当于instanceof", function() {

        expect({}).toEqual(jasmine.any(Object));

        expect(12).toEqual(jasmine.any(Number));

    });



    it("也可以用于spy", function() {

        var foo = jasmine.createSpy(""foo"");

        foo(12, function() {

            return true

        });



        expect(foo).toHaveBeenCalledWith(jasmine.any(Number), jasmine.any(Function));

        //foo被调用时的参数 类型断定

    });

});



jasmine.Clock.useMock()   jasmine本身把握时候,实现异步调试,削减守候


jasmine.Clock.tick(n:uint)   向前n毫秒



describe("jasmine本身把握时候,实现异步调试,削减守候", function() {

    var timerCallback; 



    beforeEach(function() {

        timerCallback = jasmine.createSpy(""timerCallback"");

        jasmine.Clock.useMock();

    }); 



    it("setTimeout", function() {

        setTimeout(function() {

            timerCallback();

        }, 100);



        expect(timerCallback).not.toHaveBeenCalled();



        jasmine.Clock.tick(101);



        expect(timerCallback).toHaveBeenCalled();

    });



    it("setInterval", function() {

        setInterval(function() {

            timerCallback();

        }, 100);



        expect(timerCallback).not.toHaveBeenCalled();



        jasmine.Clock.tick(101);

        expect(timerCallback.callCount).toEqual(1);



        jasmine.Clock.tick(50);

        expect(timerCallback.callCount).toEqual(1);



        jasmine.Clock.tick(50);

        expect(timerCallback.callCount).toEqual(2);

    });

});



注:在这种景象下setTimeout和setInterval的callback为同步的,体系时候不再影响履行


 


runs(function)  waitsFor(function, message, millisec)   Jasmine异步调试  按本身的懂得写个例子



describe("jasmine异步调试,对ajax成果的断言", function(){

    var data, flag = false;



    it("ajax是否按时返回了正确成果", function(){

        runs(function(){

            ¥.post(

                url,

                {},

                function(data){

                    flag = true;

                    data = data.someAttr;

                }

            );

        });



        waitsFor(function(){  //flag为true或到2秒时履行  2秒内返回true则履行最后一个runs,到时未返回则本spec失足,返回第二个参数错误信息

            return flag;

        }, "ajax在指按时候2秒内未返回", 2000);



        runs(function(){  //直到waitsFor返回true时履行

            expect(data).toEqual("someThing");

        })

    });

});



注:it是一个spec,包含


runs(function)


waitsFor(function, message, millsec)


runs(function)


第一个runs里有一些异步的代码


waitsFor中的funciton若是在millsec内返回true,履行最后一个runs


       若是在millsec内不克不及返回true,spec不经由过程,显示错误信息message


原文代码:



 


e)        应用


在测试的页面里参加以下代码:



  <script type="text/javascript">



    (function() {

       var jasmineEnv = jasmine.getEnv();

       jasmineEnv.Interval = 1000;



       var trivialReporter = new jasmine.TrivialReporter();



       jasmineEnv.addReporter(trivialReporter);



       jasmineEnv.specFilter = function(spec) {

       	return trivialReporter.specFilter(spec);

       };



       var currentWindowOnload = window.onload;



       window.onload = function() {

        	if (currentWindowOnload) {

         		currentWindowOnload();

         	}

         	execJasmine();

       };



       function execJasmine() {

         	jasmineEnv.execute();

       }



    })();

  </script>



导入jasmine.css  jasmine.js  jasmine-html.js 


       src.js(源代码)  test.js(存放describes)


 


参考:http://www.weakweb.com/articles/255.html


       http://pivotal.github.com/jasmine/


下载:https://github.com/pivotal/jasmine/downloads


 


 


 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值