为什么需要测试 随着Web应用越来越复杂,它出错的可能性就会加大。为了保证应用不会出问题,我们需要一遍又一遍手动去点击UI界面检测我们代码是否正常。 这种手动方式耗时又不可靠。通过编写测试代码,可以提高我们编码质量,降低出错可能性。
测试类型 单元测试 (Unit testing)
单元测试(英语:Unit Testing)又称为模块测试, 是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。
单元测试是对应用中独立单位函数或类进行正确性校验。 单元测试的最基本原则: 输入条件相同则输出必定一样。 如果一个单位函数依赖于其他方法或数据,在测试时我们可以Mock依赖的方法或数据。 单元测试实例步骤:
准备测试条件
触发测试函数
验证结果是否正确
单元测试对被测单元要求:
被测单元内不存在可变因素,输入条件相同,输出结果必须相同
被测单元功能单一
被测单元对外依赖性低 单元测试通常采取断言(assertion)的形式,也就是测试某个功能的返回结果,是否与预期结果一致。如果与预期不一致,就表示测试失败。 单元测试可以保证代码的正确性,将出错可能性降到最低。
测试框架(实例+断言+运行时):
端到端测试(End to End Test) 模拟真实环境对应用进行全链路全流程测试被称作端到端测试。 端到端测试范围是整个应用:
1 从用户界面触发 --> 前端代码处理 --> 前端发起网络请求 --> 后端代码接受请求处理 --> 数据库处理 --> 后端代码返回请求处理 --> 前端接受网络反馈 --> 前端对反馈处理 --> 界面更新展示
端对端测试框架:WebDriver
集成测试(Integration test) 集成测试是单元测试的逻辑扩展。它的最简单的形式是:两个已经测试过的单元组合成一个组件,并且测试它们之间的接口。
测试模式 TDD (Test-Driven Development) 测试驱动的开发,是指先写好测试,再根据测试完成开发。 TDD模式一般会有很高的代码覆盖率。
BDD(Behavior-Driven Development) 行为驱动开发,用通用语言写用例描述软件行为的过程。 BDD接口提供下面六个方法:
describe(name,fn) // 描述一组测试用例
it(name,fn)/test(name,fn) //具体的测试用例
before(fn) // 测试用例执行前的动作
after(fn) // 测试用例执行后的动作
beforeEach(fn) // 每个用例执行前的动作
afterEach(fn) // 每个用例执行后的动作
BDD术语 套件 Describe 1 2 3 describe("A suite" , function ( ) { });
测试套件(Test suit),一组针对软件某个功能点测试用例。 测试套件一般 describe 函数,第一个参数是字符串,描述测试套件。第二个参数是函数,实现测试套件。
用例 It 1 2 3 4 5 describe("A suite" , function ( ) { it("contains spec with an expectation" , function ( ) { }); });
测试用例(Test case),软件单位的测试实例。它是软件测试中最小测试单位。 测试用例一般 it 或 test 函数,第一参数是字符串,描述测试用例。第二个参数是函数,实现测试用例。
断言 Expect 1 2 3 4 5 describe("A suite" , function ( ) { it("contains spec with an expectation" , function ( ) { expect(true ).toBe(true ); }); });
断言(assert)对被测单元行为描述和结果预期。一个测试用例可以包含一个或多个断言。 断言有三种风格:assert、expect、should
1 2 3 4 5 6 7 8 assert.equal(event.detail.item, '(item)' ); expect(event.detail.item).to.equal('(item)' ); event.detail.item.should.equal('(item)' );
一般expect风格更容易理解些。将被测试单元放入expect方法,后面用链式模式匹配预期的结果.to.equal()。
例子 一个计算器案例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 <section id ="screen" > 0</section > <section id ="keyboard" > <div class ="keyboard-level" > <button onclick ="clearFunction()" class ="keyboard-key" > CE</button > <button onclick ="cleanFunction()" class ="keyboard-key" > C</button > <button onclick ="cutFunction()" class ="keyboard-key" > <<</button > <button onclick ="signalFunction('%')" class ="keyboard-key" > %</button > </div > <div class ="keyboard-level" > <button onclick ="numFunction('7')" class ="keyboard-key" > 7</button > <button onclick ="numFunction('8')" class ="keyboard-key" > 8</button > <button onclick ="numFunction('9')" class ="keyboard-key" > 9</button > <button onclick ="signalFunction('x')" class ="keyboard-key" > X</button > </div > <div class ="keyboard-level" > <button onclick ="numFunction('4')" class ="keyboard-key" > 4</button > <button onclick ="numFunction('5')" class ="keyboard-key" > 5</button > <button onclick ="numFunction('6')" class ="keyboard-key" > 6</button > <button onclick ="signalFunction('-')" class ="keyboard-key" > -</button > </div > <div class ="keyboard-level" > <button onclick ="numFunction('1')" class ="keyboard-key" > 1</button > <button onclick ="numFunction('2')" class ="keyboard-key" > 2</button > <button onclick ="numFunction('3')" class ="keyboard-key" > 3</button > <button onclick ="signalFunction('+')" class ="keyboard-key" > +</button > </div > <div class ="keyboard-level" > <button onclick ="stateFunction()" class ="keyboard-key" > +/-</button > <button onclick ="numFunction('0')" class ="keyboard-key" > 0</button > <button onclick ="pointFunction()" class ="keyboard-key" > .</button > <button onclick ="resultFunction()" class ="keyboard-key" > =</button > </div > </section >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 var ValueDom = document .getElementById('screen' ); var Value = []; var State = "" ; var Signal = "" ; function clearFunction ( ) { ValueDom.innerHTML = '0' ; State = '' ; } function cleanFunction ( ) { ValueDom.innerHTML = '0' ; Value = []; Singal = '' ; State = '' ; } function cutFunction ( ) { let value = ValueDom.innerHTML; if (value == '0' ) { return } else if (value.length == 1 ) { ValueDom.innerHTML = '0' ; } else { ValueDom.innerHTML = value.slice(0 ,-1 ); } } function signalFunction (optionType ) { if (ValueDom.innerHTML == '0' ) return ; Value[0 ]=(ValueDom.innerHTML); ValueDom.innerHTML = '0' ; Signal = optionType; State = '' ; } function numFunction (num ) { let newValue; if (ValueDom.innerHTML == '0' ) { newValue = num; } else if (ValueDom.innerHTML != '0' ) { newValue = ValueDom.innerHTML + num; } ValueDom.innerHTML = newValue; } function stateFunction ( ) { if (State == '' ) { ValueDom.innerHTML = '-' +ValueDom.innerHTML; State = '-' ; } else if (State == '-' ) { ValueDom.innerHTML = ValueDom.innerHTML.slice(1 ); State = '' ; } } function pointFunction ( ) { ValueDom.innerHTML = ValueDom.innerHTML + '.' ; } function resultFunction ( ) { if (Value.length == 0 || Signal =='' ||ValueDom.innerHTML == '0' ) return ; let result = calc(parseFloat (Value[0 ]),parseFloat (ValueDom.innerHTML),Signal); if (result < 0 ) State = '-' ; ValueDom.innerHTML = result; } function calc (value1,value2,optionType ) { let result; switch (optionType) { case '+' : result = parseFloat ((value1*1000 + value2*1000 )/1000 ); break ; case '-' : result = parseFloat ((value1*1000 - value2*1000 )/1000 ); break ; case '%' : result = value1/value2; break ; case 'x' : result = value1*value2; break ; } return result; }
业务代码测试 单元测试框架:Mocha +Chai
1 2 $ yarn add mocha chai -D $ mocha init test
Mocha 会在 test 文件夹下创建几个文件
|— index.html // 执行测试和结果展示 |— mocha.css |— mocha.js |— index.test.js //测试代码
index.html 执行测试和结果展示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <!DOCTYPE html > <html > <head > <title > Mocha</title > <meta http-equiv ="Content-Type" content ="text/html; charset=UTF-8" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <link rel ="stylesheet" href ="mocha.css" /> </head > <body > <div id ="mocha" > </div > <script src ="mocha.js" > </script > <script > mocha.setup('bdd' ); </script > <script src ="../index.js" > </script > //被测试代码 <script src ="chai.js" > </script > // chai文件 <script src ="index.test.js" > </script > // 测试代码 <script > mocha.run(); </script > </body > </html >
1 2 3 4 5 6 var expect = chai.expect;describe('demo' , function ( ) { it('1+2' ,function ( ) { expect(calc(1 ,2 ,'+' )).to.equal(3 ); }) });
执行 index.js 文件中的 calc方法 expect 会捕获到放回结果,然后用 to.equal方法进行值比对。chai Api .ok — 判断目标值是否为正确
1 2 3 4 expect('everything' ).to.be.ok; expect(1 ).to.be.ok; expect(false ).to.not.be.ok; expect(undefined ).to.not.be.ok;
.true — 判断目标是否为true
1 2 expect(true ).to.be.true; expect(1 ).to.not.be.true;
.change(function) @param { String } object @param { String } property name @param { String } message _optional_ 目标函数是否改变对象某个属性值
1 2 3 4 5 var obj = { val : 10 };var fn = function ( ) { obj.val += 3 };var noChangeFn = function ( ) { return 'foo' + 'bar' ; }expect(fn).to.change(obj, 'val' ); expect(noChangeFn).to.not.change(obj, 'val' )
界面测试 selenium-webdriver 一个自动化测试库。 以一种更底层、更灵活的方式来操作浏览器,支持的框架、弹出窗口、页面导航、下拉菜单、基于AJAX的UI元素等控件的操作。它支持多种语言开发:java、ruby、php、javasrcript
yarn add selenium-webdriver -D
1 2 3 4 5 6 7 8 9 10 11 12 13 const webdriver = require ('selenium-webdriver' );const Capabilities = require ('selenium-webdriver/lib/capabilities' ).Capabilities;By = webdriver.By, until = webdriver.until; var capabilities = Capabilities.firefox();capabilities.set('marionette' , true ); var driver = new webdriver.Builder().withCapabilities(capabilities).build();driver.get('http://localhost:9000/' ); driver.findElement(By.xpath('//*[@id="keyboard"]/div[2]/button[1]' )).click(); var screenValue = driver.findElement(By.id('screen' ));driver.wait(until.elementTextIs(screenValue,'7' ),10 );
使用firebox浏览器进行模拟测试。 selenium-webdriver 提供了丰富的Api (元素定位(By),点击操作(click),比较元素值(until.elementTextIs)),可以真实的模拟用户操作。
selenium-webdriver 常用API:
webdriver 控制浏览器行为的对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 this .get(url) this .getTitle() var web = driver.get("http://www.google.com" ); web.then(()=> { let title = driver.getTitle(); title.then(s =>console .log(s)); }); this .quit() this .wait(condition,time,message) var button = driver.wait(until.elementLocated(By.id('foo' )), 10000 ); button.click(); this .findElement(locator) driver.findElements(By.id('hplogo' )) .then(found => console .log('Element found? %s' , !!found.length));
By 通过不同条件,获取元素在页面中的定位。
1 2 3 4 By.className(name) By.id(id) By.name(name) By.xpath(xpath)
until 提供验证结果的方法,方法返回的值可以做为WebDriver wait的条件,从而验证方法是否正确调用。
1 2 until.titleIs(title) until.elementTextIs(element, text)