<dfn id="hx5t3"><strike id="hx5t3"><em id="hx5t3"></em></strike></dfn>

    <thead id="hx5t3"></thead><nobr id="hx5t3"><font id="hx5t3"><rp id="hx5t3"></rp></font></nobr>

    <listing id="hx5t3"></listing>

    <var id="hx5t3"></var>
    <big id="hx5t3"></big>

      
      

      <output id="hx5t3"><ruby id="hx5t3"></ruby></output>
      <menuitem id="hx5t3"><dfn id="hx5t3"></dfn></menuitem>

      <big id="hx5t3"></big>

        帶你入門前端工程(四):測試

        譚光志

        什么是測試

        維基百科的定義:

        在規定的條件下對程序進行操作,以發現程序錯誤,衡量軟件質量,并對其是否能滿足設計要求進行評估的過程。

        也可以這樣理解:測試的作用是為了提高代碼質量和可維護性。

        1. 提高代碼質量:測試就是找 BUG,找出 BUG,然后解決它。BUG 少了,代碼質量自然就高了。
        2. 可維護性:對現有代碼進行修改、新增功能從而造成的成本越低,可維護性就越高。

        什么時候寫測試

        如果你的程序非常簡單,可以不用寫測試。例如下面的程序,功能簡單,只有十幾行代碼:

        function add(a, b) {
            return a + b
        }
        
        function sum(data = []) {
            let result = 0
            data.forEach(val => {
                result = add(result, val)
            })
        
            return result
        }
        
        console.log(sum([1,2,3,4,5,6,7,8,9,10])) // 55

        如果你的程序有數百行代碼,但封裝得很好,完美的踐行了模塊化的理念。每個模塊功能單一、代碼少,也可以不用寫測試。

        如果你的程序有成千上萬行代碼,數十個模塊,模塊與模塊之間的交互錯綜復雜。在這種情況下,就需要寫測試了。試想一下,在你對一個非常復雜的項目進行修改后,如果沒有測試會是什么情況?你需要將跟這次修改有關的每個功能都手動測一邊,以防止有 BUG 出現。但如果你寫了測試,只需執行一條命令就能知道結果,省時省力。

        測試類型與框架

        測試類型有很多種:單元測試、集成測試、白盒測試...

        測試框架也有很多種:Jest、Jasmine、LambdaTest...

        本章將只講解單元測試和 E2E 測試(end-to-end test 端到端測試)。其中單元測試使用的測試框架為 Jest,E2E 使用的測試框架為 Cypress。

        Jest

        安裝

        npm i -D jest

        打開 package.json 文件,在 scripts 下添加測試命令:

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

        然后在項目根目錄下新建 test 目錄,作為測試目錄。

        單元測試

        什么是單元測試?維基百科中給出的定義為:

        單元測試(英語:Unit Testing)又稱為模塊測試,是針對程序模塊(軟件設計的最小單位)來進行正確性檢驗的測試工作。

        從前端角度來看,單元測試就是對一個函數、一個組件、一個類做的測試,它針對的粒度比較小。

        單元測試應該怎么寫呢?

        1. 根據正確性寫測試,即正確的輸入應該有正常的結果。
        2. 根據錯誤性寫測試,即錯誤的輸入應該是錯誤的結果。

        對一個函數做測試

        例如一個取絕對值的函數 abs(),輸入 1,2,結果應該與輸入相同;輸入 -1,-2,結果應該與輸入相反。如果輸入非數字,例如 "abc",應該拋出一個類型錯誤。

        // main.js
        function abs(a) {
            if (typeof a != 'number') {
                throw new TypeError('參數必須為數值型')
            }
        
            if (a < 0) return -a
            return a
        }
        
        // test.spec.js
        test('abs', () => {
            expect(abs(1)).toBe(1)
            expect(abs(0)).toBe(0)
            expect(abs(-1)).toBe(1)
            expect(() => abs('abc')).toThrow(TypeError) // 類型錯誤
        })

        現在我們需要測試一下 abs() 函數:在 src 目錄新建一個 main.js 文件,在 test 目錄新建一個 test.spec.js 文件。然后將上面的兩個函數代碼寫入對應的文件,執行 npm run test,就可以看到測試效果了。

        對一個類做測試

        假設有這樣一個類:

        class Math {
            abs() {
        
            }
        
            sqrt() {
        
            }
        
            pow() {
        
            }
            ...
        }

        我們必須把這個類的所有方法都測一遍。

        test('Math.abs', () => {
            // ...
        })
        
        test('Math.sqrt', () => {
            // ...
        })
        
        test('Math.pow', () => {
            // ...
        })

        對一個組件做測試

        組件測試比較難,因為很多組件都涉及了 DOM 操作。

        例如一個上傳圖片組件,它有一個將圖片轉成 base64 碼的方法,那要怎么測試呢?一般測試都是跑在 node 環境下的,而 node 環境沒有 DOM 對象。

        我們先來回顧一下上傳圖片的過程:

        1. 點擊 <input type="file" />,選擇圖片上傳。
        2. 觸發 inputchange 事件,獲取 file 對象。
        3. FileReader 將圖片轉換成 base64 碼。

        這個過程和下面的代碼是一樣的:

        document.querySelector('input').onchange = function fileChangeHandler(e) {
            const file = e.target.files[0]
            const reader = new FileReader()
            reader.onload = (res) => {
                const fileResult = res.target.result
                console.log(fileResult) // 輸出 base64 碼
            }
        
            reader.readAsDataURL(file)
        }

        上面的代碼只是模擬,真實情況下應該是這樣使用:

        document.querySelector('input').onchange = function fileChangeHandler(e) {
            const file = e.target.files[0]
            tobase64(file)
        }
        
        function tobase64(file) {
            return new Promise((resolve, reject) => {
                const reader = new FileReader()
                reader.onload = (res) => {
                    const fileResult = res.target.result
                    resolve(fileResult) // 輸出 base64 碼
                }
        
                reader.readAsDataURL(file)
            })
        }

        可以看到,上面的代碼出現了 window 的事件對象 event、FileReader。也就是說,只要我們能夠提供這兩個對象,就可以在任何環境下運行它。所以我們可以在測試環境下加上這兩個對象:

        // 重寫 File
        window.File = function () {}
        
        // 重寫 FileReader
        window.FileReader = function () {
            this.readAsDataURL = function () {
                this.onload
                    && this.onload({
                        target: {
                            result: fileData,
                        },
                    })
            }
        }

        然后測試可以這樣寫:

        // 提前寫好文件內容
        const fileData = 'data:image/test'
        
        // 提供一個假的 file 對象給 tobase64() 函數
        function test() {
            const file = new File()
            const event = { target: { files: [file] } }
            file.type = 'image/png'
            file.name = 'test.png'
            file.size = 1024
        
            it('file content', (done) => {
                tobase64(file).then(base64 => {
                    expect(base64).toEqual(fileData) // 'data:image/test'
                    done()
                })
            })
        }
        
        // 執行測試
        test()

        通過這種 hack 的方式,我們就實現了對涉及 DOM 操作的組件的測試。我的 vue-upload-imgs 庫就是通過這種方式寫的單元測試,有興趣可以了解一下(測試文件放在 test 目錄)。

        測試覆蓋率

        什么是測試覆蓋率?用一個公式來表示:代碼覆蓋率 = 已執行的代碼數 / 代碼總數。Jest 如果要開啟測試覆蓋率統計,只需要在 Jest 命令后面加上 --coverage 參數:

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

        現在我們用剛才的測試用例再試一遍,看看測試覆蓋率。

        // main.js
        function abs(a) {
            if (typeof a != 'number') {
                throw new TypeError('參數必須為數值型')
            }
        
            if (a < 0) return -a
            return a
        }
        
        // test.spec.js
        test('abs', () => {
            expect(abs(1)).toBe(1)
            expect(abs(0)).toBe(0)
            expect(abs(-1)).toBe(1)
            expect(() => abs('abc')).toThrow(TypeError) // 類型錯誤
        })

        上圖表示每一項覆蓋率都是 100%。

        現在我們把測試類型錯誤的那一行代碼注釋掉,再試試。

        // test.spec.js
        test('abs', () => {
            expect(abs(1)).toBe(1)
            expect(abs(0)).toBe(0)
            expect(abs(-1)).toBe(1)
            // expect(() => abs('abc')).toThrow(TypeError)
        })

        可以看到測試覆蓋率下降了,為什么會這樣呢?因為 abs() 函數中判斷類型錯誤的那個分支的代碼沒有執行。

        // 就是這一個分支語句
        if (typeof a != 'number') {
            throw new TypeError('參數必須為數值型')
        }

        覆蓋率統計項

        從覆蓋率的圖片可以看到一共有 4 個統計項:

        1. Stmts(statements):語句覆蓋率,程序中的每個語句是否都已執行。
        2. Branch:分支覆蓋率,是否執行了每個分支。
        3. Funcs:函數覆蓋率,是否執行了每個函數。
        4. Lines:行覆蓋率,是否執行了每一行代碼。

        可能有人會有疑問,1 和 4 不是一樣嗎?其實不一樣,因為一行代碼可以包含好幾個語句。

        if (typeof a != 'number') {
            throw new TypeError('參數必須為數值型')
        }
        
        if (typeof a != 'number') throw new TypeError('參數必須為數值型')

        例如上面兩段代碼,它們對應的測試覆蓋率就不一樣?,F在把測試類型錯誤的那一行代碼注釋掉,再試試:

        // expect(() => abs('abc')).toThrow(TypeError)

        第一段代碼對應的覆蓋率

        第二段代碼對應的覆蓋率

        它們未執行的語句都是一樣,但第一段代碼 Lines 覆蓋率更低,因為它有一行代碼沒執行。而第二段代碼未執行的語句和判斷語句是在同一行,所以 Lines 覆蓋率為 100%。

        TDD 測試驅動開發

        TDD(Test-Driven Development) 就是根據需求提前把測試代碼寫好,然后根據測試代碼實現功能。

        TDD 的初衷是好的,但如果你的需求經常變(你懂的),那就不是一件好事了。很有可能你天天都在改測試代碼,業務代碼反而沒怎么動。

        所以 TDD 用不用還得取決于業務需求是否經常變更,以及你對需求是否有清晰的認識。

        E2E 測試

        端到端測試,主要是模擬用戶對頁面進行一系列操作并驗證其是否符合預期。本章將使用 Cypress 講解 E2E 測試。

        Cypress 在進行 E2E 測試時,會打開 Chrome 瀏覽器,然后根據測試代碼對頁面進行操作,就像一個正常的用戶在操作頁面一樣。

        安裝

        npm i -D cypress

        打開 package.json 文件,在 scripts 新增一條命令:

        "cypress": "cypress open"

        然后執行 npm run cypress 就可以打開 Cypress。首次打開會自動創建 Cypress 提供的默認測試腳本。

        點擊右邊的 Run 19 integration specs 就會開始執行測試。

        第一次測試

        打開 cypress 目錄,在 integration 目錄下新建一個 e2e.spec.js 測試文件:

        describe('The Home Page', () => {
            it('successfully loads', () => {
                cy.visit('http://localhost:8080')
            })
        })

        運行它,如無意外應該會看到一個測試失敗的提示。

        因為測試文件要求訪問 http://localhost:8080 服務器,但現在還沒有。所以我們需要使用 express 創建一個服務器,新建 server.js 文件,輸入以下代碼:

        // server.js
        const express = require('express')
        const app = express()
        const port = 8080
        
        app.get('/', (req, res) => {
            res.send('Hello World!')
        })
        
        app.listen(port, () => {
            console.log(`Example app listening at http://localhost:${port}`)
        })

        執行 node server.js,重新運行測試,這次就可以看到正確的結果了。

        PS: 如果你使用了 ESlint 來校驗代碼,則需要下載 eslint-plugin-cypress 插件,否則 Cypress 的全局命令會報錯。下載插件后,打開 .eslintrc 文件,在 plugins 選項中加上 cypress

        "plugins": [
            "cypress"
        ]

        模仿用戶登錄

        上一個測試實在是有點小兒科,這次我們來寫一個稍微復雜一點的測試,模仿用戶登錄:

        1. 用戶打開登錄頁 /login.html
        2. 輸入賬號密碼(都是 admin
        3. 登錄成功后,跳轉到 /index.html

        首先需要重寫服務器,修改一下 server.js 文件的代碼:

        // server.js
        const bodyParser = require('body-parser')
        const express = require('express')
        const app = express()
        const port = 8080
        app.use(express.static('public'))
        app.use(bodyParser.urlencoded({ extended: false }))
        app.use(bodyParser.json())
        
        app.post('/login', (req, res) => {
            const { account, password } = req.body
            // 由于沒有注冊功能,所以假定賬號密碼都為 admin
            if (account == 'admin' && password == 'admin') {
                res.send({
                    msg: '登錄成功',
                    code: 0,
                })
            } else {
                res.send({
                    msg: '登錄失敗,請輸入正確的賬號密碼',
                    code: 1,
                })
            }
        })
        
        app.listen(port, () => {
            console.log(`Example app listening at http://localhost:${port}`)
        })

        由于沒有注冊功能,所以暫時在后端寫死賬號密碼為 admin。然后新建兩個 html 文件:login.htmlindex.html,放在 public 目錄。

        <!-- login.html  -->
        <!DOCTYPE html>
        <html lang="en">
        <head>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <title>login</title>
            <style>
                div {
                    text-align: center;
                }
        
                button {
                    display: inline-block;
                    line-height: 1;
                    white-space: nowrap;
                    cursor: pointer;
                    text-align: center;
                    box-sizing: border-box;
                    outline: none;
                    margin: 0;
                    transition: 0.1s;
                    font-weight: 500;
                    padding: 12px 20px;
                    font-size: 14px;
                    border-radius: 4px;
                    color: #fff;
                    background-color: #409eff;
                    border-color: #409eff;
                    border: 0;
                }
        
                button:active {
                    background: #3a8ee6;
                    border-color: #3a8ee6;
                    color: #fff;
                }
        
                input {
                    display: block;
                    margin: auto;
                    margin-bottom: 10px;
                    -webkit-appearance: none;
                    background-color: #fff;
                    background-image: none;
                    border-radius: 4px;
                    border: 1px solid #dcdfe6;
                    box-sizing: border-box;
                    color: #606266;
                    font-size: inherit;
                    height: 40px;
                    line-height: 40px;
                    outline: none;
                    padding: 0 15px;
                    transition: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
                }
              </style>
        </head>
        <body>
            <div>
                <input type="text" placeholder="請輸入賬號" class="account">
                <input type="password" placeholder="請輸入密碼" class="password">
                <button>登錄</button>
            </div>
            <script src="https://cdn.bootcdn.net/ajax/libs/axios/0.21.0/axios.min.js"></script>
            <script>
                document.querySelector('button').onclick = () => {
                    axios.post('/login', {
                        account: document.querySelector('.account').value,
                        password: document.querySelector('.password').value,
                    })
                    .then(res => {
                        if (res.data.code == 0) {
                            location.href = '/index.html'
                        } else {
                            alert(res.data.msg)
                        }
                    })
                }
            </script>
        </body>
        </html>
        <!-- index.html  -->
        <!DOCTYPE html>
        <html lang="en">
        <head>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <title>index</title>
        </head>
        <body>
            Hello World!
        </body>
        </html>

        login.html 靜態頁

        index.html 靜態頁

        然后把測試文件內容改一下:

        describe('The Home Page', () => {
            it('login', () => {
                cy.visit('http://localhost:8080/login.html')
                // 輸入賬號密碼
                cy.get('.account').type('admin')
                cy.get('.password').type('admin')
        
                cy.get('button').click()  
                // 重定向到 /index
                cy.url().should('include', 'http://localhost:8080/index.html')
        
                // 斷言 index.html 頁面是包含 Hello World! 文本
                cy.get('body').should('contain', 'Hello World!')
            })
        })

        現在重新運行服務器 node server.js,再執行 npm run cypress,點擊右邊的 Run... 開始測試。

        測試結果正確。為了統一腳本的使用規范,最好將 node server.js 命令替換為 npm run start

        "scripts": {
            "test": "jest --coverage test/",
            "lint": "eslint --ext .js test/ src/",
            "start": "node server.js",
            "cypress": "cypress open"
         }

        小結

        本章所有的測試用例都可以在我的 github 上找到,建議把項目克隆下來,親自運行一遍。

        參考資料

        帶你入門前端工程 全文目錄:

        1. 技術選型:如何進行技術選型?
        2. 統一規范:如何制訂規范并利用工具保證規范被嚴格執行?
        3. 前端組件化:什么是模塊化、組件化?
        4. 測試:如何寫單元測試和 E2E(端到端) 測試?
        5. 構建工具:構建工具有哪些?都有哪些功能和優勢?
        6. 自動化部署:如何利用 Jenkins、Github Actions 自動化部署項目?
        7. 前端監控:講解前端監控原理及如何利用 sentry 對項目實行監控。
        8. 性能優化(一):如何檢測網站性能?有哪些實用的性能優化規則?
        9. 性能優化(二):如何檢測網站性能?有哪些實用的性能優化規則?
        10. 重構:為什么做重構?重構有哪些手法?
        11. 微服務:微服務是什么?如何搭建微服務項目?
        12. Severless:Severless 是什么?如何使用 Severless?
        閱讀 614
        5.6k 聲望
        10.3k 粉絲
        0 條評論
        你知道嗎?

        5.6k 聲望
        10.3k 粉絲
        宣傳欄
        一本到在线是免费观看_亚洲2020天天堂在线观看_国产欧美亚洲精品第一页_最好看的2018中文字幕