---
# System prepended metadata

title: Unit Test (單元測試)

---

# Unit Test (單元測試) -T
[toc]

- 藉由檢查每一個 function 回傳的物件，可以確定程式是不是有跑在預期的結果裡面
- 可以避免之後改版不小心改動到原本正常的 function
- 主要是測試 function 的邏輯
    - 固定 data 是否可以得到相同輸出
- 3A
    - Arrange
        - 準備測試會用到的 object
    - Account
        - 用 object 做一些邏輯得出結果
    - Assert
        - 確認結果是否和預期一樣
- SUT(System Under Test)
    - 待測的系統，通常是 class
- 每個 function 個別測試
    - 若有 function 中用了另一個 function (ex. call library)，就兩個 function 分開測試
        - 降低耦合 : 避免測試 A function 時呼叫 B function，但 B function 是有問題的
        - mock vs stub vs spy
            - stub
                - 當要驗證的 function 需要第三方的回傳資料（沒有帶參數）
                    - ex. call db data
                - 就製作假資料取代第三方的回傳資料
                - 實際方法
                    - 可以用物件導向的方式，把會使用到的第三方物件用建構子的方式帶入 class
                    - 測試時再將帶入的第三方物件改為自己設定的
                        - ![](https://hackmd.io/_uploads/Hyr93P8gp.png)
                        - ![](https://hackmd.io/_uploads/Sk9unP8gp.png)

            - mock
                - 當要驗證的 function 需要引入第三方 function 並依照驗證 function <b>帶入的參數</b>做事情
                - 偽造第三方 function 並檢查帶入第三方物件的參數是否正確（檢查互動的方式是否正確）
                - 最後<b>是檢查第三方 function</b> 的資料是否正確
                    - ex. call function 寫入 log 檔
                    - ![](https://hackmd.io/_uploads/H19cEoDfp.png)
                        - 原本的第三方物件
                    - ![](https://hackmd.io/_uploads/HJtUfFLeT.png)
                        - 偽造假的第三方物件
                        - 偽造的要可以回傳需驗證 function 的 value
            - stub vs mock
                - 差別在於驗證原本的物件 or 假的第三方物件
                - ![](https://hackmd.io/_uploads/ryRifFIxT.png)
                - ref
                    - https://hackmd.io/@AlienHackMd/HycK5pEpo
                    - https://ithelp.ithome.com.tw/articles/10263479
            - spy
                - 驗證原本物件對其他物件的改變是否正確
                - ![](https://hackmd.io/_uploads/BJ-PIKIgp.png)
                - ref
                    - https://medium.com/starbugs/unit-test-%E4%B8%AD%E7%9A%84%E6%9B%BF%E8%BA%AB-%E6%90%9E%E4%B8%8D%E6%B8%85%E6%A5%9A%E7%9A%84dummy-stub-spy-mock-fake-94be192d5c46
            - 總結
                - stub
                    - 偽造 SUT 所需的第三方資料
                    - 驗證整個 SUT 的回傳結果
                - mock
                    - 偽造 SUT 傳入參數的第三方模組
                    - 驗證偽造的第三方模組收到的資料
                - spy
                    - 驗證被 SUT 改動到的模組被改動後的資料
- NodeJS
    - repo : 可用 `mocha` + `supertest` + `chai` 
        - `index.js`
            ```js=
            process.env.NTBA_FIX_319 = 1;
            const express = require('express')
            const app = express()
            const http = require('http');
            const server = http.createServer(app);

            // 要測試的 api

            app.get('/test', async function (req, res) {
              res.json({suc:'test suc'});
              res.end;
              return;
            }); 

            app.get('/test/good', async function (req, res) {
              res.json({suc:'test good'});
              res.end;
              return;
            }); 

            server.listen(3001, () => {
              console.log('listening on *:3001');
            });

            module.exports = app // 這邊要 export 給 test 的 script
            ```
        - `test.js`
            ```js=
            // supertest 可以讓專案不用另外跑起來也可以測試 api，它會幫你跑
            const request = require('supertest')
            const app = require('./index')
            const { expect } = require("chai")

            // dedscribe 為一個測試區塊，通常裡面會有多個 it，代表實際要測試的更小的測試區塊
            describe('GET /test', () => {
              it('should respond "test good"', (done) => {
                request(app)
                  .get('/test/good')
                  .end((err, res) => {
                    const text = res.text;
                    // chai 的語法，讓我們比較好描述要測試的東西
                    expect(text).to.be.equal('{"suc":"test good"}')
                    done();
                  })
              }),
              it('should respond "test suc"', (done) => {
                request(app)
                  .get('/test')
                  .expect(200, '{"suc":"test suc"}', done)
              })
            })
            ```
            - chai 用法
                - ![](https://hackmd.io/_uploads/HJ5LY1-f6.png)
        - `package.json` 要加上啟動 test
            ```json=
            "scripts": {
              "start": "node index.js",
              "test": "mocha test --exit "
            }
            ```
            - `--exit` : 測試完就結束 script，最好都要加
        - 開始測試 : `npm test`
            - ![](https://hackmd.io/_uploads/ByRuX1WfT.png)
    - gitlab ci
        - `.gitlab-ci.yaml`
            ```yml=
            image: node:latest

            stages:
              - build
              - test
              - deploy

            default:
              tags:
                - linux
              before_script:
                - now_time=$(date)
                - echo "${now_time}"
                - echo "started by ${GITLAB_USER_NAME}"
                - chmod 0777 ./node_modules/.bin/mocha

            build-job:
              stage: build
              script:
                - echo "running scripts in the build job"
                - npm install

            test-job1:
              stage: test
              script:
                - echo "running scripts in the test job 1"
                - npm test
              dependencies:
                - build-job

            test-job2:
              stage: test
              script:
                - echo "running scripts in the test job 2"

            deploy-job:
              stage: deploy
              script:
                - echo "running scripts in the deploy job"
            ```
        - pipeline
            - ![](https://hackmd.io/_uploads/rkkV81bzp.png)
        - test-job1
            - ![](https://hackmd.io/_uploads/HyYLUkbfT.png)
    - ref
        - https://medium.com/@stupidcoding/%E5%9C%A8node-js%E5%AF%AB%E6%B8%AC%E8%A9%A6-mocha-chai%E6%96%B7%E8%A8%80%E5%BA%AB-supertest%E6%A8%A1%E6%93%AC%E9%80%A3%E7%B7%9A-sinon%E6%9B%BF%E8%BA%AB-nyc%E7%B5%B1%E8%A8%88%E8%A6%86%E8%93%8B%E7%8E%87-f736c423b893
        - https://medium.com/cubemail88/node-js-%E7%94%A8-mocha-%E5%81%9A%E5%96%AE%E5%85%83%E6%B8%AC%E8%A9%A6-16dd9125e632

### 整合測試
- 整合和其他模組間的測試
- ex. db 和 web application

### end-to-end 測試
- 以客戶角度對整個系統測試
- 可以是人工也可以自動

### CI/CD
- Continuous Integration (持續整合) 與 Continuous Deployment (持續部署)
- CI
    - 系統 環境安裝 + 單元測試
- CD
    - 系統 環境安裝 + 單元測試 + 自動部屬
- 作用
    - push code 之後將剩下作業自動化
    - 通常包含：建置（安裝環境）、測試、部屬
- 優點
    - 自動化單元測試、部屬
    - 可以快速更新新版本（有小問題可以馬上更新），減少環境安裝、部屬時間
    - 降低操作錯誤性
- 流程通常會分 3 個 `stage` （也可以有多個）
    1. `build`
        - 環境安裝
        - 也可以省略此直接用該環境的 image
            - ex. `image: lorisleiva/laravel-docker:latest`
    2. `test`
        - 單元測試
        - 利用 `dependencies` 取得 `build` 安裝的環境
            - 所以可以測試不同環境（ex. php5, php6）
    3. `deploy`
        - 部屬到正式或測試環境
    - 只要有其中一個有問題，就不會往下執行
        - 建置成功才跑測試，測試成功才做佈署，這樣的先後關係在 GitLab CI 稱之為 `pipeline`
        - ![](https://hackmd.io/_uploads/r1N5PNIlT.png)
        - ![](https://hackmd.io/_uploads/B1lpJplMT.png)
    - 如果不分 `stage` 就會由上而下執行且不管上面有沒有成功下面的還是會執行
- `.yml`
    - 實際在哪個 stage 要執行那些指令是以 `job` 為單位
        - 每個 `job` 都是獨立的 container
        - 所以如果要 `job` 間要共用一些環境，就要用 `dependencies`
    - `runner` : `job` 的執行器
        - 預設每個 job 的 runner 各自獨立
        - 可以用 gitlab 上提供的，也可以用自己架的
            - 自己架
                - 要提供 ssh 金鑰登入
            - gitlab 上提供的 shared runners
                - settings -> CI/CD
                    - ![](https://hackmd.io/_uploads/r1lYRhgfa.png)
                - runners -> expand
                    - gitlab 提供的 runner, 每個 runner 都有 tag, 去區分不同環境的 container
                        - ![](https://hackmd.io/_uploads/ryDMJaDMa.png)
                        - ![](https://hackmd.io/_uploads/rk7rkaDzp.png)
    - `tag` : 指定哪一種類的 runner
    - `only` : 指定 branch
        - 通常 `deploy` stage 會指定 master branch（只有 master branch 的 code 會被 deploy 到正式環境）
    - `before_script` : `job` 執行 `script` 前會執行的指令
    - `script` : `job` 真正執行的指令
    - ex.
        - ![](https://hackmd.io/_uploads/HyrVtrIl6.png)
            - https://ithelp.ithome.com.tw/articles/10187654
        - ![](https://hackmd.io/_uploads/HkfHKH8xT.png)
            - https://nick-chen.medium.com/%E6%95%99%E5%AD%B8-gitlab-ci-%E5%85%A5%E9%96%80%E5%AF%A6%E4%BD%9C-%E8%87%AA%E5%8B%95%E5%8C%96%E9%83%A8%E7%BD%B2%E7%AF%87-ci-cd-%E7%B3%BB%E5%88%97%E5%88%86%E4%BA%AB%E6%96%87-cbb5100a73d4
        ```yml=
        stages:
          - build
          - test
          - deploy

        default:
          tags:
            - windows
          before_script:
            - Set-Variable -Name "time" -Value (date -Format "%H:%m")
            - echo ${time}
            - echo "started by ${GITLAB_USER_NAME}"

        build-job:
          stage: build
          script:
            - echo "running scripts in the build job"

        test-job1:
          stage: test
          script:
            - echo "running scripts in the test job 1"

        test-job2:
          stage: test
          script:
            - echo "running scripts in the test job 2"

        deploy-job:
          stage: deploy
          script:
            - echo "running scripts in the deploy job"
        ```
        - https://editor.leonh.space/2022/gitlab-ci/
    - 查看此次 commit 的 pipeline
        1. 點進 repo 的 branch
        2. Build -> Pipelines （此 repo 的所有 branch 的 pipeline 都會在這）
            - ![](https://hackmd.io/_uploads/r1D7xU8g6.png)
    - 查看單一 pipeline 中各別 job 執行狀況
        1. 點進 repo 的 branch
        2. Build -> Jobs
            - ![](https://hackmd.io/_uploads/BydFRSUga.png)
            - 點進 job 看 log
                - ![](https://hackmd.io/_uploads/SJXuRBLeT.png)
    - ex. nodejs
        ```yml=
        image: node:latest
        stages:
          - build
          - test
          - deploy

        default:
          tags:
            - linux
          before_script:
            - now_time=$(date)
            - echo now_time
            - echo "started by ${GITLAB_USER_NAME}"

        build-job:
          stage: build
          script:
            - echo "running scripts in the build job"
            - npm install
            - node index.js

        test-job1:
          stage: test
          script:
            - echo "running scripts in the test job 1"
            - curl 127.0.0.1:3001
          dependencies:
            - build_job

        test-job2:
          stage: test
          script:
            - echo "running scripts in the test job 2"

        deploy-job:
          stage: deploy
          script:
            - echo "running scripts in the deploy job"
        ```
        - ref
            - https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Nodejs.gitlab-ci.yml
