iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 19
0
Modern Web

Angular新手村學習筆記(2019)系列 第 19

Day19_Cypress.io - 優雅的E2E測試

  • 分享至 

  • xImage
  •  

延申閱讀

2019 ng conf
Avoiding The Suck Of Testing Using Cypress.io | Joe Eames & Jesse Sanders
https://www.youtube.com/watch?v=GH9Dvo_BYkk&list=PLOETEcp3DkCpimylVKTDe968yNmNIajlR&index=29

[S04E12] Cypress.io - 優雅的E2E測試 & NG-ZORRO

這一集是Mike大大分享的Cypress.io,另外,NG-ZORRO超酷的,但不是測試就自行參考囉
https://www.youtube.com/watch?v=uNxkrTX_xmI&list=PL9LUW6O9WZqgUMHwDsKQf3prtqVvjGZ6S&index=22
Mike大大的文件
https://paper.dropbox.com/doc/print/wVmBd63ifvIkk7FI89Krt?print=true&noDesktopRedirect=1

建議可以再多看看官方文件
https://docs.cypress.io/zh-cn/guides/core-concepts/introduction-to-cypress.html#

Cypress建議大家可以試看看,因為e2e其實跟angular沒有什麼關係了
Cypress可以拿來測各種網站,完全可以獨立成一個測試專案
而且Cypress開Chrome的測試畫面好美喔
缺點是他只能用Chrome

我覺得Cypress最大的缺點:會吃Chrome的版本

  1. 如果你不想升級你的angular專案,但你的chrome一直自動更新,你就會無法跑cypress。
  2. 如果你想固定開發環境,也是很困難的,你很難安裝某一版的chrome
  3. 所以cypress適合第三方套件用很少,Angular大改版就會升級的專案(UI可能用Angular Material才能確保版本同步)

首先,Mike大大對Cypress.io的優缺點分析
優勢

  • e2e 測試框架
  • 具有更多符合操作邏輯的API
  • 內建&整合更多常用的 e2e testing 功能,像是截圖、錄影等
  • 自動等待動畫、disabled等狀態,直到穩定才運行
  • 不是直接使用Selenium,而是開啟瀏覽器後內嵌一隻功能強大的test runner
  • Dashboard功能,可以與CI直接整合
    缺點
  • 目前只支援 Chrome、Electron 系列瀏覽器;其他瀏覽器的支援開發中
  • 一隻測試只支援一個domain下的操作,跨多個domain會有CORS問題

延申閱讀
(angular6時期的文章)
https://angularfirebase.com/lessons/cypress-angular-testing-end-to-end/
https://medium.com/@ronnieschaniel/getting-started-with-cypress-e2e-testing-in-angular-bc42186d913d

Cypress Angular Schematic

可以自動幫你裝cypress到你的angular專案
https://github.com/briebug/cypress-schematic

延伸閱讀:

npm install -g @briebug/cypress-schematic
# 在你的angular專案目錄
ng g @briebug/cypress-schematic:add
# 幫angular專案裝@briebug/cypress-schematic,及相依套件
ng add @briebug/cypress-schematic

試著用Mike大大教的知識,來試玩網路上其他cypress範例

其他網路找的範例程式
比較新一點的cypress的文章(Angular 7的),以Login為例
因為時間的關係,我就不找Angular 8的囉,不過7升8難度應該較低
https://blog.angularindepth.com/get-started-with-cypress-d6ac4b910605
原始碼
https://github.com/melcor76/angular-7-registration-login-example-cli

  • clone下來後,試著跑看看
npm install
ng e2e
# cypress會發現會吃chrome的版本
# 如果你的cypress版本太舊,會這樣
# 2019/9/28,我的chrome version為77,就無法跑cypress
[06:30:17] E/launcher - session not created: Chrome version must be between 71 and 75

# 如果有新版的cypress,會提醒你更新
# Version 3.4.1 is now available (currently running Version 3.2.0)
# To update Cypress:
# Quit this app.
# If using npm, run npm install --save-dev cypress@3.4.1
# If using yarn, run yarn add cypress@3.4.1
# Run node_modules/.bin/cypress open to open the new version.
# 更新cypress
npm install --save-dev cypress@3.4.1
  • 跑單元測試
ng test 跑執行單元測試(Karma)
  • 用 cypress 跑 end-to-end 測試
# 因為這個專案的package.json的"scripts"裡沒有定義e2e
# 所以不能下 ng e2e 喔

cypress開chrome的測試畫面真的很美

==再來2個沒有整合angular的範例==

再來試玩另一個很厲害的範例

Commands drive your tests in the browser like a real user would. They let you perform actions like typing, clicking, xhr requests, and can also assert things like "my button should be disabled".
用cypress來測試cypress doc
原始碼
https://github.com/cypress-io/cypress-example-kitchensink
要測的網站
https://example.cypress.io
整個example.cypress.io很大,會跑很久喔

  • 試著跑看看
# 安裝packages
npm i
# 執行測試方式1
npm start # 把網站跑起來
npm run cy:open # 執行cypress test runner
# 執行測試方式2
npm run local:run # 定義在package.json的sciprts裡 "local:run": "start-test 8080 cy:run",

這個範例會在背景執行chrome,會把結果錄成mp4放在cypress/videos/

  • cypress的目錄結構
    cypress的相關檔案寫在cypress/目錄裡
  1. cypress/fixtures
    測試過程中需要用到的外部靜態資料,通常會使用cy.fixture(),
    常用於儲存http request

  2. cypress/integration
    測試案例寫在這裡,支援.js, .jsx.coffee, .cjsx
    也支援ES2015 modules、CommonJS modules
    https://github.com/cypress-io/cypress-example-recipes/blob/master/examples/fundamentals__node-modules/cypress/integration/es2015-commonjs-modules-spec.js
    context()與describe()相同、而define()與it()相同

  3. cypress/plugins/index.js
    在跑spec.js前都會include的
    https://on.cypress.io/plugins-guide

const _ = require('lodash') // yup, dev dependencies
const path = require('path') // yup, built in node modules
const debug = require('debug') // yup, dependencies
const User = require('../../lib/models/user') // yup, relative local modules
  1. cypress/support/index.js
    https://docs.cypress.io/api/cypress-api/custom-commands.html#Arguments
    幫cy客製化指令,整個spec.js都能用
    語法
Cypress.Commands.add(name, callbackFn)
Cypress.Commands.add(name, options, callbackFn)
Cypress.Commands.overwrite(name, callbackFn)
Cypress.Commands.overwrite(name, options, callbackFn)
// 幫cy加個login()
Cypress.Commands.add('login', (userType, options = {}) => {
                      ^^name  ^^^^^^^^^^^^^^^^^^^^^^^^callbackFn
  // this is an example of skipping your UI and logging in programmatically

  // 定義2種user
  const types = {
    admin: {
      name: 'Jane Lane',
      admin: true,
    },
    user: {
      name: 'Jim Bob',
      admin: false,
    }
  }

  // 抓出是admin或user
  const user = types[userType]

  // 新增一筆user
  cy.request({
    url: '/seed/users', // assuming you've exposed a seeds route
    method: 'POST',
    body: user,
  })
  .its('body')
  .then((body) => {
    // (body)是假設server回傳的資料,包含:body.email, body.password
    cy.request({
      url: '/login',
      method: 'POST',
      body: {
        email: body.email,
        password: body.password,
      }
    })
  })
})

在spec.js裡使用login

cy.login('admin') 

cy
  .get('button')
  .login('user') // 可以串接,但不會收到前一個subject
touch {your_project}/cypress/integration/sample_spec.js
# 寫完spec.js後,打開cypress就可以看到sample_spec.js
./node_modules/.bin/cypress open

依照教學文件,玩測試案例
/cypress/integration/sample_spec.js

describe('My First Test', function() {
  it('測試會通過', function() {
    expect(true).to.equal(true) // 預期true等於true
  })
  it('測試會失敗', function() {
    expect(true).to.equal(false)
  })
  it('Visits the Kitchen Sink', function() {
    // 開啟https://example.cypress.io
    cy.visit('https://example.cypress.io')
    // 找到包含type的elements,並click()
    // 點下type後,網址會跳到https://example.cypress.io/commands/actions
    cy.contains('type').click()
    
    // 網址列包含'/commands/actions'
    cy.url().should('include', '/commands/actions')
       ^^^^ 取得瀏覽器目前的路由
    
    // Get an input, type into it and verify that the value has been updated
    // selector找到input的class=.action-email的
    // 輸入fake@email.com
    // 輸入後,input值為'fake@email.com'
    cy.get('.action-email')
      .type('fake@email.com')
      .should('have.value', 'fake@email.com')
  })
})
官方文件提到,cypress是用來測自己的專案,不是拿來當爬蟲的
有些網站基於安全,會阻擋cypress

用cypress測todomvc(React App)

這個範例好在readme.md就教你玩CI/CD
fork出repository到你的github,CI跑完,再CD到你自已fork出來的repository
https://github.com/cypress-io/cypress-example-todomvc

還好不一定要玩一整套CI/CI,就可以

  • 試著跑看看
npm i
npm start # 跑http://localhost:8888
./node_modules/.bin/cypress open # 開啟cypress
  • 設定cypress要測的網站
    cypress.json
{
  "baseUrl": "http://localhost:8888"
}
  • cypress的測試腳本
    cypress/integration/app_spec.js
    所以我們來看一下app_spec.js
    app_spec.js都有註解,跟相關的官網文件,k完就掌握cypress了
// 定義Cypress object "cy"
/// <reference types="cypress" />

// 自定義指令,例如 "createDefaultTodos"
/// <reference types="../support" />

// 檢查可不可以使用TypeScript
// @ts-check

// ***********************************************
// 用 Cypress tests 來重寫官網的 TodoMVC tests (使用Selenium)
// getting started guide
// https://on.cypress.io/introduction-to-cypress

我們可以發現Cypress提供很多function,跟其他e2e框架不同的語法,要再去熟練

describe('TodoMVC - React', function () {

  // 定義變數
  let TODO_ITEM_ONE = 'buy some cheese'
  let TODO_ITEM_TWO = 'feed the cat'
  let TODO_ITEM_THREE = 'book a doctors appointment'

  beforeEach(function () {
    // Cypress預設每次測試都會清掉Local Storage
    // 每次都回到cypress.json定義的http://localhost:8888
    // https://on.cypress.io/api/visit
    cy.visit('/') 
  })
  
// a very simple example helpful during presentations
  it('adds 2 todos', function () {
    cy.get('.new-todo') // selector,找到class為new-todo的element
    .type('learn testing{enter}') // 輸入learn testing,按enter
    .type('be cool{enter}') // 輸入be cool,按enter
    // 此時符合.todo-list li的elements應該有2個
    cy.get('.todo-list li').should('have.length', 2)
    
    cy.get('.mobile-nav', { timeout: 10000 }) // 設timeout
      .should('be.visible') // 應該要可以看的到的
      .and('contain', 'Home') // 而且包含'Home'
    
  })
  
  context('When page is initially opened', function () {
    it('should focus on the todo input field', function () {
      // http://on.cypress.io/focused
      // 當頁面一打開後,預期判斷focused的element的class會有.new-todo
      cy.focused().should('have.class', 'new-todo')
    })
  })

  context('No Todos', function () {
    it('should hide #main and #footer', function () {
      // http://on.cypress.io/get
      // 下列3個selector應該都要抓不到element
      cy.get('.todo-list li').should('not.exist')
      cy.get('.main').should('not.exist')
      cy.get('.footer').should('not.exist')
    })
  })

  context('New Todo', function () {
    // New commands used here:
    // https://on.cypress.io/type
    // https://on.cypress.io/eq
    // https://on.cypress.io/find
    // https://on.cypress.io/contains
    // https://on.cypress.io/should
    // https://on.cypress.io/as

    it('should allow me to add todo items', function () {
      // create 1st todo,建第1筆todo
      cy.get('.new-todo').type(TODO_ITEM_ONE).type('{enter}')
      // 建完後,第1筆todo的label應該包含剛剛輸入的TODO_ITEM_ONE
      cy.get('.todo-list li').eq(0).find('label').should('contain', TODO_ITEM_ONE)

      // create 2nd todo
      cy.get('.new-todo').type(TODO_ITEM_TWO).type('{enter}')
      ...
    })

    it('adds items', function () {
      // 輸入4筆todo,應該要有4筆
      cy.get('.new-todo')
      .type('todo A{enter}')
      .type('todo B{enter}') // we can continue working with same element
      .type('todo C{enter}') // and keep adding new items
      .type('todo D{enter}')
      cy.get('.todo-list li').should('have.length', 4)
    })

    it('should clear text input field when an item is added', function () {
      // 當.new-todo輸入完後,text應該恢復為空字串''
      cy.get('.new-todo').type(TODO_ITEM_ONE).type('{enter}')
      cy.get('.new-todo').should('have.text', '')
    })

    it('should append new items to the bottom of the list', function () {
      // defined in cypress/support/commands.js
      cy.createDefaultTodos().as('todos')
         ^^^^^^^^^^^^^^^^^^^自定義的function,可重複使用

      cy.get('.todo-count').contains('3 items left')

      cy.get('@todos').eq(0).find('label').should('contain', TODO_ITEM_ONE)
      cy.get('@todos').eq(1).find('label').should('contain', TODO_ITEM_TWO)
      cy.get('@todos').eq(2).find('label').should('contain', TODO_ITEM_THREE)
              ^ Alias 
// https://docs.cypress.io/guides/core-concepts/variables-and-aliases.html#Stale-Elements
    })
    ...
    it('should show #main and #footer when items added', function () {
      cy.createTodo(TODO_ITEM_ONE) // 輸入完1個todo後
         ^^^^^^^^^ 自定義的function
      cy.get('.main').should('be.visible') // 應該要有.main
      cy.get('.footer').should('be.visible') // 應該要有.footer
    })
  })
  ...其他的都類似,k完並熟練就好囉

上一篇
Day18_CI Pipeline for Angular
下一篇
Day20_JS Array 基本介紹(AngularTaiwan線上讀書會第5季-主題:RxJS)
系列文
Angular新手村學習筆記(2019)33
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
Leo
iT邦新手 3 級 ‧ 2020-12-31 11:13:57

媽,我上電視了

我要留言

立即登入留言