前端測試框架Jest總結


很多前端開源框架都有對應的測試代碼,保證框架的穩定性

1.前端自動化測試產生的背景與原理

為了降低上線的bug,使用TypeScript,Flow, Eslint ,StyleLint這些工具可以實現。前端自動化測試工具普及情況不是很好。測試分為單元測試,集成測試和端到端測試。單元測試主要是對一個獨立的功能單元進行的測試,通過一個小例子來了解前端自動化測試的背景。

1.1 實例引入

新建文件夾,在目錄下新建index.html文件,math.js和math.test.js文件。

打開visual studio code編輯器,在index.html文件里輸入英文感嘆號!,然后輸入tab鍵,將自動生成標准的html代碼。

文件內容如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>math.js</title>
    <script src="math.js"></script>
</head>
<body>
    
</body>
</html>

小貼士:html文件運行的時候,可以使用Live Server插件,直接來運行。

在math.js中編寫數據庫的基本函數代碼,如下代碼,這時候我們將減法寫錯了,通過編寫測試代碼就可以發現這個問題。

function add(a, b) {
    return a + b;
}

function minus(a, b) {
    return a * b;
}

測試代碼寫在math.test.js文件中,如下代碼:

var result = add(3, 7);
var expected = 10;

if (result !== 10) {
    throw Error(`3+7應該等於${expected},但是結果卻是${result}`);
}


var result = min(3, 3);
var expected = 0;
if (result !== 0) {
    throw Error(`3-3應該等於${expected},但是結果卻是${result}`);
}

怎么運行上面測試代碼呢?

1.運用Live Server運行html文件,

2.在控件台中發現math.js中定義的方法都是全局的,

3.math.test.js文件中的代碼,復制到控制台,發現運行結果如下,

VM256:12 Uncaught Error: 3-3應該等於0,但是結果卻是9
    at <anonymous>:12:11

這樣自動化測試的代碼就可以發現minus方法有問題,寫錯了。將minus方法修改成正確的方法,執行上面步驟,發現已經沒有報錯了。

1.2 增加代碼

如果此時要在math.js文件中添加乘法的函數,代碼 如下

function add(a, b) {
    return a + b;
}

function minus(a, b) {
    return a - b;
}

function multiply(a, b) {
    return a * b;
}

再次在控制台中執行測試代碼,如果發現全部執行通過,沒有錯誤,說明新寫的代碼沒有影響之前的代碼;如果測試沒通過,就說明新寫的代碼影響了之前的代碼。

通過自動化測試可以很容易發現新增代碼對之前功能的印象。

1.3 代碼優化

那么我們能不能將寫的這些代碼簡化一下,封裝成公有函數呢?如下代碼所示,我們希望創建一種語法來表示函數執行結果和預期值比較的結果來做測試,比上面的if,else要好很多。

expect(add(3, 3)).toBe(6);
expect(minus(6, 3)).toBe(3);

接下來我們就實現這個方法,如下:

function expect(result) {
    return {
        toBe: function (actual) {
            if (result !== actual) {
                throw new Error('預期值和實際值不相等');
            }
        }
    }
}

expect(add(3, 7)).toBe(10);
expect(minus(3, 3)).toBe(0);

在瀏覽器的控制台中執行上面代碼,發現沒有什么錯誤;

這時,如果我們將minus函數方法中的“-”變成“+”,發現控制台中有錯誤出現了。如下所示,但是通過這樣的提示信息我們並不知道是哪個函數執行出錯了,所以我們再優化一下代碼。

VM570:5 Uncaught Error: 預期值和實際值不相等
at Object.toBe ( :5:23)
at :12:21

優化代碼如下:

function expect(result) {
    return {
        toBe: function (actual) {
            if (result !== actual) {
                throw new Error('預期值和實際值不相等');
            }
        }
    }
}

function test(desc, fn) {
    try {
        fn();
        console.log(`${desc}通過測試`);
    } catch (e) {
        console.log(`${desc}沒有通過測試`);
    }
}

test('測試加法3+7', () => {
    expect(add(3, 7)).toBe(10);
})

test('測試減法3-3', () => {
    expect(minus(3, 3)).toBe(0);
})

在控制台中執行上述代碼,發現控制台輸出內容如下:

測試加法3+7通過測試
測試減法3-3沒有通過測試

通過上面的方式,我們就可以很容易的知道哪個方法出錯了。繼續優化提示信息,將預期結果和實際結果也打印出來,代碼如下:

function expect(result) {
    return {
        toBe: function (actual) {
            if (result !== actual) {
                throw new Error(`預期值和實際值不相等,預期${actual}結果卻是${result}`);
            }
        }
    }
}

function test(desc, fn) {
    try {
        fn();
        console.log(`${desc}通過測試`);
    } catch (e) {
        console.log(`${desc}沒有通過測試 ${e}`);
    }
}

test('測試加法3+7', () => {
    expect(add(3, 7)).toBe(10);
})

test('測試減法3-3', () => {
    expect(minus(3, 3)).toBe(0);
})

控制台中執行代碼的結果如下:這樣提示信息就更加完善了。

測試加法3+7通過測試
測試減法3-3沒有通過測試 Error: 預期值和實際值不相等,預期0結果卻是6

通過這樣的方式我們就更清楚地知道了哪個函數有問題,然后去修改bug。有了這個測試函數,如果我們再想測試其他的函數,就輕松多了。

其實這個函數就是自動化測試框架Jest、Mocha、Jasmine框架的底層函數。理解了這個函數,理解自動化測試框架就會更容易理解。

2.前端自動化測試框架Jest

2.1 使用Jest修改自動化測試樣例

使用Jest項目必須要有npm的包,在項目目錄下運行npm init命令初始化項目的npm包。

通過下面命令安裝指定版本的jest包,保存在package.json文件中的devDependencies。因為只有在開發的時候我們才會運行測試用例,上線的時候就不會運行了。

npm install jest@24.8.0 -D

首先將math.test.js文件中自己定義的有關方法刪除掉,因為jest里面已經定義了test方法了,就不需要我們自己定義了。刪除之后的代碼如下:

test('測試加法3+7', () => {
    expect(add(3, 7)).toBe(10);
})

test('測試減法3-3', () => {
    expect(minus(3, 3)).toBe(0);
})

之前我們定義的方法都是全局函數,如果想通過jest來做自動化測試的話,必須使用模塊的形式把要測試的方法導出。使用commonJS的語法將其導出,math.test.js文件修改后的代碼如下:

function add(a, b) {
    return a + b;
}

function minus(a, b) {
    return a + b;
}

function multiply(a, b) {
    return a * b;
}

module.exports = {
    add,
    minus,
    multiply
}

在math.test.js文件中引入要測試方法,代碼如下:

const math = require('./math.js');
const {
    add,
    minus
} = math;

test('測試加法3+7', () => {
    expect(add(3, 7)).toBe(10);
})

test('測試減法3-3', () => {
    expect(minus(3, 3)).toBe(0);
})

接下來怎么運行math.test.js文件呢?修改package.json文件中的命令代碼,如下:

"scripts": {
    "test": "jest"
  },

這樣就可以通過運行npm run test命令來執行項目中的所有以test.js文件結尾的文件了。

npm run test

如下運行結果,就可以很清楚地看到執行測試用例時,哪些用例是成功的,哪些用例是失敗的。

 √ 測試加法3+7 (2ms)
  × 測試減法3-3 (2ms)

  ● 測試減法3-3

    expect(received).toBe(expected) // Object.is equality

    Expected: 0
    Received: 6

      10 | 
      11 | test('測試減法3-3', () => {
    > 12 |     expect(minus(3, 3)).toBe(0);
         |                         ^
      13 | })

      at Object.<anonymous> (math.test.js:12:25)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 passed, 2 total

是不是有個疑問,使用jest的時候一定要把函數導出呢?原因是Jest框架在前端自動化測中幫我們完成的是兩類內容,單元測試和集成測試,單元測試是測試一個模塊,是模塊測試,集成測試是測試多個模塊。所以使用jest的時候想測試這些內容,測試的內容一定會是模塊。符合Jest的模塊標准,jest才能幫助你完成測試。

在math.js文件中加入了模塊的相關內容后,瀏覽器運行就會報錯

Uncaught ReferenceError: module is not defined
at math.js:13

如何解決這個問題呢?修改代碼如下,瀏覽器中就不會報錯了。因為瀏覽器中會捕獲異常。

try {
    module.exports = {
        add,
        minus,
        multiply
    }
} catch (e) {

}

其實現在的react和vue框架都已經集成了模塊化的思想,所以實際我們並不需要通過捕獲異常的方式來處理這個問題,

2.2 Jest的簡單配置

2.1中我們沒有對Jest進行配置,也可以執行Jest,因為Jest本身就有一些默認配置。

有時,需要對Jest的默認配置進行修改,如何進行呢?首先需要把Jest的一些配置項暴露出來,執行下面命令:

npx jest --init

該命令表示調用目錄下面的nodeModule目錄下面的jest命令,進行初始化。如下所示

The following questions will help Jest to create a suitable configuration for your project

√ Choose the test environment that will be used for testing » jsdom (browser-like)
√ Do you want Jest to add coverage reports? ... yes
√ Automatically clear mock calls and instances between every test? ... yes

進行初始化選項選擇之后,目錄下面 生成了jest.config.js文件,這個文件是jest的配置文件,打開文件發現我們剛才配置的一些內容被注釋放開了。配置項里面有個配置項是coverageDirectory( coverageDirectory: "coverage",)項,這個是生成代碼覆蓋率的配置項。執行npx jest --coverage命令發現控制台會生成函數測試覆蓋的結果

npx jest --coverage

PASS ./math.test.js
√ 測試加法3+7 (2ms)
√ 測試減法3-3

----------|----------|----------|----------|----------|-------------------|

File % Stmts % Branch % Funcs % Lines Uncovered Line #s
All files 80 100 66.67 80
math.js 80 100 66.67 80 10
---------- ---------- ---------- ---------- ---------- -------------------

Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 2.986s
Ran all test suites.

於此同時發現,在項目目錄下面生成了coverage文件目錄,運行該目錄下的index.html文件,可以看到目錄下的測試代碼對功能代碼覆蓋的百分比。

npx jest --coverage命令如果不理解的話,也可以通過下面的配置通過npm命令(npm run coverage)生成代碼覆蓋率的結果

"scripts": {
    "test": "jest",
    "coverage": "jest --coverage"
  },

將jest配置文件中的配置項( coverageDirectory: "coverage",)修改成( coverageDirectory: "delle",),刪除生成的coverage文件目錄,執行npm命令(npm run coverage),發現目錄下面生成了delle文件夾。說明coverageDirectory配置的內容是生成的測試報告所在的文件夾名稱。

在es6中不會使用commonjs的方式對模塊進行導出,通常通過export和import的方式, 如何測試這種導出導入的方式呢?修改math.js文件代碼如下

export function add(a, b) {
    return a + b;
}

export function minus(a, b) {
    return a - b;
}

export function multiply(a, b) {
    return a * b;
}

修改math.test.js文件代碼如下:

import {
    add,
    minus
} from './math';

test('測試加法3+7', () => {
    expect(add(3, 7)).toBe(10);
})

test('測試減法3-3', () => {
    expect(minus(3, 3)).toBe(0);
})

進行npm run test命令之后發現報錯了,如下報錯

jest

FAIL ./math.test.js
● Test suite failed to run

D:\zdj\jest學習文件夾\Jest\lesson2\math.test.js:1
({"Object. ":function(module,exports,require,__dirname,__filename,global,jest){import { add, minus } from './math';
^^^^^^

SyntaxError: Cannot use import statement outside a module

at ScriptTransformer._transformAndBuildScript (node_modules/@jest/transform/build/ScriptTransformer.js:537:17)
at ScriptTransformer.transform (node_modules/@jest/transform/build/ScriptTransformer.js:579:25)

為什么會有上面的報錯呢?是因為運行Jest的時候,當前環境是node環境,不認識ESModule的內容,node下面是 不支持這種語法的?

怎么解決這個問題呢?使用babel進行裝換,將ESModule的代碼轉換成Commonjs的代碼,如何轉換呢?

安裝babel和babel-preset

npm install @babel/core@7.4.5
npm install @babel/preset-env@7.4.5 -D

要使用babel,必須要對babel進行一定的配置,在項目根目錄下新建.babelrc文件,文件內容如下:

{
    "presets": [
        [
            "@babel/preset-env", {
                "targets": {
                    "node": "current"
                }
            }
        ]
    ]
}

這樣就可以將ESModule的代碼轉換為支持node語法的代碼了。執行npm run test命令就不會報錯了。

之所以能運行,是因為執行npm run jest命令的時候,

jest內部集成了babel-jest插件,

該插件會檢查當前環境是否安裝了babel或babel-core,然后獲取.babelrc配置

在運行測試之前,結合babel,先把測試代碼進行一次轉化

運行轉化過的測試用例代碼

2.3 Jest中的匹配器

在目錄下新建matchers.test.js文件,文件內容如下:

test('測試', () => {
    expect(10).toBe(10);
})

當每次修改代碼的時候,都需要通過運行npm run test命令來執行測試用例。這樣比較麻煩,在package.json文件中修改命令如下,每次修改test文件的代碼,test文件就會被自動運行了。

  "scripts": {
    "test": "jest --watchAll",
    "coverage": "jest --coverage"
  },

代碼中的toBe就是一個匹配器(matchers),相當於===,如下代碼是常用的適配器

test('測試10和10', () => {
    //toBe匹配器,類似於Object.is ==="
    expect(10).toBe(10);
    //如果是一個對象用toBe就不會通過
    // const a = {
    //     one: 1
    // };
    // expect(a).toBe({
    //     one: 1
    // });

})

test('測試對象內容相等', () => {
    //toEqual匹配器,只是去匹配內容,這樣測試用例就通過了
    const a = {
        one: 1
    };
    expect(a).toEqual({
        one: 1
    });
})

test('測試toBeNull匹配器', () => {
    //toBeNull匹配器,測試用例通過
    const a = null;
    expect(a).toBeNull();
    //undefined和null不相等,測試用例不通過
    // const b = undefined;
    // expect(b).toBeNull()
})

test('測試undefined匹配器', () => {
    //undefined匹配器,測試用例通過
    const b = undefined;
    expect(b).toBeUndefined();

    //'',null和undefined不相等,測試用例不通過
    // const b = '';
    // expect(b).toBeUndefined();
})


//真假有關的匹配器
test('toBeDefined匹配器', () => {
    //a沒有被定義過,測試用例不通過
    // const a = undefined;
    // expect(a).toBeDefined();

    //a被定義過,測試用例通過
    const a = null;
    expect(a).toBeDefined();
})

//真假有關的匹配器
test('toBeTruthy', () => {
    //null,''和0在js里面是false,測試用例不通過
    // const a = null;
    // expect(a).toBeTruthy();

    // const b = '';
    // expect(b).toBeTruthy();

    // const b = 0;
    // expect(b).toBeTruthy();

    //1在js里面是真,測試用例通過
    const b = 1;
    expect(b).toBeTruthy();
})

//真假有關的匹配器
test('toBeFalsy', () => {
    // //1在js里面是真,測試用例不通過
    // const b = 1;
    // expect(b).toBeFalsy();

    // null,''和0在js里面是false,測試用例通過
    // const a = null;
    // expect(a).toBeFalsy();

    // const b = '';
    // expect(b).toBeFalsy();

    const b = 0;
    expect(b).toBeFalsy();
})

//toBeTruthy和toBeFalsy是取反的匹配器,還可以用not匹配器來對其他匹配器進行取反
test('not 匹配器', () => {
    const a = 1;
    expect(a).not.toBeFalsy();
});

除了true或者false的匹配器,還有數字相關的匹配器

//數字相關的匹配器
test('toBeGreaterThan匹配器', () => {
    // Expected: > 11
    // Received: 10
    //10比11小,測試用例不通過
    // const count = 10;;
    // expect(count).toBeGreaterThan(11);
});

test('toBeLessThan匹配器', () => {
    //10比11小,測試用例通過
    const count = 10;;
    expect(count).toBeLessThan(11);
});

test('toBeGreaterThanOrEqual匹配器', () => {
    //10比10相等,測試用例通過
    const count = 10;;
    expect(count).toBeGreaterThanOrEqual(10);
});

test('toBeCloseTo匹配器', () => {

    const firstNumber = 0.1;
    const secondNumber = 0.2
    //測試用例不通過,不能用toEqual來比較2個浮點數
    // Expected: 0.3
    // Received: 0.30000000000000004
    // expect(firstNumber + secondNumber).toEqual(0.3);
    // 測試用例通過
    expect(firstNumber + secondNumber).toBeCloseTo(0.3);
});

接下來介紹和字符串,數組,異常相關的匹配器

// 字符串相關的匹配器
test('toMatch', () => {
    const str = "http://www.dell-lee.com"
    //str字符串里面包含了'dell'字符串,測試用例通過
    expect(str).toMatch('dell');

    //不僅可以寫字符串,還可以寫正則表達式,測試用例通過
    expect(str).toMatch(/dell/);

    //str字符串里面不包含了'delllee'字符串,測試用例不通過
    // expect(str).toMatch(/delllee/);
});

//和Array,Set相關的匹配器
test('toContain', () => {
    const arr = ['dell', 'lee']
    //arr里面包含了'dell'字符串,測試用例通過
    expect(arr).toContain('dell');

    //arr里面不包含'dell'字符串,測試用例不通過
    // expect(arr).toContain('delle');

    // 將數組轉換為set,來進行測試
    const data = new Set(arr);
    //data里面包含了'dell'字符串,測試用例通過
    expect(data).toContain('dell');
});

//與異常相關的匹配器
const throwNewErrorFunc = () => {
    throw new Error('this is a new error');
}

test('toThrow', () => {
    //toThrow來判斷函數是否拋出了異常
    //函數拋出異常,測試用例通過
    expect(throwNewErrorFunc).toThrow();
    //測試拋出異常的內容是否一致
    expect(throwNewErrorFunc).toThrow('this is a new error');

    //函數拋出異常,測試用例不通過 
    // expect(throwNewErrorFunc).not.toThrow();
});

除了上面提到的匹配器,jest還提供了其他的匹配器。

2.4 Jest命令行工具的使用

上面代碼運行的時候,發現每次修改測試用例代碼的時候,所有的測試用例都被執行了一遍。這樣是比較麻煩的。執行npm run test命令的時候,會有一個Watch Usage用法,通過不同的配置執行不同的方法

Watch Usage
› Press f to run only failed tests.
› Press o to only run tests related to changed files.
› Press p to filter by a filename regex pattern.
› Press t to filter by a test name regex pattern.
› Press q to quit watch mode.
› Press Enter to trigger a test run.

當我們在執行npm run test命令之后,按住f每次只會執行失敗的測試用例,成功的測試用例會被略過,通過之后再修改,也不會執行用例了。再按f會退出f的模式,正常執行所有的測試用例。

當我們在執行npm run test命令之后,按住 o 每次只會執行和修改有關的測試用例,但是發現按下o之后執行出錯了,是因為jest不知道哪些文件被修改了,如果想知道哪些文件被修改了,需要使用git來管理我們的代碼。

接下來安裝git,安裝完git之后

在命令行窗口運行下面命令

git init  //初始化git
git add.  //將當前代碼存到倉庫里
git commit -m 'version1' //將當前代碼提交到本地倉庫里面
git status //可以看到代碼和提交代碼改動的地方
git checkout math.test.js //將修改撤回

在jest的配置中,我們配置的是

"scripts": {
"test": "jest --watchAll",
"coverage": "jest --coverage"
},

我們可以將watchAll修改成watch,這樣是和o模式的作用是相同的,jest每次都會運行和修改有關的測試用例。

 "scripts": {
    "test": "jest --watch",
    "coverage": "jest --coverage"
  },

t模式是通過正則表達式來過濾對應的測試用例。

進入t模式,會有一個pattern >輸入toMatch字符串就會執行與toMatch有關的測試用例(名稱)

2.5 異步代碼的測試方法

在項目目錄下新建文件fetchData.js,測試異步代碼需要使用axios,執行下面命令

npm install axios@0.19.0 --save

文件fetchData.js就可以使用axios調用異步接口了,文件內容如下:

import axios from "axios";

export const fetchData = (fn) => {
  //   {
  //     "success": true
  //   }
  axios.get("http://www.dell-lee.com/react/api/demo.json").then((response) => {
    fn(response.data);
  });
};

新建測試文件fetchData.test.js

import { fetchData } from "./fetchData";

test("fetchData 返回結果是{success:true}", () => {
  fetchData((data) => {
    expect(data).toEqual({
      success: true,
    });
  });
});

執行npm run test命令之后,發現測試用例通過,

將fetchData.js文件里面的demo.json變成demo1.json時,發現測試用例也能通過,但其實改成這樣之后,接口是不通的,會返回404。

為什么呢?是因為測試用例執行時,發現fetchData函數能夠成功地執行,測試用例就結束了,就不會走到fetchData里面的函數里面,就沒有執行expect函數了,執行時沒有等到回調函數執行結束才會認為測試用例執行結束。

當測試回調式的異步函數時,上面的寫法是有問題的,可以改成下面的代碼

import { fetchData } from "./fetchData";

test("fetchData 返回結果是{success:true}", (done) => {
  fetchData((data) => {
    expect(data).toEqual({
      success: true,
    });
    done();
  });
});
//給異步回調函數添加一個參數done,只有當done執行結束之后,異步回調函數才算執行結束。

接下來介紹另外一種異步函數

import axios from "axios";

// 回調類型異步函數
// export const fetchData = (fn) => {
//   //   {
//   //     "success": true
//   //   }
//   axios.get("http://www.dell-lee.com/react/api/demo.json").then((response) => {
//     fn(response.data);
//   });
// };

//異步函數的第2種形式
export const fetchData = (fn) => {
  //   {
  //     "success": true
  //   }
  return axios.get("http://www.dell-lee.com/react/api/demo2.json");
};

測試代碼如下:

import { fetchData } from "./fetchData";

// 回調類型異步函數的測試
test("fetchData 返回結果是{success:true}", (done) => {
  fetchData((data) => {
    expect(data).toEqual({
      success: true,
    });
    done();
  });
});

// 第2種異步函數的測試(返回promise對象的異步函數)
test("fetchData 返回結果是{success:true}", () => {
  //記得當fetchData函數返回的是promise對象的時候
  //和then方法做正確性測試的時候,一定要記得在前面使用return返回一下

  return fetchData().then((response) => {
    expect(response.data).toEqual({
      success: true,
    });
  });
});

// 第2種異步函數的測試(返回promise對象的異步函數)
test("fetchData 返回結果為404", () => {
  //記得當fetchData函數返回的是404的時候
  //expect.assertions(1)表示下面的expect語句至少執行一遍
  //如果不加expect.assertions(1)的話,當promise對象返回不是404的時候
  //下面語句執行也會返回true,因為根本沒有執行catch語句
  expect.assertions(1);
  return fetchData().catch((e) => {
    console.log(e.toString());
    expect(e.toString().indexOf("404") > -1).toBe(true);
  });
});

// 第2種異步函數的另外一種測試方法(返回promise對象的異步函數)
test("fetchData 返回結果是{success:true}", () => {
  //toMatchObject匹配器表示只要返回的對象包含toMatchObject里面的內容
  //就返回true,toMatchObject函數的參數是返回對象的子集

  return expect(fetchData()).resolves.toMatchObject({
    data: {
      success: true,
    },
  });
});

test("fetchData 返回結果為404", () => {
  //當請求可以調通的時候,測試用例不會通過,因為這是測試請求不通的情況,如下
  //Received promise resolved instead of rejected,

  //當請求不通的時候,測試用例可以通過
  //比上面的寫法更簡單
  return expect(fetchData()).rejects.toThrow();
});

// 第2種異步函數的第3種測試方法(返回promise對象的異步函數)
test("fetchData 返回結果是{success:true}", async () => {
  //用await代替return
  //要注意await要和async一起使用
  await expect(fetchData()).resolves.toMatchObject({
    data: {
      success: true,
    },
  });
});

test("fetchData 返回結果為404", async () => {
  //當請求可以調通的時候,測試用例不會通過,因為這是測試請求不通的情況,如下
  //Received promise resolved instead of rejected,

  //當請求不通的時候,測試用例可以通過
  //比上面的寫法更簡單
  await expect(fetchData()).rejects.toThrow();
});

// 第2種異步函數的第4種測試方法(返回promise對象的異步函數)
test("fetchData 返回結果是{success:true}", async () => {
  //用await代替return
  //要注意await要和async一起使用
  const response = await fetchData();
  expect(response.data).toEqual({
    success: true,
  });
});

test("fetchData 返回結果為404", async () => {
  //當請求不通的時候,測試用例可以通過
  expect.assertions(1);
  try {
    await fetchData();
  } catch (e) {
    console.log(e.toString());
    expect(e.toString()).toEqual("Error: Request failed with status code 404");
  }
});

關於異步函數的測試方法基本上就有這么多了。

2.6 Jest中的鈎子函數

在目錄下新建文件Counter.js和Counter.test.js,

Counter.js文件內容如下:

class Counter {
  constructor() {
    this.number = 0;
  }
  addOne() {
    this.number += 1;
  }
  minusOne() {
    this.number -= 1;
  }
}

export default Counter;

Counter.test.js文件內容如下:

import Counter from "./Counter";
//Jest中如果想要在測試用例執行之前,做一些准備

//創建Counter類的對象
const counter = new Counter();
test("測試Counter 中的addOne方法", () => {
  counter.addOne();
  expect(counter.number).toBe(1);
});

test("測試Counter 中的minusOne方法", () => {
  counter.minusOne();
  expect(counter.number).toBe(0);
});
//這2個測試用例中,counter使用的是同一個實例對象,因此這2個測試用例彼此受到影響。

如何解決上面的問題呢?使用鈎子函數來解決,是jest里面要對測試內容基礎化處理。Counter.test.js文件內容如下:

import Counter from "./Counter";
//Jest中如果想要在測試用例執行之前,做一些准備

//創建Counter類的對象
let counter = null;
beforeAll(() => {
  console.log("BeforeAll");
  //beforeAll是在所有測試用例執行之前被jest調用的函數
  counter = new Counter();
});

beforeEach(() => {
  //在每個測試用例執行之前,該函數都會被jest調用執行
  //這樣就可以解決多個測試用例互相影響的問題
  counter = new Counter();
});

afterEach(() => {
  //在每個測試用例執行之后,該函數都會被jest調用執行
  counter = new Counter();
});

afterAll(() => {
  //等待所有的測試用例執行結束之后會被jest調用的函數
});
test("測試Counter 中的addOne方法", () => {
  counter.addOne();
  expect(counter.number).toBe(1);
});

test("測試Counter 中的minusOne方法", () => {
  counter.minusOne();
  expect(counter.number).toBe(-1);
});

接下來給Counter.js再添加2個方法,如下代碼

class Counter {
  constructor() {
    this.number = 0;
  }
  addOne() {
    this.number += 1;
  }
  minusOne() {
    this.number -= 1;
  }

  addTwo() {
    this.number += 2;
  }

  minusTwo() {
    this.number -= 2;
  }
}

export default Counter;

Counter.test.js文件內容也要加上相應的方法測試內容,代碼如下:

import Counter from "./Counter";
//Jest中如果想要在測試用例執行之前,做一些准備

//創建Counter類的對象
let counter = null;
beforeAll(() => {
  console.log("BeforeAll");
  //beforeAll是在所有測試用例執行之前被jest調用的函數
  counter = new Counter();
});

beforeEach(() => {
  //在每個測試用例執行之前,該函數都會被jest調用執行
  //這樣就可以解決多個測試用例互相影響的問題
  counter = new Counter();
});

afterEach(() => {
  //在每個測試用例執行之后,該函數都會被jest調用執行
  counter = new Counter();
});

afterAll(() => {
  //等待所有的測試用例執行結束之后會被jest調用的函數
});
test("測試Counter 中的addOne方法", () => {
  counter.addOne();
  expect(counter.number).toBe(1);
});

test("測試Counter 中的minusOne方法", () => {
  counter.minusOne();
  expect(counter.number).toBe(-1);
});

test("測試Counter 中的addTwo方法", () => {
  counter.addTwo();
  expect(counter.number).toBe(2);
});

test("測試Counter 中的minusTwo方法", () => {
  counter.minusTwo();
  expect(counter.number).toBe(-2);
});

但是如果我們想讓加法相關的方法放到一起,減法相關的方法放到一起,這時,就會用到分組的概念,引入describe相關的概念,如下代碼,這樣測試代碼層次更加清晰

import Counter from "./Counter";

describe("測試Counter相關的代碼", () => {
  //Jest中如果想要在測試用例執行之前,做一些准備
  //創建Counter類的對象
  let counter = null;
  beforeAll(() => {
    console.log("BeforeAll");
    //beforeAll是在所有測試用例執行之前被jest調用的函數
    counter = new Counter();
  });

  beforeEach(() => {
    //在每個測試用例執行之前,該函數都會被jest調用執行
    //這樣就可以解決多個測試用例互相影響的問題
    counter = new Counter();
  });

  afterEach(() => {
    //在每個測試用例執行之后,該函數都會被jest調用執行
    counter = new Counter();
  });

  afterAll(() => {
    //等待所有的測試用例執行結束之后會被jest調用的函數
  });

  describe("測試增加相關的代碼", () => {
    test("測試Counter 中的addOne方法", () => {
      counter.addOne();
      expect(counter.number).toBe(1);
    });

    test("測試Counter 中的addTwo方法", () => {
      counter.addTwo();
      expect(counter.number).toBe(2);
    });
  });

  describe("測試減少相關的代碼", () => {
    test("測試Counter 中的minusOne方法", () => {
      counter.minusOne();
      expect(counter.number).toBe(-1);
    });

    test("測試Counter 中的minusTwo方法", () => {
      counter.minusTwo();
      expect(counter.number).toBe(-2);
    });
  });
});

執行上面的測試代碼,運行結果如下,這樣看起來就很清晰了。

PASS ./Counter.test.js
測試Counter相關的代碼
測試增加相關的代碼
√ 測試Counter 中的addOne方法 (3ms)
√ 測試Counter 中的addTwo方法 (1ms)
測試減少相關的代碼
√ 測試Counter 中的minusOne方法 (1ms)
√ 測試Counter 中的minusTwo方法 (1ms)

console.log Counter.test.js:8
BeforeAll

上面提到的BeforeAll等這些鈎子函數,其實可以寫在每一個describe函數里面,也就是說每個describe函數里面都可以定義這些鈎子函數,對其下面的每個測試用例(test函數)都生效的。

每次運行的時候,先執行外部的鈎子函數,再執行內部的鈎子函數。

在有很多測試用例執行的時候,我們很難發現用例中的問題,可以用test.only函數只執行這個測試用例,而不是執行其他的測試用例。

import Counter from "./Counter";

describe("測試Counter相關的代碼", () => {
  //Jest中如果想要在測試用例執行之前,做一些准備
  //創建Counter類的對象
  let counter = null;
  beforeAll(() => {
    console.log("BeforeAll");
    //beforeAll是在所有測試用例執行之前被jest調用的函數
    counter = new Counter();
  });

  beforeEach(() => {
    //在每個測試用例執行之前,該函數都會被jest調用執行
    //這樣就可以解決多個測試用例互相影響的問題
    counter = new Counter();
  });

  afterEach(() => {
    //在每個測試用例執行之后,該函數都會被jest調用執行
    counter = new Counter();
  });

  afterAll(() => {
    //等待所有的測試用例執行結束之后會被jest調用的函數
  });

  describe("測試增加相關的代碼", () => {
    test.only("測試Counter 中的addOne方法", () => {
      counter.addOne();
      expect(counter.number).toBe(1);
    });

    test("測試Counter 中的addTwo方法", () => {
      counter.addTwo();
      expect(counter.number).toBe(2);
    });
  });

  describe("測試減少相關的代碼", () => {
    test("測試Counter 中的minusOne方法", () => {
      counter.minusOne();
      expect(counter.number).toBe(-1);
    });

    test("測試Counter 中的minusTwo方法", () => {
      counter.minusTwo();
      expect(counter.number).toBe(-2);
    });
  });
});

執行結果如下:

PASS ./Counter.test.js
測試Counter相關的代碼
測試增加相關的代碼
√ 測試Counter 中的addOne方法 (4ms)
○ skipped 測試Counter 中的addTwo方法
測試減少相關的代碼
○ skipped 測試Counter 中的minusOne方法
○ skipped 測試Counter 中的minusTwo方法

准備型的代碼一定要鈎子函數中,而不要直接放在describe函數中,因為describe函數中的代碼會先被執行,然后再執行鈎子函數中的方法。

2.7 Jest中的Mock

在文件目錄下新增demo.js文件和demo.test.js文件

demo.js文件內容如下:

export const runCallback = (callback) => {
  callback("abc");
};

demo.test.js文件內容如下:

import { runCallback } from "./demo";

test("測試 runCallback", () => {
  //   const func = () => {
  //     return "hello";
  //   };
  //   //如果想要測試成功,就要在之前的函數代碼里面添加return
  //   //這樣就修改了之前的函數代碼
  //   expect(runCallback(func)).toBe("hello");

  //使用jest提供的mock函數來解決這個問題
  const func = jest.fn();
  //測試callback函數,如果在執行之后,func函數被調用
  //說明callback函數被成功執行了
  runCallback(func);
  expect(func).toBeCalled();
  //測試用例通過,說明func函數被調用
  //mock函數,可以捕獲函數的調用
  //   console.log(func.mock);
  //   {
  //     calls: [ [] ],
  //     instances: [ undefined ],
  //     invocationCallOrder: [ 1 ],
  //     results: [ { type: 'return', value: undefined } ]
  //   }
  //如果測試func被調用2次,可以通過calls屬性,[]里面是函數的參數,當修改callback函數的參數為'abc'時
  runCallback(func);
  //   console.log(func.mock);
  //   {
  //     calls: [ [ 'abc' ], [ 'abc' ] ],
  //     instances: [ undefined, undefined ],
  //     invocationCallOrder: [ 1, 2 ],
  //     results: [
  //       { type: 'return', value: undefined },
  //       { type: 'return', value: undefined }
  //     ]
  //   }
  //判斷函數調用的次數
  expect(func.mock.calls.length).toBe(2);
  //判斷函數執行的參數
  expect(func.mock.calls[0]).toEqual(["abc"]);
});

當我們想讓Mock的函數有返回值時候,可以采用下面的形式

import { runCallback } from "./demo";

test("測試 runCallback", () => {
  //   const func = () => {
  //     return "hello";
  //   };
  //   //如果想要測試成功,就要在之前的函數代碼里面添加return
  //   //這樣就修改了之前的函數代碼
  //   expect(runCallback(func)).toBe("hello");

  //使用jest提供的mock函數來解決這個問題
  const func = jest.fn(() => {
    return "456";
  });
  console.log(func.mock);
  //   {
  //     calls: [ [] ],
  //     instances: [ undefined ],
  //     invocationCallOrder: [ 1 ],
  //     results: [ { type: 'return', value: '456' } ]
  //   }
  
});

也可以采用下面的形式

import { runCallback } from "./demo";

test("測試 runCallback", () => {
  //使用jest提供的mock函數來解決這個問題
  //也可以使用下面的形式
  const func = jest.fn();
  //第1次調用返回值
  func.mockReturnValueOnce("Dell");
  //   //第2次調用返回值
  //   func.mockReturnValueOnce("Dell").mockReturnValueOnce("Dell");
  //   //每次調用返回值
  //   func.mockReturnValue("Dell");
  runCallback(func);
  console.log(func.mock);
  //測試用例通過
  expect(func.mock.results[0].value).toBe("Dell");
  
  //   {
  //     calls: [ [ 'abc' ] ],
  //     instances: [ undefined ],
  //     invocationCallOrder: [ 1 ],
  //     results: [ { type: 'return', value: 'Dell' } ]
  //   }
  runCallback(func);
  console.log(func.mock);
  //   {
  //     calls: [ [ 'abc' ], [ 'abc' ] ],
  //     instances: [ undefined, undefined ],
  //     invocationCallOrder: [ 1, 2 ],
  //     results: [
  //       { type: 'return', value: 'Dell' },
  //       { type: 'return', value: undefined }
  //     ]
  //   }
});

從mock函數打印的內容可以看出,invocationCallOrder屬性表示函數調用順序;還有一個屬性instances,表示mock函數中this的指向,接下來通過一個例子來看下這個屬性

demo.js文件中添加一個創建對象的函數,代碼如下:

export const runCallback = (callback) => {
  callback("abc");
};

export const createObject = (classItem) => {
  new classItem();
};

修改demo.test.js文件,來測試新增加的函數,文件內容如下

import { runCallback, createObject } from "./demo";

test.only("測試createObject函數", () => {
  const func = jest.fn();
  createObject(func);
  console.log(func.mock);
  //   {
  //     calls: [ [] ],
  //     instances: [ mockConstructor {} ],
  //     invocationCallOrder: [ 1 ],
  //     results: [ { type: 'return', value: undefined } ]
  //   }
  //從打印結果看出instances屬性里面有東西了,是mockConstructor,
  //因為傳入的是mock函數對象,this就指向mock的函數
  //因為mock函數是被當成構造函數去執行的,所以this指向mock的構造函數
  //而上面函數的測試用例中,mock函數執行時this指向undefined
});

上面的例子可以看出,

mock函數

  1. 可以捕獲函數的調用和返回結果,以及this指向和執行順序;
  2. 可以讓我們自由設置函數的返回結果
  3. 還可以改變內部函數的實現

接下來通過異步函數分析一下第3點:

demo.js文件內容如下:

import axios from "axios";

export const runCallback = (callback) => {
  callback("abc");
};

export const createObject = (classItem) => {
  new classItem();
};

export const getData = () => {
  return axios.get("/api").then((res) => res.data);
};

demo.test.js文件內容如下:

import { runCallback, createObject, getData } from "./demo";
import Axios from "axios";
jest.mock("axios");
//寫了上面的代碼,jest就不會去請求真正的數據了

test("測試 runCallback", () => {
  //   const func = () => {
  //     return "hello";
  //   };
  //   //如果想要測試成功,就要在之前的函數代碼里面添加return
  //   //這樣就修改了之前的函數代碼
  //   expect(runCallback(func)).toBe("hello");

  //使用jest提供的mock函數來解決這個問題
  //也可以使用下面的形式
  const func = jest.fn();
  //第1次調用返回值
  func.mockReturnValueOnce("Dell");
  //   //第2次調用返回值
  //   func.mockReturnValueOnce("Dell").mockReturnValueOnce("Dell");
  //   //每次調用返回值
  //   func.mockReturnValue("Dell");
  runCallback(func);
  console.log(func.mock);
  expect(func.mock.results[0].value).toBe("Dell");
  //也可以用下面的形式來確認函數調用的參數,和上面的是等價的
  //expect(func).toBeCalledWith("abc");
    
  //   {
  //     calls: [ [ 'abc' ] ],
  //     instances: [ undefined ],
  //     invocationCallOrder: [ 1 ],
  //     results: [ { type: 'return', value: 'Dell' } ]
  //   }
  runCallback(func);
  console.log(func.mock);
  //   {
  //     calls: [ [ 'abc' ], [ 'abc' ] ],
  //     instances: [ undefined, undefined ],
  //     invocationCallOrder: [ 1, 2 ],
  //     results: [
  //       { type: 'return', value: 'Dell' },
  //       { type: 'return', value: undefined }
  //     ]
  //   }
});

test("測試createObject函數", () => {
  const func = jest.fn();
  createObject(func);
  console.log(func.mock);
  //   {
  //     calls: [ [] ],
  //     instances: [ mockConstructor {} ],
  //     invocationCallOrder: [ 1 ],
  //     results: [ { type: 'return', value: undefined } ]
  //   }
  //從打印結果看出instances屬性里面有東西了,是mockConstructor,
  //因為傳入的是mock函數對象,this就指向mock的函數
  //因為mock函數是被當成構造函數去執行的
});

test.only("測試getData函數", async () => {
  //一般在真實的項目中,測試異步函數時不會真正發送ajax請求
  //模擬axios調用請求返回值
  //mock,第3個用處是改變函數的內部實現
  Axios.get.mockResolvedValue({ data: "hello" });
  //只模擬一次
  // Axios.get.mockResolvedValueOnce({ data: "hello" });
  await getData().then((data) => {
    expect(data).toBe("hello");
  });
});

mock函數還可以使用下面的形式來定義,mockImplementation函數來定義函數。

test("測試 runCallback", () => {
  //使用jest提供的mock函數來解決這個問題
  //也可以使用下面的形式
  const func = jest.fn();
  //   每次調用返回值
  //   func.mockReturnValue("Dell");
  //也可以用下面的方式來實現
  func.mockImplementation(() => {
    return "Dell";
  });

  //   func.mockImplementationOnce(() => {
  //     return "Dell";
  //   });

  runCallback(func);
  console.log(func.mock);
  //斷言
  expect(func.mock.results[0].value).toBe("Dell");
});

當mock函數返回this的時候,可以用下面的形式

import { runCallback, createObject, getData } from "./demo";
import Axios from "axios";
jest.mock("axios");
//寫了上面的代碼,jest就不會去請求真正的數據了

test("測試 runCallback", () => {
  //使用jest提供的mock函數來解決這個問題
  //也可以使用下面的形式
  const func = jest.fn();
  //希望函數做了一些事情,但最后返回this
  func.mockImplementation(() => {
    return this;
  });
  //mock函數返回的this是undefined的
  //上面的mock函數返回this相當於mockReturnThis函數
  //   func.mockReturnThis();
  runCallback(func);
  console.log(func.mock);
  //斷言
  expect(func.mock.results[0].value).toBeUndefined();
});

vs中安裝jest插件,該插件自動運行test文件,不用每次在命令行npm run start命令了,而且執行測試用例有問題,會在控制台中出現錯誤信息,非常方便。

3.Jest難點深入

3.1 snapshot功能測試

snapshot一般用於對配置文件做測試,在項目目錄下新建demo.js文件和demo.test.js文件

demo.js文件內容如下:

export const generateConfig = () => {
    return {
        server: 'http://localhost',
        port: 8080
    }
}

對demo.js文件的測試文件demo.test.js代碼如下:

import {
    generateConfig
} from './demo';


test('測試generateConfig函數', () => {
    //傳統寫法
    expect(generateConfig()).toEqual({
        server: 'http://localhost',
        port: 8080
    })
})

如果我們想增加配置項,往demo.js再添加配置項的時候,我們也要去修改對應的測試文件

如下代碼:

export const generateConfig = () => {
    return {
        server: 'http://localhost',
        port: 8080,
        domain: 'localhost'
    }
}

測試文件也要修改

import {
    generateConfig
} from './demo';


test('測試generateConfig函數', () => {
    //傳統寫法
    expect(generateConfig()).toEqual({
        server: 'http://localhost',
        port: 8080,
        domain: 'localhost'
    })
})

這樣太麻煩了,其實Jest提供了另外一種方式,toMatchSnapshot,如下的代碼所示:

toMatchSnapshot測試當前函數的執行結果和快照相等

當第一次運行的時候, 並沒有任何快照

第一次保存運行的時候, 會幫我們生成一個快照, 在目錄下多了一個_snapshots的文件夾

當再次執行代碼的時候, 生成一個新的snapshot,然后和老的snapshot做比較, 如果一樣的話, 測試用例就會通過,否則不會通過

toMatchSnapshot匹配器一般用於測試配置文件, 配置文件一般不會變

當修改了配置文件之后,測試用例就會報錯提示,這時提醒我們是否確認本次修改,確認之后再更新快照內容(或者執行之后按下u就可以更新快照)

import {
    generateConfig
} from './demo';


test('測試generateConfig函數', () => {
    //傳統寫法
    // expect(generateConfig()).toEqual({
    //     server: 'http://localhost',
    //     port: 8080,
    //     domain: 'localhost'
    // })

    expect(generateConfig()).toMatchSnapshot();
})

如果環境上有多個快照呢,這時候我們增加一個配置文件,demo.js文件內容如下:

export const generateConfig = () => {
    return {
        server: 'http://localhost',
        port: 8080,
        domain: 'localhost',
        time: '201'
    }
}

export const generateAnotherConfig = () => {
    return {
        server: 'http://localhost',
        port: 8080,
        domain: 'localhost',
        time: '2019'
    }
}

demo.test.js文件內容如下:

import {
    generateConfig,
    generateAnotherConfig
} from './demo';


test('測試generateConfig函數', () => {
    expect(generateConfig()).toMatchSnapshot();
})


test('測試generateAnotherConfig函數', () => {
    expect(generateAnotherConfig()).toMatchSnapshot();
})

執行npm run test命令之后,如果我們修改了多個配置文件的內容,按下u會更新所有的快照,如果我們想交互式確實每個快照的內容呢?可以按下i,一個一個逐一確認。

上面的配置文件里面time是寫死的,但如果它是動態生成的時間呢?如下代碼:

export const generateConfig = () => {
    return {
        server: 'http://localhost',
        port: 8080,
        domain: 'localhost',
        time: new Date()
    }
}

export const generateAnotherConfig = () => {
    return {
        server: 'http://localhost',
        port: 8080,
        domain: 'localhost',
        time: new Date()
    }
}

那我們每次保存快照更新之后,每次都不相同,下一次運行的結果和快照內容還不相同,怎么解決呢?如下代碼:

import {
    generateConfig,
    generateAnotherConfig
} from './demo';

test('測試generateConfig函數', () => {
    expect(generateConfig()).toMatchSnapshot({
        time: expect.any(Date)
        //Date也可以寫成Number,String之類的
    });
});

//上面代碼表示toMatchSnapshot里面的time字段匹配成任意的Date


test('測試generateAnotherConfig函數', () => {
    expect(generateConfig()).toMatchSnapshot({
        time: expect.any(Date)
    });
})

接下來嘗試使用行內的snapshot,在項目中安裝prettier,如下命令

npm install prettier@1.18.2 --save

將上面的demo.test.js文件中的toMatchSnapshot改成toMatchInlineSnapshot函數后,再次保存運行之后,會發現快照的內容沒有出現在snapshot文件夾,而是出現在demo.test.js文件中了,如下所示:

import {
    generateConfig,
    generateAnotherConfig
} from "./demo";

test("測試generateConfig函數", () => {
    expect(generateConfig()).toMatchInlineSnapshot({
            time: expect.any(Date)
            //Date也可以寫成Number,String之類的
        },
        `
    Object {
      "domain": "localhosts",
      "port": 8080,
      "server": "http://localhost",
      "time": Any<Date>,
    }
  `
    );
});

test("測試generateAnotherConfig函數", () => {
    expect(generateAnotherConfig()).toMatchInlineSnapshot({
            time: expect.any(Date)
        },
        `
    Object {
      "domain": "localhosts",
      "port": 8080,
      "server": "http://localhost",
      "time": Any<Date>,
    }
  `
    );
});

3.2 mock深入學習

在項目目錄下新建demo.js文件和demo.test.js文件,

demo.js文件代碼如下:

import axios from 'axios';

export const fetchData = () => {
    return axios.get('/').then(res => res.data);
}

//接口返回
// {
//     data: '(function(){retutrn '123 '})()'
// }

demo.test.js文件代碼如下,這種測試方法是2.7章節中提到的對axios進行模擬。

import {
    fetchData
} from './demo';
import Axios from 'axios';

jest.mock('axios');
test('fetchData測試', () => {
    Axios.get.mockResolvedValue({
        data: "(function(){return '123'})()"
    })
    return fetchData().then(data => {
        //用eval是因為返回的數據結果是字符串函數,需要執行一下字符串函數,得到函數執行返回的結果
        expect(eval(data)).toEqual('123');
    })
});

接下來我們介紹其他的模擬方式

可以在本地自己寫個函數來替代發送請求的函數,在同級目錄下(和demo.js同級目錄)創建文件夾__ mocks __,(在其下面創建demo.js文件來替代之前寫的demo.js文件),該目錄下的demo.js文件內容如下:

注意:這里的模擬的目錄只能在同級目錄,不在同級目錄就不會找到對應的模擬文件了。

//這樣異步代碼的測試就修改成同步代碼的測試了
export const fetchData = () => {
    return new Promise((resolved, reject) => {
        resolved(
            "(function(){return '123'})()"
        )
    })
}

demo.test.js文件里面就可以使用我們模擬文件下的文件里面的函數了,這樣遇到fetchData函數就不會使用之前demo.js文件里面的函數了,而是使用__ mocks __文件夾下面的demo.js文件里面的fetchData函數了。

jest.mock('./demo');
//讓jest模擬當前目錄下demo.js的內容,jest會自動去__mocks__目錄下去找模擬的demo.js內容

import {
    fetchData
} from './demo';

test('fetchData測試', () => {
    return fetchData().then(data => {
        //用eval是因為返回的數據結果是字符串函數,需要執行一下字符串函數,得到函數執行返回的結果
        expect(eval(data)).toEqual('123');
    })
});

jest不但提供了mock函數,還提供了unmock函數,

jest.mock('./demo');和 jest.unmock('./demo');

在Jest的config文件里面有個配置項,叫做automock,將其設置為automock:true之后,和jest.mock函數效果是一樣的

利用Jest.mock功能mock這個文件的時候有個問題,就是這個文件里面的有些函數需要模擬,有些函數不需要模擬,如何解決這個問題呢,看下面的例子,在demo.js文件添加一個函數

import axios from 'axios';

export const fetchData = () => {
    return axios.get('/').then(res => res.data);
}

export const getNumber = () => {
    return '123';
}

在demo.test.js文件里面添加測試代碼,如下所示:

jest.mock('./demo');
//讓jest模擬當前目錄下demo.js的內容,jest會自動去__mocks__目錄下去找模擬的demo.js內容

import {
    fetchData,
    getNumber
} from './demo';

test('fetchData測試', () => {
    return fetchData().then(data => {
        //用eval是因為返回的數據結果是字符串函數,需要執行一下字符串函數,得到函數執行返回的結果
        expect(eval(data)).toEqual('123');
    })
});

test('getNumber', () => {
    expect(getNumber()).toEqual('123');
});

運行代碼發現報錯了,這是因為我們模擬的demo文件里面並沒有getNumber()函數,怎么解決這個問題呢?

在寫測試代碼的時候,我們希望模擬異步函數,但是同步函數進不進行模擬了,同步函數就用之前的函數進行測試,如下代碼,使用真實的getNumber函數,使用jest.requireActual函數獲取真實的函數。

jest.mock('./demo');
//讓jest模擬當前目錄下demo.js的內容,jest會自動去__mocks__目錄下去找模擬的demo.js內容

import {
    fetchData
} from './demo';
const {
    getNumber
} = jest.requireActual('./demo');

test('fetchData測試', () => {
    return fetchData().then(data => {
        //用eval是因為返回的數據結果是字符串函數,需要執行一下字符串函數,得到函數執行返回的結果
        expect(eval(data)).toEqual('123');
    })
});

test('getNumber', () => {
    expect(getNumber()).toEqual('123');
});

3.3 mock timers

3.4 ES6中對類的測試

本小節通過對類的Mock理解單元測試和集成測試。

在根目錄下新建文件util.js文件和util.test.js文件,

util.js文件內容如下,它是一個工具類,里面的每個方法都比較復雜。

class Util {
    init() {
        //...復雜的邏輯
    }
    a() {
        //...復雜的邏輯

    }
    b() {
        //...復雜的邏輯

    }
}
export default Util;

對Util類中的方法進行測試,其實也比較簡單,如下util.test.js文件的代碼:

import Util from './util';

let util = null;
beforeAll(() => {
    util = new Util();
})
test('測試a方法', () => {
    // expect(util.a(1, 2)).toBr('12');

})

新建demo.js文件和demo.test.js文件,demo.js文件代碼如下:

import Util from './util';


const demoFunction = (a, b) => {
    const util = new Util();
    util.a(a);
    util.b(b);
}

export default demoFunction;

對demo.js文件進行測試:

執行demoFunction的時候,會創建一個Util實例,然后這個實例又去調用a方法和b方法

調用a方法和b方法的時候,時間比較長,會特別耗費性能,而且a和b的執行對測試來說是沒什么幫助的

我們只需要知道a方法和b方法執行過就可以了,不去執行原始的a方法和b方法,就可以節約性能

這里我們對Util類進行模擬

demo.test.js文件代碼如下:

jest.mock('./util');
//jest.mock函數發現util是一個類,jest會做一件事情,會自動將類中的構造函數和方法都自動地替換成jest.fn(),如下面的代碼所示
//通過jest.mock,我們就可以對函數進行追溯了 
// const Util = jest.fn();
// Util.a = jest.fn();
// Util.b = jest.fn();
// Util.init = jest.fn();

import Util from './util';
//為什么要import Util呢?因為demo.js里面運行的demoFunction函數里面的Util是從這個目錄引入的Util,mock的也是這個目錄下面的Util

import demoFunction from './demo';


test('測試demoFunction', () => {
    demoFunction();
    expect(Util).toHaveBeenCalled();
    console.log(Util.mock);

    //  {
    //      calls: [
    //          []
    //      ],
    //      instances: [Util {
    //          init: [Function],
    //          a: [Function],
    //          b: [Function]
    //      }],
    //      invocationCallOrder: [1],
    //      results: [{
    //          type: 'return',
    //          value: undefined
    //      }]
    //  }

    expect(Util.mock.instances[0].a).toHaveBeenCalled();
    expect(Util.mock.instances[0].b).toHaveBeenCalled();
})

從上面例子可以看出,關於Util類中a方法和b方法的測試應該放在對Util類的測試,對demo.js文件中的demoFunction的測試的重點不是方法a和方法b是如何執行的,a和b方法執行耗時,而且測試也不需要a和b都執行,而是a和b被執行過,所以我們通過mock方法的形式對a和b進行模擬。

單元測試是指對一個獨立的模塊單元進行的測試,而這個模塊依賴其他模塊的,我們並不關心,可以使用mock來提升測試性能。

集成測試不僅是對本單元,還對包含的單元統一進行的測試。

mock可以讓我們引入其他模塊變得更加簡單。

最后再說一個有關mock的模擬方法,上面代碼中的jest.mock函數主要是通過jest.fn函數進行的函數模擬

jest.mock('./util');

//jest.mock函數發現util是一個類,jest會做一件事情,會自動將類中的構造函數和方法都自動地替換成jest.fn(),如下面的代碼所示

//通過jest.mock,我們就可以對函數進行追溯了

// const Util = jest.fn();

// Util.a = jest.fn();

// Util.b = jest.fn();

// Util.init = jest.fn();

我們還可以自己實現Util類和Util類中的函數,在和util.js文件同級目錄下新建__ mock __目錄,然后在目錄下新建util.js文件,代碼內容如下;

const Util = jest.fn();
Util.prototype.a = jest.fn();
Util.prototype.b = jest.fn();
Util.prototype.init = jest.fn();

export default Util;

這樣代碼中的mock部分就由我們自己實現了,我們還可以自己去mock函數的實現,來實現mock函數的自定義,如下面的代碼所示,自定義Util類以及方法的實現。

const Util = jest.fn(() => {
    console.log('constuctor');
});
Util.prototype.a = jest.fn(() => {
    console.log('a');
});

Util.prototype.b = jest.fn();
Util.prototype.init = jest.fn();

export default Util;

還有一種寫法可以讓我們自定義mock函數的實現,這樣運行測試用例的時候,就是我們自己mock的util中的內容了,util.test.js文件代碼 如下所示:

jest.mock('./util', () => {
    const Util = jest.fn(() => {
        console.log('constuctor--');
    });
    Util.prototype.a = jest.fn(() => {
        console.log('a');
    });

    Util.prototype.b = jest.fn();
    Util.prototype.init = jest.fn();
    return Util;
});

import Util from './util';

import demoFunction from './demo';


test('測試demoFunction', () => {
    demoFunction();
    expect(Util).toHaveBeenCalled();

    expect(Util.mock.instances[0].a).toHaveBeenCalled();
    expect(Util.mock.instances[0].b).toHaveBeenCalled();
})

3.5 Jest中對DOM節點操作的測試

因為要對DOM節點做測試,需要安裝jquery,執行下面命令

npm install jquery --save

在根目錄下面新建demo.js和demo.test.js文件,demo.js代碼如下

import $ from 'jquery';

const addDivToBody = () => {
    $('body').append('<div/>');
}

export default addDivToBody;

demo.test.js文件代碼如下:

import addDivToBody from './demo';
import $ from 'jquery';


test('測試 addDivToBody', () => {
    addDivToBody();
    //打印輸出dom元素的長度
    // console.log($('body').find('div').length);
    //1
    expect($('body').find('div').length).toBe(1);
    addDivToBody();
    // console.log($('body').find('div').length);
    //2
    expect($('body').find('div').length).toBe(2);
})

上面代碼可以看出,jest可以通過jquery對dom元素做一些測試,為什么呢?

是因為jest運行的環境是node環境,

node環境不具備dom,

jest在node環境下自己模擬了一套dom的api,稱作jsDom

4.React中的TDD和單元測試

4.1 什么是TDD

TDD的開發流程:(Red-Green development)

1.編寫測試用例;

2.運行測試,測試用例無法通過測試;

3.編寫代碼,使測試用例通過測試

4.優化代碼,完成開發

5.重復上述步驟

TDD優勢

1.長期減少回歸bug;

2.代碼質量良好(組織,可維護性)

3.測試覆蓋率高,一般測試覆蓋率在80%,90%,不能做到100%

4.錯誤測試代碼不容易出現

接下來通過一個TodoList項目來了解TDD的流程


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM