# cypress 前端測試 with angular 簡單介紹及教學
## 簡介
Cypress是一個開源的端對端測試框架,它可以幫助開發人員在網頁應用程序上進行自動化測試。Cypress與其他測試框架不同之處在於它直接在瀏覽器中運行,而不是使用Selenium等工具來模擬瀏覽器行為。
### 特點
* 快速而穩定:由於它直接在瀏覽器中運行,Cypress比其他自動化測試框架更快且更穩定。
* 簡單易用:Cypress具有簡潔的API和易於閱讀的錯誤報告,使開發人員能夠快速編寫和調試測試腳本。
* 全面性:Cypress可以檢查網頁的所有方面,包括DOM元素、Ajax請求、時間等等。
* 實時測試:Cypress具有實時測試功能,開發人員可以實時查看應用程序在測試期間的變化。
### Cypress的運作方式可以簡述為以下步驟
* 通過Cypress API編寫測試腳本,例如訪問網站、模擬使用者行為等。
* 執行測試腳本,Cypress將在瀏覽器中打開應用程序,然後注入測試腳本。
* Cypress將在測試過程中監聽所有瀏覽器事件,例如單擊、鍵盤輸入等。這樣可以確保測試腳本在與應用程序交互時能夠正確運行。
* Cypress會在執行期間顯示詳細的錯誤報告,包括錯誤類型、錯誤描述和堆棧跟踪。
* 測試運行完成後,Cypress會生成一份詳細的報告,報告中包含測試腳本運行的結果、執行時間、失敗原因等信息。
總之,Cypress是一個強大的自動化測試框架,它可以幫助開發人員更快地發現和解決應用程序的問題。Cypress的簡潔API和易於閱讀的報告使測試腳本編寫和調試變得更加容易。
### cypress是怎麼找到要測試的位置
Cypress可以透過簡單的命令和API來定位和控制網頁上的元素,從而實現對網頁應用程序的自動化測試。以下是Cypress定位和控制網頁元素的幾種方法:
使用CSS選擇器
Cypress支持使用CSS選擇器來定位網頁上的元素。開發人員可以使用jQuery的語法,例如:
```csharp
cy.get('button') // 找到所有的button元素
cy.get('#myInput') // 找到id為myInput的元素
cy.get('.myClass') // 找到所有class為myClass的元素
cy.get('[data-testid=myId]') // 找到所有data-testid屬性為myId的元素
```
使用Cypress內置的命令
Cypress還提供了一些內置的命令,例如:
```csharp
cy.contains('Submit') // 找到包含Submit文本的元素
cy.get('input').first() // 找到第一個input元素
cy.get('input').eq(1) // 找到第二個input元素
cy.get('form').find('button') // 在form元素中找到所有button元素
```
使用XPath
Cypress也支持使用XPath來定位網頁上的元素。開發人員可以使用XPath的語法,例如:
```arduino
cy.xpath('//button[text()="Submit"]') // 找到文本為Submit的button元素
cy.xpath('//*[@id="myInput"]') // 找到id為myInput的元素
cy.xpath('//*[contains(@class, "myClass")]') // 找到所有class包含myClass的元素
```
而在官方文件中有提到推薦開發者自行在html中,針對要進行測試的tag加上官方推薦的屬性如data-cy,data-test,data-testid,方便定位資料,且若使用cypress的錄製功能時,也會以這些為優先偵測,進而自動產生測試碼。例如:
```html
<button
id="main"
class="btn btn-large"
name="submission"
role="button"
data-cy="submit"
>
Submit
</button>
```


## 安裝
### way 1
到根目錄中,使用以下指令安裝
```
npm install cypress --save-dev
```
### way 2(推薦,以下介紹以此方案安裝介紹)
到根目錄中,使用以下指令安裝
```
ng add @cypress/schematic
```
使用這個指令會做下面這些事情
* 安裝Cypress
* 添加npm腳本以運行Cypress端到端(e2e)測試的運行模式和開啟模式
* 可以在angular.json中看到以下這些設定
```json
"cypress-run": {
"builder": "@cypress/schematic:cypress",
"options": {
"devServerTarget": "web-management:serve"
},
"configurations": {
"production": {
"devServerTarget": "web-management:serve:production"
}
}
},
"cypress-open": {
"builder": "@cypress/schematic:cypress",
"options": {
"watch": true,
"headless": false
}
},
"ct": {
"builder": "@cypress/schematic:cypress",
"options": {
"devServerTarget": "web-management:serve",
"watch": true,
"headless": false,
"testingType": "component"
},
"configurations": {
"development": {
"devServerTarget": "web-management:serve:development"
}
}
},
"e2e": {
"builder": "@cypress/schematic:cypress",
"options": {
"devServerTarget": "web-management:serve",
"watch": true,
"headless": false
},
"configurations": {
"production": {
"devServerTarget": "web-management:serve:production"
}
}
}
```
* 可以在package.json中看到以下設定,就可以使用 npm run cypress:run 或npm run cypress:open 來執行
```json
"cypress:open": "cypress open",
"cypress:run": "cypress run"
```
* 建立基礎的Cypress文件和目錄架構
* cypress
- download
- e2e:用來放e2e測試腳本的地方
- fixtures:主要用來存儲測試用泛例的外部數據json檔
- support
- screenshots (有設定錄製功能才會產生)
- videos (有設定錄製功能才會產生)
* 提供使用ng-generate輕鬆添加新的e2e和組件規格的能力
* 產生一個cypress.config.ts
* 可選:提示您添加或更新默認的ng e2e命令以使用Cypress進行端到端測試。
* 可選:提示您添加ng ct命令以使用Cypress進行組件測試。
### 啟動
```
npm run cypress:open
```
### 在CMD中開始測試指令
```
npm run cypress:run
```
### 啟動後

#### E2E testing
E2E testing是End-to-End testing的簡稱,也稱作黑盒測試,這種測試方式的目的是驗證一個應用程式的完整性和正確性,從用戶的角度來模擬各種操作,並且測試應用程式的所有部分,包括UI、後端、資料庫、API等。E2E testing通常是在模擬真實環境下執行的,例如模擬不同的使用者、網絡狀態、瀏覽器等。
#### Component testing
Component testing是測試應用程式的獨立模塊或組件的功能和邏輯,也稱作單元測試。這種測試方式主要是針對一個組件的內部邏輯進行測試,並且通常是在隔離的環境下進行的,例如使用模擬器或mock對象來測試。Component testing可以測試每個組件的正確性,並且可以快速發現和修復bug。
## 簡單來玩一下
### 寫一頁登入頁面
html在要針對測試的點,加上data-cy的屬性
```html
<div class="fixMiddle">
<div class="row">
<div class="col-md-4 col-md-offset-4">
<h1 style="text-align: center;">Login</h1>
<form name="form" (ngSubmit)="f.form.valid && login()" #f="ngForm" novalidate style="text-align: center;">
<div class="form-group m-2" [ngClass]="{ 'has-error': f.submitted && !username.valid }">
<label for="username">Username</label>
<input type="text" class="form-control" name="username" [(ngModel)]="model.username" #username="ngModel" required data-cy="username" />
</div>
<div class="form-group m-2" [ngClass]="{ 'has-error': f.submitted && !password.valid }">
<label for="password">Password</label>
<input type="password" class="form-control" name="password" [(ngModel)]="model.password" #password="ngModel" data-cy="password" required />
</div>
<div class="form-group m-2">
<button [disabled]="loading" class="btn btn-primary" data-cy="submit-login-form-btn" >Login</button>
<a routerLink="/register" class="btn btn-link" data-cy="register-btn">Register</a>
</div>
</form>
</div>
</div>
</div>
```
component
```typescript=
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.css']
})
export class LoginComponent {
loading = false;
@ViewChild('f')
loginForm!: NgForm;
model:{username:string, password:string} = {username: '', password: ''};
login(){
this.loading = true;
console.log(this.loginForm.value);
this.loginForm.reset();
this.loading = false;
}
}
```
### 編寫測試腳本
編寫腳本有兩種方式
* 手寫
[基本語法](##基本語法)這裡我就不廢話了,常用的指令我寫在文章結尾
* 錄製
1. 到cypress.config.ts中的e2e加上 experimentalStudio: true 來開啟錄製功能
```typescript
export default defineConfig({
e2e: {
baseUrl: 'http://localhost:4200',
experimentalStudio: true,
},
component: {
devServer: {
framework: 'angular',
bundler: 'webpack',
},
specPattern: '**/*.cy.ts'
}
})
```
2. 啟動cypress,選E2E Testing
```
npm run cypress:open
```

3. 選你想要的瀏覽器

4. 選擇你想要編輯的測試腳本

5. 當滑鼠移到腳本的右方會有一個魔法棒,點擊他即可進行錄製,你只要直接進行一般使用者的操作,cypress就會自動產生腳本

6. 霹靂卡霹靂拉拉波波莉娜貝貝魯多 cypress就自動產生了studio註解內的腳本

## 基本語法
### Hook
```javascript
describe('Hooks', () => {
before(() => {
// runs once before all tests in the block
})
beforeEach(() => {
// runs before each test in the block
})
afterEach(() => {
// runs after each test in the block
})
after(() => {
// runs once after all tests in the block
})
})
```
* __before()__ - 執行所有測試前執行一次 (only)
* __beforeEach()__ - 每一個測試執行前會執行一次
* __afterEach()__ - 每一個測試執行後會執行一次
* __after()__ - 執行所有測試後執行一次 (only)
### 元素定位
查找 DOM 元素的基本方式 :
```html
<button id="main1" class="btn" data-cy="submit"> submit </button>
<button id="main2" class="btn" data-test="submit"> submit </button>
<button id="main3" class="btn" data-testid="submit"> submit </button>
<ul>
<li id="li1">test1</li>
<li data-cy="li2">test2</li>
<li data-test="li3">test3</li>
<li data-testid="li4">test4</li>
</ul>
```
### __.get(selector)__
用來在 DOM 樹中查找 selector 對應的 DOM 元素。
```javascript
// 兩種格式
cy.get(selector)
cy.get(alias)
// 測試文件
context("get命令用例", function() {
it("測試用例", function() {
cy.get("#main1").as("myMain") // sets the alias
cy.get("@myMain") // re-queries the DOM as before (only if necessary)
cy.get(".btn")
cy.get("li")
cy.get("ul>[data-testid=li4]")
})
})
```
### __.find(selector)__
在 DOM 樹中搜尋 __已被定位到的元素後代__,將匹配到的元素返回一個新的JQuery對象。
```javascript
// 測試文件
context("find 命令用例", function() {
it("正確寫法", function() {
cy.get("ul").find("#li1")
})
it("錯誤寫法", function() {
cy.find("#li1")
})
})
```
### __.contains()__
獲取包含指定文本的 DOM 元素
```javascript
// 兩種格式
.contains(content)
.contains(selector, content)
// 測試文件
context("contains 命令用例", function() {
it("contains(content) 例子", function() {
cy.contains("submit")
})
it("contains(selector, content) 例子", function() {
cy.contains("ul>li", "test1")
})
it("contains(content) 正則例子", function() {
cy.contains(/test\d/)
})
})
```
[更多查找元素輔助方式](https://www.cnblogs.com/poloyy/p/13065998.html)
Cypress 提供三個定位器 <僅用來測試> :
* data-cy
* data-test
* data-testid
```javascript
// 測試文件
context("data-元素定位器", function() {
it("測試用例", function() {
cy.get("[data-cy=submit]").click()
cy.get("[data-test=submit]").click()
cy.get("[data-testid=submit]").click()
})
})
```
可以透過自訂義 command 來簡化
```javascript
// support/commands.ts
declare namespace Cypress {
interface Chainable {
byTestId<E extends Node = HTMLElement>(
id: string,
options?: Partial<Cypress.Loggable & Cypress.Timeoutable & Cypress.Withinable & Cypress.Shadow>,
): Cypress.Chainable<JQuery<E>>;
}
}
Cypress.Commands.add(
'byTestId',
<E extends Node = HTMLElement>(
id: string,
options?: Partial<Cypress.Loggable & Cypress.Timeoutable & Cypress.Withinable & Cypress.Shadow>,
): Cypress.Chainable<JQuery<E>> => cy.get(`[data-testid="${id}"]`, options),
)
// 測試文件
context("data-元素定位器", function() {
it("測試用例", function() {
cy.get("[data-cy=submit]").click()
cy.get("[data-test=submit]").click()
cy.byTestId("submit").click()
})
})
```
### __與元素互動__
在 DOM 元素上,可以進行以下操作 :
* __.type()__ - 輸入框輸入文本元素。
* __.focus()__ - 聚焦在 DOM 元素。
* __.blur()__ - DOM 元素失去焦點。
* __.clear()__ - 清空 DOM 元素。
* __.check()__ - 選中單選框、複選框。
* __.uncheck()__ - 取消選中複選框。
* __.select()__ - select options的選項框
* __.dblclick()__ - 雙擊。
* __.rightclick()__ - 右鍵點擊。
* __.submit()__ - 提交表單。
* __.scrollIntoView()__ - 將 DOM 元素滑動到可視區域。
* __.trigger()__ - DOM 元素上觸發事件。
* __cy.scrollTo()__ - 滑動滾動條。
[更多內容說明](https://www.cnblogs.com/poloyy/p/13066035.html)
### 關於斷言
#### __default Assertions__
* __cy.visit()__ expects the page to send text/html content with a 200 status code.
* __cy.request()__ expects the remote server to exist and provide a response.
* __cy.contains()__ expects the element with content to eventually exist in the DOM.
* __cy.get()__ expects the element to eventually exist in the DOM.
* __.find()__ also expects the element to eventually exist in the DOM.
* __.type()__ expects the element to eventually be in a typeable state.
* __.click()__ expects the element to eventually be in an actionable state.
* __.its()__ expects to eventually find a property on the current subject.
<p class="callout info">
某些 command 可能有特定的要求,導致它們立即失敗而不重試,例如 cy.request(),<br>
其它基於 DOM 的 command,會自動重試並在失敗前等待其對應的元素存在,<br>
甚至還有 action command 在失敗之前自動等待其元素達到可以操作狀態。
</p>
##### __Writing Assertions__
There are two ways to write assertions in Cypress:
* 顯性斷言: 使用 expect().
* 隱性斷言: 使用 .should() or .and(),這個是 Cypress 推崇的方式。.and() 和 .should() 的使用方式和效果是一樣的。
```
.and(chainers)
.and(chainers, value)
.and(chainers, method, value)
.and(callbackFn)
```
* chainers : 斷言器。 [BDD](https://docs.cypress.io/guides/references/assertions#BDD-Assertions)
* value : 需要斷言的值。
* method : 需要調用的方法。
* callbackFn : 回調方法,可以滿足自己想要的斷言內容 ; 總是返回前一個 cy 命令返回的結果,方法內的 return 是無效的 ; 會一直運行到裡面沒有斷言。
##### __Should__
[should api](https://docs.cypress.io/api/commands/should#Syntax)
[大補帖](https://www.cnblogs.com/poloyy/p/13744006.html)
##### __Subject Management__
在 Cypress 在使用 command 後產生的內容,將會確定接下來能夠用的 command,像是在 cy.get() 或 cy.contains() 後產生 DOM 元素,允許我們進一步使用 command ,像是 .click() 甚至是再使用 cy.contains(),但是有些 command 回傳一個 null 就不能夠再接其它 command,像是 cy.clearCookies()。
```javascript
cy.clearCookies() // Done: 'null' was yielded, no chaining possible
cy.get('.main-container') // Yields an array of matching DOM elements
.contains('Headlines') // Yields the first DOM element containing content
.click() // Yields same DOM element from previous command
```
##### __Using then() to Act On A Subject__
使用 .then() 則使你可以使用上一條命令產生的主題。
```javascript
cy
// Find the el with id 'some-link'
.get('#some-link')
.then(($myElement) => {
// ...massage the subject with some arbitrary code
// grab its href property
const href = $myElement.prop('href')
// strip out the 'hash' character and everything after it
return href.replace(/(#.*)/, '')
})
.then((href) => {
// href is now the new subject
// which we can work with now
})
```
##### __Commands 為 async 異步執行__
舉個例子 ~ <br>
錯誤寫法 :
```javascript
it('does not work as we expect', () => {
cy.visit('/my/resource/path') // Nothing happens yet
cy.get('.awesome-selector') // Still nothing happening
.click() // Nope, nothing
// Cypress.$ is synchronous, so evaluates immediately
// there is no element to find yet because
// the cy.visit() was only queued to visit
// and did not actually visit the application
let el = Cypress.$('.new-el') // evaluates immediately as []
if (el.length) {
// evaluates immediately as 0
cy.get('.another-selector')
} else {
// this will always run
// because the 'el.length' is 0
// when the code executes
cy.get('.optional-selector')
}
})
// Ok, the test function has finished executing...
// We've queued all of these commands and now
// Cypress will begin running them in order!
```
正確寫法 :
```javascript
it('does not work as we expect', () => {
cy.visit('/my/resource/path') // Nothing happens yet
cy.get('.awesome-selector') // Still nothing happening
.click() // Nope, nothing
.then(() => {
// placing this code inside the .then() ensures
// it runs after the cypress commands 'execute'
let el = Cypress.$('.new-el') // evaluates after .then()
if (el.length) {
cy.get('.another-selector')
} else {
cy.get('.optional-selector')
}
})
})
// Ok, the test function has finished executing...
// We've queued all of these commands and now
// Cypress will begin running them in order!
```
[官網例子](https://docs.cypress.io/guides/core-concepts/introduction-to-cypress#Commands-Are-Asynchronous)
#### __Commands Run Serially__
```javascript
it('changes the URL when "awesome" is clicked', () => {
cy.visit('/my/resource/path') // 1.
cy.get('.awesome-selector') // 2.
.click() // 3.
cy.url() // 4.
.should('include', '/my/resource/path#awesomeness') // 5.
})
```
#### __新增斷言__
Because we are using chai, that means you can extend it however you'd like. Cypress will "just work" with new assertions added to chai. You can:
* Write your own chai assertions as [documented here](https://www.chaijs.com/api/plugins/).
* npm install any existing chai library and import into your test file or support file.
> [Check out our example recipe extending chai with new assertions.](https://docs.cypress.io/examples/examples/recipes#Fundamentals)
#### 取得某component中的變數範例
```typescript=
describe('My Test', () => {
it('Get the variable in component', () => {
// get the verifyCode from the component and type it into the input
cy.window().then((win: any) => {
const appComponent = win.ng.getComponent(
win.document.querySelector('app-login'),
'app-login'
);
const verifyCode = appComponent.verifyCode;
cy.get('[data-cy=login-verify-code]').type(verifyCode);
console.log(verifyCode);
});
});
});
```
### 產出報告
內建三種格式
* spec
* json
* junit
`npx cypress run --reporter junit `<br>
後面加 --spec"特定測試檔" ,可針對特定測試檔執行。
#### 使用 Mochawesome
安裝
`npm install mocha` <br>
`npm install mochawesome` <br>
執行
`npx cypress run --reporter mochawesome `
後面加 --spec"特定測試檔" ,可針對特定測試檔執行。
[了解更多](https://www.cnblogs.com/poloyy/p/13030898.html)
### Plugins
#### cypress-file-upload
install<br>
`npm install --save-dev cypress-file-upload`
在 cypress/support/command.ts 添加<br>
`import 'cypress-file-upload'; `<br>
測試 code [測試的網站](https://practice.automationbro.com/cart/)
```
describe('cypress-file-upload test', () => {
it('upload file', () => {
cy.visit("https://practice.automationbro.com/cart/");
cy.get("input[type=file]").attachFile("test.txt");
cy.get("input[value='Upload File']").click();
cy.get("#wfu_messageblock_header_1_label_1").should(
"contain",
"uploaded successfully"
)
})
})
```
----
參考資料
* [https://www.cypress.io/](https://www.cypress.io/)
* [https://www.npmjs.com/package/@cypress/schematic](https://www.npmjs.com/package/@cypress/schematic)
* [https://docs.cypress.io/guides/references/best-practices](https://docs.cypress.io/guides/references/best-practices)
* [https://levelup.gitconnected.com/write-e2e-tests-with-angular-and-cypress-1f011f673a5e](https://levelup.gitconnected.com/write-e2e-tests-with-angular-and-cypress-1f011f673a5e)
###### tags: `angular` `前端` `cypress`