[TOC]
## 為什么要寫測試?
Web應用程序越來越復雜,這意味著有更多的可能出錯。測試是幫助我們提高代碼質量、降低錯誤的最好方法和工具之一。
* 測試可以確保得到預期結果。
* 加快開發速度。
* 方便維護。
* 提供用法的文檔。
## 測試的類型
### 單元測試
單元測試(unit testing)指的是以軟件的單元為單位,對軟件進行測試。單元(unit)可以是一個函數,也可以是一個模塊或組件。它的基本特征就是,只要輸入不變,必定返回同樣的輸出。
單元測試這個詞,本身就暗示,軟件應該以模塊化結構存在。每個模塊的運作,是獨立于其他模塊的。一個軟件越容易寫單元測試,往往暗示著它的模塊化結構越好,各模塊之間的耦合就越弱;越難寫單元測試,或者每次單元測試,不得不模擬大量的外部條件,很可能暗示軟件的模塊化結構越差,模塊之間存在較強的耦合。
單元測試的要求是,每個模塊都必須有單元測試,而軟件由模塊組成。
單元測試通常采取斷言(assertion)的形式,也就是測試某個功能的返回結果,是否與預期結果一致。如果與預期不一致,就表示測試失敗。
單元測試是函數正常工作、不出錯的最基本、最有效的方法之一。 每一個單元測試發出一個特定的輸入到所要測試的函數,看看函數是否返回預期的輸出,或者采取了預期的行動。單元測試證明了所測試的代碼行為符合預期。
單元測試有助于代碼的模塊化,因此有助于長期的重用。因為有了測試,你就知道代碼是可靠的,可以按照預期運行。從這個角度說,測試可以節省開發時間。單元測試的另一個好處是,有了測試,就等于就有了代碼功能的文檔,有助于其他開發者了解代碼的意圖。
單元測試應該避免依賴性問題,比如不存取數據庫、不訪問網絡等等,而是使用工具虛擬出運行環境。這種虛擬使得測試成本最小化,不用花大力氣搭建各種測試環境。
單元測試的步驟
* 準備所有的測試條件
* 調用(觸發)所要測試的函數
* 驗證運行結果是否正確
* 還原被修改的記錄
### 集成測試
集成測試(Integration test)指的是多個部分在一起測試,比如在一個測試數據庫上,測試數據庫連接模塊。
### 功能測試
功能測試(Functional test)指的是,自動測試整個應用程序的某個功能,比如使用Selenium工具自動打開瀏覽器運行程序。
## 開發模式
### TDD
TDD是“測試驅動的開發”(Test-Driven Development)的簡稱,指的是先寫好測試,然后再根據測試完成開發。使用這種開發方式,會有很高的測試覆蓋率。
TDD的開發步驟如下。
* 先寫一個測試。
* 寫出最小數量的代碼,使其能夠通過測試。
* 優化代碼。
* 重復前面三步。
TDD開發的測試覆蓋率通常在90%以上,這意味著維護代碼和新增特性會非常容易。因為測試保證了你可以信任這些代碼,修改它們不會破壞其他代碼的運行。
TDD接口提供以下四個方法。
* suite()
* test()
* setup()
* teardown()
下面代碼是測試計數器是否加1。
~~~
suite('Counter', function() {
test('tick increases count to 1', function() {
var counter = new Counter();
counter.tick();
assert.equal(counter.count, 1);
});
});
~~~
### BDD
BDD是“行為驅動的開發”(Behavior-Driven Development)的簡稱,指的是寫出優秀測試的最佳實踐的總稱。
BDD認為,不應該針對代碼的實現細節寫測試,而是要針對行為寫測試。BDD測試的是行為,即軟件應該怎樣運行。
BDD接口提供以下四個方法。
* describe()
* it()
* before()
* after()
* beforeEach()
* afterEach()
下面是測試計數器是否加1的BDD寫法。
~~~
describe('Counter', function() {
it('should increase count by 1 after calling tick', function() {
var counter = new Counter();
var expectedCount = counter.count + 1;
counter.tick();
assert.equal(counter.count, expectedCount);
});
});
~~~
## 斷言
斷言是判斷實際值與預期值是否相等的工具。
斷言有assert、expext、should三種風格,或者稱為三種寫法。
~~~
// assert風格
assert.equal(event.detail.item, '(item).);
// expect風格
expect(event.detail.item).to.equal('(item)');
// should風格
event.detail.item.should.equal('(item)');
~~~
Chai.js是一個很流行的斷言庫,同時支持上面三種風格。
(1) assert風格
~~~
var assert = require('chai').assert;
var foo = 'bar';
var beverages = { tea: [ 'chai', 'matcha', 'oolong' ] };
assert.typeOf(foo, 'string', 'foo is a string');
assert.equal(foo, 'bar', 'foo equal `bar`');
assert.lengthOf(foo, 3, 'foo`s value has a length of 3');
assert.lengthOf(beverages.tea, 3, 'beverages has 3 types of tea');
~~~
上面代碼中,assert方法的最后一個參數是錯誤提示信息,只有測試沒有通過時,才會顯示。
(2)expect風格
~~~
var expect = require('chai').expect;
var foo = 'bar';
var beverages = { tea: [ 'chai', 'matcha', 'oolong' ] };
expect(foo).to.be.a('string');
expect(foo).to.equal('bar');
expect(foo).to.have.length(3);
expect(beverages).to.have.property('tea').with.length(3);
~~~
(3)should風格
~~~
var should = require('chai').should();
var foo = 'bar';
var beverages = { tea: [ 'chai', 'matcha', 'oolong' ] };
foo.should.be.a('string');
foo.should.equal('bar');
foo.should.have.length(3);
beverages.should.have.property('tea').with.length(3);
~~~
## Mocha.js
Mocha是一個測試框架,也就是運行測試的工具。它使用下面的命令安裝。
~~~
$ npm install -g mocha
~~~
Mocha自身不帶斷言庫,所以還需要安裝一個斷言庫,這里選用Chai.js。
~~~
$ npm install -g chai
~~~
Mocha默認執行test目錄的腳本文件,所以我們將所有測試腳本放在test子目錄。
Mocha允許指定測試腳本文件,可以使用通配符,同時指定多個文件。
~~~
$ mocha --reporter spec spec/{my,awesome}.js
$ mocha --ui tdd test/unit/*.js etc
~~~
如果希望測試非存放于test子目錄的測試用例,可以在test子目錄中新建Mocha的配置文件mocha.opts。在該文件中寫入以下內容。
~~~
server-tests
~~~
上面代碼指定Mocha默認測試server-tests子目錄的測試腳本。
~~~
server-tests
--recursive
~~~
上面代碼中,--recursive參數指定同時運行子目錄中的測試用例腳本。
report參數用于指定Mocha的報告格式。
~~~
$ mocha --reporter spec server-test/*.js
~~~
上面代碼指定報告格式是spec。
grep參數用于搜索測試用例的名稱(即it方法的第一個參數),然后只執行匹配的測試用例。
~~~
$ mocha --reporter spec --grep "Fnord:" server-test/*.js
~~~
上面代碼只測試名稱中包含“Fnord:”的測試用例。
invert參數表示只運行不符合條件的測試腳本。
~~~
$ mocha --grep auth --invert
~~~
測試腳本中,describe方法和it方法都允許調用only方法,表示只運行某個測試套件或測試用例。
~~~
describe("using only", function() {
it.only("this is the only test to be run", function() {
});
it("this is not run", function() {
});
});
~~~
上面代碼中,只有第一個測試用例會運行。
describe方法和it方法還可以調用skip方法,表示跳過指定的測試套件或測試用例。
~~~
describe("using only", function() {
it.skip("this is the only test to be run", function() {
});
it("this is not run", function() {
});
});
~~~
上面代碼中,只有第二個測試用例會執行。
如果測試用例包含異步操作,可以done方法顯式指定測試用例的運行結束時間。
~~~
it('logs a', function(done) {
var f = function(){
console.log('logs a');
done();
};
setTimeout(f, 500);
});
~~~
上面代碼中,正常情況下,函數f還沒有執行,Mocha就已經結束運行了。為了保證Mocha等到測試用例跑完再結束運行,可以手動調用done方法
## WebDriver
WebDriver是一個瀏覽器的自動化框架。它在各種瀏覽器的基礎上,提供一個統一接口,將接收到的指令轉為瀏覽器的原生指令,驅動瀏覽器。
WebDriver由Selenium項目演變而來。Selenium是一個測試自動化框架,它的1.0版叫做Selenium RC,通過一個代理服務器,將測試腳本轉為JavaScript腳本,注入不同的瀏覽器,再由瀏覽器執行這些腳本后返回結果。WebDriver就是Selenium 2.0,它對每個瀏覽器提供一個驅動,測試腳本通過驅動轉換為瀏覽器原生命令,在瀏覽器中執行。
### 定制測試環境
DesiredCapabilities對象用于定制測試環境。
* 定制DesiredCapabilities對象的各個屬性
* 創建DesiredCapabilities實例
* 將DesiredCapabilities實例作為參數,新建一個WebDriver實例
### 操作瀏覽器的方法
WebDriver提供以下方法操作瀏覽器。
close():退出或關閉當前瀏覽器窗口。
~~~
driver.close();
~~~
quit():關閉所有瀏覽器窗口,中止當前瀏覽器driver和session。
~~~
driver.quit();
~~~
getTitle():返回當前網頁的標題。
~~~
driver.getTitle();
~~~
getCurrentUrl():返回當前網頁的網址。
~~~
driver.getCurrentUrl();
~~~
getPageSource():返回當前網頁的源碼。
~~~
// 斷言是否含有指定文本
assert(driver.getPageSource().contains("Hello World"),
"預期含有文本Hello World");
~~~
click():模擬鼠標點擊。
~~~
// 例一
driver.findElement(By.locatorType("path"))
.click();
// 例二
driver.get("https://www.google.com");
driver.findElement(By.name("q"))
.sendKeys("webDriver");
driver.findElement(By.id("sblsbb"))
.click();
~~~
clear():清空文本輸入框。
~~~
// 例一
driver.findElement(By.locatorType("path")).clear();
// 例二
driver.get("https://www.google.com");
driver.findElement(By.name("q"))
.sendKeys("webDriver");
driver.findElement(By.name("q"))
.clear();
driver.findElement(By.name("q"))
.sendKeys("testing");
~~~
sendKeys():在文本輸入框輸入文本。
~~~
driver.findElement(By.locatorType("path"))
.sendKeys("your text");
~~~
submit():提交表單,或者用來模擬按下回車鍵。
~~~
// 例一
driver.findElement(By.locatorType("path"))
.submit();
// 例二
driver.get("https://www.google.com");
driver.findElement(By.name("q"))
.sendKeys("webdriver");
element.submit();
~~~
findElement():返回選中的第一個元素。
~~~
driver.get("https://www.google.com");
driver.findElement(By.id("lst-ib"));
~~~
findElements():返回選中的所有元素(0個或多個)。
~~~
// 例一
driver.findElement(By.id("searchbox"))
.sendKeys("webdriver");
driver.findElements(By.xpath("//div[3]/ul/li"))
.get(0)
.click();
// 例二
driver.findElements(By.tagName("select"))
.get(0)
.findElements(By.tagName("option"))
.get(3)
.click()
.get(4)
.click()
.get(5)
.click();
// 例三:獲取頁面所有鏈接
var links = driver
.get("https://www.google.com")
.findElements(By.tagName("a"));
var linkSize = links.size();
var linksSrc = [];
console.log(linkSize);
for(var i=0;i<linkSize;i++) {
? linksSrc[i] = links.get(i).getAttribute("href");
}
for(int i=0;i<linkSize;i++){
? driver.navigate().to(linksSrc[i]);
? Thread.sleep(3000);
}
~~~
可以使用`size()`,查看到底選中了多少個元素。
### 網頁元素的定位
WebDriver提供8種定位器,用于定位網頁元素。
* By.id:HTML元素的id屬性
* By.name:HTML元素的name屬性
* By.xpath:使用XPath語法選中HTML元素
* By.cssSelector:使用CSS選擇器語法
* By.className:HTML元素的class屬性
* By.linkText:鏈接文本(只用于選中鏈接)
* By.tagName:HTML元素的標簽名
* By.partialLinkText:部分鏈接文本(只用于選中鏈接)
下面是一個使用id定位器,選中網頁元素的例子。
~~~
driver.findElement(By.id("sblsbb")).click();
~~~
### 網頁元素的方法
以下方法屬于網頁元素的方法,而不是webDriver實例的方法。需要注意的是,有些方法是某些元素特有的,比如只有文本框才能輸入文字。如果在網頁元素上調用不支持的方法,WebDriver不會報錯,也不會給出給出任何提示,只會靜靜地忽略。
getAttribute():返回網頁元素指定屬性的值。
~~~
driver.get("https://www.google.com");
driver.findElement(By.xpath("//div[@id='lst-ib']"))
.getAttribute("class");
~~~
getText():返回網頁元素的內部文本。
~~~
driver.findElement(By.locatorType("path")).getText();
~~~
getTagName():返回指定元素的標簽名。
~~~
driver.get("https://www.google.com");
driver.findElement(By.xpath("//div[@class='sbib_b']"))
.getTagName();
~~~
isDisplayed():返回一個布爾值,表示元素是否可見。
~~~
driver.get("https://www.google.com");
assert(driver.findElement(By.name("q"))
.isDisplayed(),
'搜索框應該可選擇');
~~~
isEnabled():返回一個布爾值,表示文本框是否可編輯。
~~~
driver.get("https://www.google.com");
var Element = driver.findElement(By.name("q"));
if (Element.isEnabled()) {
? driver.findElement(By.name("q"))
.sendKeys("Selenium Essentials");
} else {
? throw new Error();
}
~~~
isSelected():返回一個布爾值,表示一個元素是否可選擇。
~~~
driver.findElement(By.xpath("//select[@name='jump']/option[1]"))
.isSelected()
~~~
getSize():返回一個網頁元素的寬度和高度。
~~~
var dimensions=driver.findElement(By.locatorType("path"))
.getSize();?
dimensions.width;
dimensions.height;
~~~
getLocation():返回網頁元素左上角的x坐標和y坐標。
~~~
var point = driver.findElement(By.locatorType("path")).getLocation();
point.x; // 等同于 point.getX();
point.y; // 等同于 point.getY();
~~~
getCssValue():返回網頁元素指定的CSS屬性的值。
~~~
driver.get("https://www.google.com");
var element = driver.findElement(By.xpath("//div[@id='hplogo']"));
console.log(element.getCssValue("font-size"));
console.log(element.getCssValue("font-weight"));
console.log(element.getCssValue("color"));
console.log(element.getCssValue("background-size"));
~~~
### 頁面跳轉的方法
以下方法用來跳轉到某一個頁面。
get():要求瀏覽器跳到某個網址。
~~~
driver.get("URL");
~~~
navigate().back():瀏覽器回退。
~~~
driver.navigate().back();
~~~
navigate().forward():瀏覽器前進。
~~~
driver.navigate().forward();
~~~
navigate().to():跳轉到瀏覽器歷史中的某個頁面。
~~~
driver.navigate().to("URL");
~~~
navigate().refresh():刷新當前頁面。
~~~
driver.navigate().refresh();
// 等同于
driver.navigate()
.to(driver.getCurrentUrl());
// 等同于
driver.findElement(By.locatorType("path"))
.sendKeys(Keys.F5);
~~~
### cookie的方法
getCookies():獲取cookie
~~~
driver.get("https://www.google.com");
driver.manage().getCookies();
~~~
getCookieNamed() :返回指定名稱的cookie。
~~~
driver.get("https://www.google.com");
console.log(driver.manage().getCookieNamed("NID"));
~~~
addCookie():將cookie加入當前頁面。
~~~
driver.get("https://www.google.com");
driver.manage().addCookie(cookie0);
~~~
deleteCookie():刪除指定的cookie。
~~~
driver.get("https://www.google.co.in");
driver.manage().deleteCookieNamed("NID");
~~~
### 瀏覽器窗口的方法
maximize():最大化瀏覽器窗口。
~~~
var driver = new FirefoxDriver();
driver.manage().window().maximize();
~~~
getSize():返回瀏覽器窗口、圖像、網頁元素的寬和高。
~~~
driver.manage().window().getSize();
~~~
getPosition():返回瀏覽器窗口左上角的x坐標和y坐標。
~~~
console.log("Position X: " + driver.manage().window().getPosition().x);
console.log("Position Y: " + driver.manage().window().getPosition().y);
console.log("Position X: " + driver.manage().window().getPosition().getX());
console.log("Position Y: " + driver.manage().window().getPosition().getY());
~~~
setSize():定制瀏覽器窗口的大小。
~~~
var d = new Dimension(320, 480);
driver.manage().window().setSize(d);
driver.manage().window().setSize(new Dimension(320, 480));
~~~
setPosition():移動瀏覽器左上角到指定位置。
~~~
var p = new Point(200, 200);
driver.manage().window().setPosition(p);
driver.manage().window().setPosition(new Point(300, 150));
~~~
getWindowHandle():返回當前瀏覽器窗口。
~~~
var parentwindow = driver.getWindowHandle();
driver.switchTo().window(parentwindow);
~~~
getWindowHandles():返回所有瀏覽器窗口。
~~~
var childwindows =? driver.getWindowHandles();
driver.switchTo().window(childwindow);
~~~
switchTo.window():在瀏覽器窗口之間切換。
~~~
driver.SwitchTo().Window(childwindow);
driver.close();
driver.SwitchTo().Window(parentWindow);
~~~
### 彈出窗口
以下方法處理瀏覽器的彈出窗口。
dismiss() :關閉彈出窗口。
~~~
var alert = driver.switchTo().alert();
alert.dismiss();
~~~
accept():接受彈出窗口,相當于按下接受OK按鈕。
~~~
var alert = driver.switchTo().alert();
alert.accept();
~~~
getText():返回彈出窗口的文本值。
~~~
var alert = driver.switchTo().alert();
alert.getText();
~~~
sendKeys():向彈出窗口發送文本字符串。
~~~
var alert = driver.switchTo().alert();
alert.sendKeys("Text to be passed");
~~~
authenticateUsing():處理HTTP認證。
~~~
var user = new UserAndPassword("USERNAME", "PASSWORD");
alert.authenticateUsing(user);
~~~
### 鼠標和鍵盤的方法
以下方法模擬鼠標和鍵盤的動作。
* click():鼠標在當前位置點擊。
* clickAndHold():按下鼠標不放
* contextClick():右擊鼠標
* doubleClick():雙擊鼠標
* dragAndDrop():鼠標拖放到目標元素
* dragAndDropBy():鼠標拖放到目標坐標
* keyDown():按下某個鍵
* keyUp():從按下狀態釋放某個鍵
* moveByOffset():移動鼠標到另一個坐標位置
* moveToElement():移動鼠標到另一個網頁元素
* release():釋放拖拉的元素
* sendKeys():控制鍵盤輸出
- 第一章 導論
- 1.1 前言
- 1.2 為什么學習JavaScript?
- 1.3 JavaScript的歷史
- 第二章 基本語法
- 2.1 語法概述
- 2.2 數值
- 2.3 字符串
- 2.4 對象
- 2.5 數組
- 2.6 函數
- 2.7 運算符
- 2.8 數據類型轉換
- 2.9 錯誤處理機制
- 2.10 JavaScript 編程風格
- 第三章 標準庫
- 3.1 Object對象
- 3.2 Array 對象
- 3.3 包裝對象和Boolean對象
- 3.4 Number對象
- 3.5 String對象
- 3.6 Math對象
- 3.7 Date對象
- 3.8 RegExp對象
- 3.9 JSON對象
- 3.10 ArrayBuffer:類型化數組
- 第四章 面向對象編程
- 4.1 概述
- 4.2 封裝
- 4.3 繼承
- 4.4 模塊化編程
- 第五章 DOM
- 5.1 Node節點
- 5.2 document節點
- 5.3 Element對象
- 5.4 Text節點和DocumentFragment節點
- 5.5 Event對象
- 5.6 CSS操作
- 5.7 Mutation Observer
- 第六章 瀏覽器對象
- 6.1 瀏覽器的JavaScript引擎
- 6.2 定時器
- 6.3 window對象
- 6.4 history對象
- 6.5 Ajax
- 6.6 同域限制和window.postMessage方法
- 6.7 Web Storage:瀏覽器端數據儲存機制
- 6.8 IndexedDB:瀏覽器端數據庫
- 6.9 Web Notifications API
- 6.10 Performance API
- 6.11 移動設備API
- 第七章 HTML網頁的API
- 7.1 HTML網頁元素
- 7.2 Canvas API
- 7.3 SVG 圖像
- 7.4 表單
- 7.5 文件和二進制數據的操作
- 7.6 Web Worker
- 7.7 SSE:服務器發送事件
- 7.8 Page Visibility API
- 7.9 Fullscreen API:全屏操作
- 7.10 Web Speech
- 7.11 requestAnimationFrame
- 7.12 WebSocket
- 7.13 WebRTC
- 7.14 Web Components
- 第八章 開發工具
- 8.1 console對象
- 8.2 PhantomJS
- 8.3 Bower:客戶端庫管理工具
- 8.4 Grunt:任務自動管理工具
- 8.5 Gulp:任務自動管理工具
- 8.6 Browserify:瀏覽器加載Node.js模塊
- 8.7 RequireJS和AMD規范
- 8.8 Source Map
- 8.9 JavaScript 程序測試
- 第九章 JavaScript高級語法
- 9.1 Promise對象
- 9.2 有限狀態機
- 9.3 MVC框架與Backbone.js
- 9.4 嚴格模式
- 9.5 ECMAScript 6 介紹
- 附錄
- 10.1 JavaScript API列表
- 草稿一:函數庫
- 11.1 Underscore.js
- 11.2 Modernizr
- 11.3 Datejs
- 11.4 D3.js
- 11.5 設計模式
- 11.6 排序算法
- 草稿二:jQuery
- 12.1 jQuery概述
- 12.2 jQuery工具方法
- 12.3 jQuery插件開發
- 12.4 jQuery.Deferred對象
- 12.5 如何做到 jQuery-free?
- 草稿三:Node.js
- 13.1 Node.js 概述
- 13.2 CommonJS規范
- 13.3 package.json文件
- 13.4 npm模塊管理器
- 13.5 fs 模塊
- 13.6 Path模塊
- 13.7 process對象
- 13.8 Buffer對象
- 13.9 Events模塊
- 13.10 stream接口
- 13.11 Child Process模塊
- 13.12 Http模塊
- 13.13 assert 模塊
- 13.14 Cluster模塊
- 13.15 os模塊
- 13.16 Net模塊和DNS模塊
- 13.17 Express框架
- 13.18 Koa 框架