# Cypress初探指南
Crypress為一前端自動化測試框架,使用javascript撰寫。
## Getting Started
### 參考網站
---
:::info
* [官方文件](https://docs.cypress.io/)
* [鐵人賽](https://ithelp.ithome.com.tw/users/20140883/ironman/4316)
:::
### 安裝工具
---
:::info
- [VS Code](https://code.visualstudio.com/)
- [Cypress Recorder](https://chrome.google.com/webstore/detail/cypress-recorder/glcapdcacdfkokcmicllhcjigeodacab)
:::
### Gitlab
---
:::info
* [cypress-e2e-test專案](https://web-gitlab.internal.ihmi.net/weincloud/cypress-e2e-test)
:::
### 環境建立
---
- npm版本12 or 14 或以上
:::info
* [系統要求參考網址](https://docs.cypress.io/guides/getting-started/installing-cypress#System-requirements)
* [npm下載連結](https://nodejs.org/en/download/)
:::
### 檔案目錄結構
---
cypress
├── /downloads 下載檔案暫存目錄
├── /e2e
│ └── /partner partner 測試案例
│ └── /weincloud weincloud 測試案例
├── /fixtures 靜態測試假資料
├── /reports 測試結果報告
├── /screenshots 錯誤畫面載圖
├── /scripts 自定義 script function
├── /support 自定義 command
├── /videos 執行過程影片
├── cypress.config.js cypress 設定檔及自定義 event handler
### 啟動project
---
1. 下載cypress-e2e-test project
2. 在project根目錄下terminal執行
```
npm install
```
3. 開啟GUI選擇要執行的測試案例
```
npm run cy:open
```
4. 或執行npm run cy:run執行所有測試案例
```
npm run cy:run
```
5. Command Line測試,並將測試結果上傳到Dashboard
Windows:
```
set CYPRESS_API_URL="http://192.168.2.44:1234/"
npx cy2 run --browser chrome --record --key XXX --parallel --ci-build-id %date:~0,4%-%date:~5,2%-%date:~8,2%_%time:~0,2%:%time:~3,2%:%time:~6,2%
```
Linux:
```
export CYPRESS_API_URL="http://192.168.2.40:1234/"
npx cy2 run --browser ${BROWSER} --record --key XXX --parallel --ci-build-id `TZ='Asia/Taipei' date +"%Y-%m-%d_%H:%M:%S"`
```
#### 開啟GUI進行測試
:::info
* [參考網址-Opening the App](https://docs.cypress.io/guides/getting-started/opening-the-app#What-you-ll-learn)
:::
```
npm run cy:open
```
執行上述指令會跳出如下畫面,選擇左邊圖示E2E Testing

選擇要測試的瀏覽器

選擇左邊選單Specs後,右邊列出所有測試腳本,partner資料夾為Weincloud Partner測試腳本,weincloud資料夾為Weincloud測試腳本

點擊測試腳本開始執行測試,左邊為腳本指令執行過程,顯示執行成功或失敗及錯誤錯息,右邊為腳本在瀏覽器執行的畫面,可按f12協助除錯

#### Command Line進行測試
:::info
* [參考網址-Command Line](https://docs.cypress.io/guides/guides/command-line)
:::
```
npm run cy:run 或 npx cypress run
```
指令Options:
| Options | Description |
| ----------------- |:----------------------- |
| --browser, -b | 選擇測試瀏覽器,參數可以為chrome, chromium, edge, electron, firefox |
| --spec, -s | 指定要運行的測試腳本 |
|...|...|
在project根目錄執行npm run cy:run後會執行所有測試腳本,測試結束後顯示所有執行結果

/screenshots目錄下存放錯誤畫面截圖

/videos目錄下存放執行過程影片

/report目錄下存放執行結果報告網頁

執行結果報告

#### Command Line測試,並將測試結果上傳到Dashboard
:::info
[Dashboard服務](http://192.168.1.251:8080/)
[參考網址-sorry cypress](https://docs.sorry-cypress.dev/)
:::
在project根目錄下terminal執行(windows)
```
set CYPRESS_API_URL="http://192.168.1.251:1234/"
npx cy2 run --browser chrome --record --key XXX --parallel --ci-build-id %date:~0,4%-%date:~5,2%-%date:~8,2%_%time:~0,2%:%time:~3,2%:%time:~6,2%
```
在project根目錄下terminal執行(linux)
```
export CYPRESS_API_URL="http://192.168.1.251:1234/"
npx cy2 run --browser ${BROWSER} --record --key XXX --parallel --ci-build-id `TZ='Asia/Taipei' date +"%Y-%m-%d_%H:%M:%S"`
```
打開http://192.168.2.40:8080/,點擊Project選單

點擊sw-iiot project

最上方具有紅色圓點為正在執行的測試

點擊進入可看測試案例pass或fail

點擊測試案例進入可回放測試過程錄影

點擊失敗測試案例可看cypress失敗log及截圖

## 撰寫第一個測試
---
### 規劃測試情境
以Weincloud登入為例,一個測試情境需包含步驟及斷言以驗證測試是否通過
```
1. 進到Weincloud首面
2. 輸入Domain Name
3. 輸入Username
4. Password
5. 按下Login按鈕
6. 檢查登入後瀏覽器路徑是否為/account/users
```
### 轉換為程式碼
```gherkin=
describe('Weincloud User Login', () => {
beforeEach(() => {
cy.visit('https://test.weincloud.net/')
})
it('should redirect authenticated user to /account/users', () => {
//選取元素,輸入Domain Name e2e_test
cy.get('.el-input__inner').first().type('e2e_test')
//選取元素,輸入帳號admin
cy.get('.el-input__inner').eq(1).type('admin')
//選取元素,輸入密碼123456
cy.get('.el-input__inner').eq(2).type('123456')
//選取登入按鈕,點擊
cy.get('.login-btn').contains('Login').click({ force: true })
//斷言驗證瀏覽器路行是否為/account/users
cy.location('pathname').should('equal', '/account/users')
})
})
```
### 範例說明
* 測試腳本介面結構由describe()、it()、before()、beforeEach()、after()、afterEach()所組成
> [參考網址-Test Structure](https://docs.cypress.io/guides/core-concepts/writing-and-organizing-tests#Test-Structure)
> [參考網址-Mocha](https://docs.cypress.io/guides/references/bundled-libraries#Mocha)
* decribe()
> 描述場景或圈出特定區塊,例如:標明測試的功能或 function,在這個例子我們標明測試Weincloud Login功能。
* it()
> 撰寫測試案例(Test Case),此測試案例測試使用者成功登入後是否抵達路徑/account/users
* get()
> 選取DOM元素進行後續操作,參數為CSS選擇器。在此腳本中,我們選取DOM元素輸入domain_name, username,password後,再點擊登入按鈕。
[參考網址-get](https://docs.cypress.io/api/commands/get)
* location()
> 取得此頁面的window.location物件,此例中我們用以取得此頁面的路徑位址。
> [參考網址-location](https://docs.cypress.io/api/commands/location)
* should()
> 建立一個斷言,斷言會重試直到通過或者是超時,在此例中,我們建立一個斷言判斷location('pathname')取得的頁面路徑是否等於/account/users,如果斷言未能成功通過,則測試失敗。
> [參考網址-should](https://docs.cypress.io/api/commands/should)
## Cypress Concept 介紹
---
:::info
[Introduction to crypress](https://docs.cypress.io/guides/core-concepts/introduction-to-cypress)
:::
### 選取元素
Cypress包進jQuery的selector engine,所以可以像JQeury一樣利過CSS選擇器來選取DOM元素。
### 斷言
斷言讓我們可以描述應用程式所需要的狀態,如果斷言未能通過,測試案例將失敗,斷言可以用來確保某一個元素存在、具有某些屬性或CSS class或具有某些狀態。
#### 利用should()來撰寫斷言
取得subject後,利用should()來寫斷言,如:
- cy.get('button').click().should('have.class', 'active')
- cy.request('/users/1').its('body').should('deep.eq', { name: 'Jane' })
#### 預設斷言(or 內建斷言)
有的command具有內建預設的斷言,使得我們不用顯式地撰寫斷言,在某些條件未能達成時,一樣能使測試案例失敗。
如:
- cy.visit()
- cy.request()
- cy.contains()
- .get()
- .find()
- .type()
- .click()
- .its()
#### Cypress支援的斷言
:::info
[assertion references](https://docs.cypress.io/guides/references/assertions)
:::
Cypress包進了Chai、Chai-jQuery及Sinon-Chai libraries來撰寫各式的斷言。
### Commands(API)
:::info
[參考網址-API](https://docs.cypress.io/api/)
[Commands are asynchronous](https://docs.cypress.io/guides/core-concepts/introduction-to-cypress#Commands-Are-Asynchronous)
:::
注意: Cypress的commands在呼叫當下並不會立刻執行,而是先放到queue裡以待稍後執行,所以我們會說Cypress的commands是非同步的。
常用api介紹(按字母排序):
* as
> 指定別名供之後在cy.get()或cy.wait()參數中使用。
> [參考網址-as](https://docs.cypress.io/api/commands/as)
* check
> 對複選框或單選框做選取。
> [參考網址-check](https://docs.cypress.io/api/commands/check)
* contains
> 選取包含指定文字的DOM元素。
> [參考網址-contains](https://docs.cypress.io/api/commands/contains)
* click
> 點擊DOM元素。
> [參考網址-click](https://docs.cypress.io/api/commands/click)
* get
> 選取DOM元素進行後續操作,參數為CSS選擇器
> [參考網址-get](https://docs.cypress.io/api/commands/get)
* intercept
> 對api請求及回應進行spy跟stub。
> [參考網址-intercept](https://docs.cypress.io/api/commands/intercept)
* location()
> 取得此頁面的window.location物件
> [參考網址-location](https://docs.cypress.io/api/commands/location)
* request
> 發出HTTP請求
> [參考網址-request](https://docs.cypress.io/api/commands/request)
* screenshot
> 對測試畫面進行截圖
> [參考網址-screenshot](https://docs.cypress.io/api/commands/screenshot)
* select
> 在HTML元素select中選擇其中一個option選項。
> [參考網址-select](https://docs.cypress.io/api/commands/select)
* selectFile
> 在HTML元素input[type=file]中選取一個檔案或是模擬拖曳檔案進瀏覽器。
> [參考網址-selectFile](https://docs.cypress.io/api/commands/selectfile)
* should
> 建立一個斷言,斷言會重試直到通過或者是超時。
> [參考網址-should](https://docs.cypress.io/api/commands/should)
* submit
> 對表單進行提交。
> [參考網址-submit](https://docs.cypress.io/api/commands/submit)
* task
> 透過plugin event執行程式碼。
> [參考網址-task](https://docs.cypress.io/api/commands/task)
* then
> 對上個api產生的物件進行後續連接操作。
> [參考網址-then](https://docs.cypress.io/api/commands/then)
* type
> 對DOM元素進行輸入。
> [參考網址-type](https://docs.cypress.io/api/commands/type)
* uncheck
> 取消選取複選框。
> [參考網址-uncheck](https://docs.cypress.io/api/commands/uncheck)
* wait
> 等待參數指定秒數或是等待別名的資源解析完畢。
> [參考網址-wait](https://docs.cypress.io/api/commands/wait)
### Custom Command
:::info
* [參考網址-Custom Commands](https://docs.cypress.io/api/cypress-api/custom-commands)
:::
我們可以定義自己的Command,也可以覆蓋現有的Command。
目前Project含有
| Command | Description |
| ----------------- |:----------------------- |
| weincloudLogin | 登入Weincloud |
| partnerLogin | 登入Weincloud Partner |
| getBySel(參數) | 選取具有data-test="參數"屬性的DOM元素 |
| getBySelLike(參數) | 選取data-test屬性的值包含參數的DOM元素 |
| ... | ... |
### Hooks
:::info
* [參考網址-Hooks](https://docs.cypress.io/guides/core-concepts/writing-and-organizing-tests#Hooks)
* [參考網址-Mocha](https://docs.cypress.io/guides/references/bundled-libraries#Mocha)
* [參考網址-認試軟體測試的世界 & TDD/BDD 入門](https://www.slideshare.net/wantingj/tdd-bdd-47559903)
* [參考網址-單元測試:Mocha、Chai 和 Sinon](https://www.cythilya.tw/2017/09/17/unit-test-with-mocha-chai-and-sinon/)
:::
Cypress採用Mocha的BDD語法,測試腳本都會基於Mocha提供的以下工具:
* before()
> 在所有測試開始前會執行的程式碼區塊。
* beforeEach()
> 在每個 Test Case 開始前執行的程式碼區塊。
* it()
> 撰寫測試案例(Test Case)。
* afterEach()
> 在每個 Test Case 結束後執行的程式碼區塊
* after()
>在所有測試結束後會執行的程式碼區塊。
```mermaid
graph LR
before --> beforeEach --> it --> afterEach --> after
```
## 使用Cypress Recorder錄製腳本
:::info
* [Cypress Recorder](https://chrome.google.com/webstore/detail/cypress-recorder/glcapdcacdfkokcmicllhcjigeodacab)
* [利用工具錄製腳本](https://ithelp.ithome.com.tw/articles/10265802)
:::
點擊上方連結下載Chrome Extension

打開Cypress Recorder後,按下start Recording按鈕,即可開始在網頁上開始操作畫面

錄製結束時,點擊Stop Recording按鈕停止錄製

點擊Copy to Clipboard複製script至專案中,測試script運行是否如預期

:::warning
自動產生的script,選取元素的寫法通常比較不清楚易懂,建議可以再調整,或是改為官方建議的,使用data-test屬性來選取(參考最佳實踐)
:::
## 最佳實踐
:::info
* [參考網址-Best Practices](https://docs.cypress.io/guides/references/best-practices)
:::
* 選取元素
:x: 避免使用容易變動的selector
:heavy_check_mark: 建議使用data-* attribute,使用自定義的cy.getBySel()來選取DOM元素
* 訪問外部網站
:x: 避免訪問無法控制的第三方系統
:heavy_check_mark: 只測試可以控制的部分,避免第三方系統,若有必要,通過cy.request()發出api請求與第三方通訊。
* 腳本之間的關聯
:x: 每個腳本之間都高度緊密,彼此相互依賴
:heavy_check_mark: 腳本之間是獨立運作且可以執行成功的
* 腳本控制登入及其它狀態
:x: 腳本透過操作UI進入登入狀態
:heavy_check_mark: 腳本間應該互相獨立,應該透過編程的方式進入登入或其它狀態,使用腳本不會依賴於登入測試是否成功。
* 不必要的等錯,錯誤使用cy.wait()
:x:等待任意millisecond的時間
:heavy_check_mark:使用cy.intercept()取route alias(路徑別名)或是使用斷言來等待。
說明: 不需要下cy.wait(4000),因為cy.get('table tr').should('have.length', 2)斷言會讓腳本在斷言通過前不繼續往下執行或直到timeout失敗。

說明: 或是設定api路徑別名,讓腳本明確等待api執行完成或直到發生timeout。

* 在before或beforeEach重置狀態
:x:在after或afterEach重置狀態
:heavy_check_mark:在測試腳本開始運行前重置狀態
* 設定global baseUrl
:x:使用cy.visit(),但沒有設定 baseUrl
:heavy_check_mark:在 cypress.json 檔設定 baseUrl 可以讓腳本執行時節省一些時間
* ...
## Dashboard
:::info
* [官方Dashboard](https://www.cypress.io/dashboard/)
* [Sorry Cypress](https://docs.sorry-cypress.dev/)
:::
使用sorry cypress架設的dashboard位於(http://192.168.1.251:8080)