Cypress前端E2E自動(dòng)化測(cè)試記錄

Cypress前端E2E自動(dòng)化測(cè)試記錄

近期用Cypress作一個(gè)新項(xiàng)目的前端E2E自動(dòng)化測(cè)試另凌,對(duì)比TestCafe作前端E2E自動(dòng)化測(cè)試谱轨,Cypress有一些不同之處,現(xiàn)記錄下來(lái)吠谢。

所有Command都是異步的

Cypress中的所有Command都是異步的土童,所以編寫自動(dòng)化腳本時(shí)要時(shí)刻記住這點(diǎn)。比如:

不能從Command中直接返回工坊,而要用 .then() 來(lái)處理献汗。下面例子不工作:

const allProductes = cy.get('@allProductes')

正確的應(yīng)當(dāng)用

let allProductes = null;
cy.get('@allProductes').then((values) => {
  allProductes = values;
}

還有一例,下面代碼也不能正常工作王污,是因?yàn)樵谝粋€(gè)代碼塊中罢吃,所有command都是異步先入隊(duì)列,只有代碼塊結(jié)束后才會(huì)依次執(zhí)行:

it('is not using aliases correctly', function () {
  cy.fixture('users.json').as('users')
    // 此處as還沒(méi)有執(zhí)行呢昭齐,只是入了隊(duì)列而已
  const user = this.users[0]
})

正確的應(yīng)當(dāng)仍然是要用 .then() 范式:

cy.fixture('users.json').then((users) => {
  const user = users[0]
  // passes
  cy.get('header').should('contain', user.name)
})

這樣的結(jié)果就是尿招,測(cè)試代碼中難免出現(xiàn)很多的回調(diào)函數(shù)嵌套。

同樣阱驾,因?yàn)镃ypress命令是異步的就谜,所以debugger也要在 then() 里調(diào)用,不能象下面這樣:

it('let me debug like a fiend', function() {
 cy.visit('/my/page/path')
 cy.get('.selector-in-question')
 debugger // Doesn't work
})
// cy.pause()

上下文中的this不能用在箭頭函數(shù)中

當(dāng)用Mocha的共享上下文對(duì)象機(jī)制時(shí)里覆, this 不能用在箭頭函數(shù)丧荐,要用傳統(tǒng)的函數(shù)形式

beforeEach(function () {
  cy.fixture('product').as('allProductes');
  cy.gotoIndexPage();
})
...
phonePage.actRootSel.forEach((actSel, index) => {
  cy.get(actSel + phonePage.btnExchangeSel, {timeout: Cypress.env('timeoutLevel1')}).click();
  cy.get(rechargePage.sidebarSel).should('exist');
  cy.get(rechargePage.sidebarSel).within(function ($sidebar) {
    productName = this.allProductes[index].product;
    cy.get(rechargePage.productNameSel).should('have.text', productName);
    gameCount = UTILS.randomPhoneNo('1323434');
    cy.get(rechargePage.gameCountSel).type(gameCount);
    cy.get(rechargePage.btnRechargeSel).should('have.text', '支付 ' + this.allProductes[index].jfValue + ' 積分');
    cy.get(rechargePage.btnRechargeSel).click();
  });
  ...

如上所示,在within的回調(diào)函數(shù)中用了 function ($sidebar) { 這樣形式喧枷,而不能用箭頭函數(shù)篮奄。為了避免這樣的麻煩,可以考慮用 cy.get('@*') 來(lái)獲取上下文中變量割去。但那樣又要面對(duì)異步的問(wèn)題(注: cy.get 是異步的窟却,而 this.* 是同步的),還得用 then() 來(lái)解決問(wèn)題呻逆,有些兩難夸赫。

Mocha的共享上下文對(duì)象會(huì)在所有可用的hook和test之間共享。而且每個(gè)測(cè)試結(jié)束后咖城,會(huì)自動(dòng)全部清除抒钱。正是因?yàn)橛羞@樣的上下文共享機(jī)制缴阎,可以在test和hook之間共享變量或別名,要么用閉包 this.* 形式,要么用 .as(*) 這樣的形式蜓氨,實(shí)際上cy.get(@*) 相當(dāng)于 cy.wrap(this.*)骏掀。而且還可以在多層級(jí)中共享:

describe('parent', function () {
  beforeEach(function () {
    cy.wrap('one').as('a')
  })

  context('child', function () {
    beforeEach(function () {
      cy.wrap('two').as('b')
    })

    describe('grandchild', function () {
      beforeEach(function () {
        cy.wrap('three').as('c')
      })

      it('can access all aliases as properties', function () {
        expect(this.a).to.eq('one')   // true
        expect(this.b).to.eq('two')   // true
        expect(this.c).to.eq('three') // true
      })
    })
  })
})

強(qiáng)大的重試機(jī)制

Cypress有缺省的重試(re-try)機(jī)制甚亭,會(huì)在執(zhí)行時(shí)進(jìn)行一些內(nèi)置的assertion白对,然后才超時(shí),兩種超時(shí)缺省值都是4秒:

  • command操作的timeout

  • assertion的timeout

Cypress一般只在幾個(gè)查找DOM元素的命令如 cy.get()find() 苔严、 contains() 上重試定枷,但決不會(huì)在一些可能改變應(yīng)用程序狀態(tài)的command上重試(比如 click() 等)。但是有些command届氢,比如 eq 就算后面沒(méi)有緊跟assertion欠窒,它也會(huì)重試。

你可以修改每個(gè)command的timeout值退子,這個(gè)timeout時(shí)間會(huì)影響到本command和其下聯(lián)接的所有assertion的超時(shí)時(shí)間岖妄,所以不要在command后面的assertion上人工指定timeout。

cy.get(actSel + phonePage.btnExchangeSel, { timeout: Cypress.env('timeoutLevel1') }).click();
...

cy.location({ timeout: Cypress.env('timeoutLevel2') }).should((loc) => {
  expect(loc.toString()).to.eq(Cypress.env('gatewayUrl'));
});

緊接著command的assertion失敗后寂祥,重試時(shí)會(huì)重新運(yùn)行command去查詢dom元素然后再次assert荐虐,直到超時(shí)或成功。多個(gè)assertion也一樣壤靶,每次重試時(shí)本次失敗的assertion都會(huì)把之前成功的assertion順便再次assert缚俏。

.and() 實(shí)際上是 .should() 的別名惊搏,同樣可用于傳入callback的方式贮乳。

cy.get('.todo-list li')     // command
  .should('have.length', 2) // assertion
  .and(($li) => {
    // 2 more assertions
    expect($li.get(0).textContent, 'first item').to.equal('todo a')
    expect($li.get(1).textContent, 'second item').to.equal('todo B')
  })

注意:重試機(jī)制只會(huì)作用在最后一個(gè)command上,解決方法一般有下面兩種恬惯。

解決方法一 :僅用一個(gè)命令來(lái)選擇元素

// not recommended
// only the last "its" will be retried
cy.window()
  .its('app')             // runs once
  .its('model')           // runs once
  .its('todos')           // retried
  .should('have.length', 2)
// recommended
cy.window()
  .its('app.model.todos') // retried
  .should('have.length', 2)

順便提一下向拆,assertion都最好用最長(zhǎng)最準(zhǔn)確的定位元素方式,要不然偶爾會(huì)出現(xiàn)"detached from the DOM"這樣的錯(cuò)誤酪耳,比如:

cy.get('.list')
  .contains('li', 'Hello')
  .should('be.visible')

這是因?yàn)樵?cy.get('.list') 時(shí)浓恳, .list 被當(dāng)作了subject存了起來(lái),如果中途DOM發(fā)生了變化碗暗,就會(huì)出現(xiàn)上面的錯(cuò)誤了颈将。改為:

cy.contains('.list li', 'Hello')
  .should('be.visible')

所以,最好用最精確的定位方式言疗,而不要用方法鏈的形式晴圾。cypress定位元素時(shí)不但可以用CSS選擇器,還可以用JQuery的方式噪奄,比如:

// get first element
cy.get('.something').first()
cy.get('.something:first-child')
// get last element
cy.get('.something').last()
cy.get('.something:last-child')
// get second element
cy.get('.something').eq(1)
cy.get('.something:nth-child(2)')

解決方法二 :在命令后及時(shí)再加一個(gè)合適的assertion死姚,導(dǎo)致它及時(shí)自動(dòng)重試掉當(dāng)前元素(即“過(guò)程中”元素)

cy
  .get('.mobile-nav', { timeout: 10000 })
  .should('be.visible')
  .and('contain', 'Home')

這樣處理后,在判斷 .mobile-nav 存在于DOM中勤篮、可見都毒、包括Home子串這三種情況下,都會(huì)等待最大10秒碰缔。

強(qiáng)大的別名機(jī)制

用于Fixture

這是最常用的用途账劲,比如:

beforeEach(function () {
  // alias the users fixtures
  cy.fixture('users.json').as('users')
})

it('utilize users in some way', function () {
  // access the users property
  const user = this.users[0]
  // make sure the header contains the first
  // user's name
  cy.get('header').should('contain', user.name)
})

數(shù)據(jù)驅(qū)動(dòng)的自動(dòng)化測(cè)試,就可以考慮用這樣的方式讀取數(shù)據(jù)文件。

用于查找DOM

個(gè)人很少用這個(gè)方式涤垫,因?yàn)闆](méi)有帶來(lái)什么的好處姑尺。

// alias all of the tr's found in the table as 'rows'
cy.get('table').find('tr').as('rows')
// Cypress returns the reference to the <tr>'s
// which allows us to continue to chain commands
// finding the 1st row.
cy.get('@rows').first().click()

注意:用alias定議dom時(shí),最好一次精確倒位而不要用命令鏈的方式蝠猬,當(dāng)cy.get參照別名元素時(shí)切蟋,當(dāng)參照的元素不存在時(shí),Cypress也會(huì)再用生成別名的命令再查詢一次榆芦。但正如所知的柄粹,cypress的re-try機(jī)制只會(huì)在最近yield的subject上才起作用,所以一定要用一次精確倒位的選擇元素方式匆绣。

cy.get('#nav header .user').as('user')  (good)
cy.get('#nav').find('header').find('.user').as('user')  (bad)

用于Router

用來(lái)設(shè)置樁
cy.server()
// we set the response to be the activites.json fixture
cy.route('GET', 'activities/*', 'fixture:activities.json')

cy.server()
cy.fixture('activities.json').as('activitiesJSON')
cy.route('GET', 'activities/*', '@activitiesJSON')
等待xhr的回應(yīng)
cy.server()
cy.route('activities/*', 'fixture:activities').as('getActivities')
cy.route('messages/*', 'fixture:messages').as('getMessages')
// visit the dashboard, which should make requests that match
// the two routes above
cy.visit('http://localhost:8888/dashboard')
// pass an array of Route Aliases that forces Cypress to wait
// until it sees a response for each request that matches
// each of these aliases
cy.wait(['@getActivities', '@getMessages'])

cy.server()
cy.route({
  method: 'POST',
  url: '/myApi',
}).as('apiCheck')
cy.visit('/')
cy.wait('@apiCheck').then((xhr) => {
  assert.isNotNull(xhr.response.body.data, '1st API call has data')
})
cy.wait('@apiCheck').then((xhr) => {
  assert.isNotNull(xhr.response.body.data, '2nd API call has data')
})
斷言HXR的回應(yīng)內(nèi)容
    // 先偵聽topay請(qǐng)求
    cy.server();
    cy.route({
      method: 'POST',
      url: Cypress.env('prePaymentURI'),
    }).as('toPay');
...
    // 從topay請(qǐng)求中獲取網(wǎng)關(guān)單
    cy.wait('@toPay').then((xhr) => {
      expect(xhr.responseBody).to.have.property('data');
      cy.log(xhr.responseBody.data);
      let reg = /name="orderno" value="(.*?)"/;
      kpoOrderId = xhr.responseBody.data.match(reg)[1];
      cy.log(kpoOrderId);
      // 自定義命令去成功支付 
      cy.paySuccess(kpoOrderId);
      // 校驗(yàn)訂單情況
      validateOrderItem(productName, gameCount);
    });
...

環(huán)境

Cypress的環(huán)境相關(guān)機(jī)制是分層級(jí)驻右、優(yōu)先級(jí)的,后面的會(huì)覆蓋前面的方式崎淳。

如果在cypress.json中有一個(gè) env key后堪夭,它的值可以用 Cypress.env()獲取出來(lái):

// cypress.json
{
  "projectId": "128076ed-9868-4e98-9cef-98dd8b705d75",
  "env": {
    "foo": "bar",
    "some": "value"
  }
}
Cypress.env()       // {foo: 'bar', some: 'value'}
Cypress.env('foo')  // 'bar'

如果直接放在cypress.env.json后,會(huì)覆蓋掉cypress.json中的值拣凹。這樣可以把cypress.env.json放到 .gitignore 文件中森爽,每個(gè)環(huán)境都將隔離:

// cypress.env.json
{
  "host": "veronica.dev.local",
  "api_server": "http://localhost:8888/api/v1/"
}
Cypress.env()             // {host: 'veronica.dev.local', api_server: 'http://localhost:8888/api/v1'}
Cypress.env('host')       // 'veronica.dev.local'

CYPRESS_cypress_ 打頭的環(huán)境變量,Cypress會(huì)自動(dòng)處理:

export CYPRESS_HOST=laura.dev.local
export cypress_api_server=http://localhost:8888/api/v1/
Cypress.env()             // {HOST: 'laura.dev.local', api_server: 'http://localhost:8888/api/v1'}
Cypress.env('HOST')       // 'laura.dev.local'
Cypress.env('api_server') // 'http://localhost:8888/api/v1/'

最后嚣镜,還是要以用 --env 環(huán)境變量來(lái)指定環(huán)境變量:

cypress run --env host=kevin.dev.local,api_server=http://localhost:8888/api/v1
Cypress.env()             // {host: 'kevin.dev.local', api_server: 'http://localhost:8888/api/v1'}
Cypress.env('host')       // 'kevin.dev.local'
Cypress.env('api_server') // 'http://localhost:8888/api/v1/'

環(huán)境還能在在 plugins/index.js 中處理:

module.exports = (on, config) => {
  // we can grab some process environment variables
  // and stick it into config.env before returning the updated config
  config.env = config.env || {}
  // you could extract only specific variables
  // and rename them if necessary
  config.env.FOO = process.env.FOO
  config.env.BAR = process.env.BAR
  console.log('extended config.env with process.env.{FOO, BAR}')
  return config
}
// 在spec文件中爬迟,都用Cypress.env()來(lái)獲取
  it('has variables FOO and BAR from process.env', () => {
    // FOO=42 BAR=baz cypress open
    // see how FOO and BAR were copied in "cypress/plugins/index.js"
    expect(Cypress.env()).to.contain({
      FOO: '42',
      BAR: 'baz'
    })
  })

自定義Command

自定義一些命令可以簡(jiǎn)化測(cè)試腳本,以下舉兩個(gè)例子菊匿。

簡(jiǎn)化選擇元素

Cypress.Commands.add('dataCy', (value) => cy.get(`[data-cy=${value}]`))
...
it('finds element using data-cy custom command', () => {
  cy.visit('index.html')
  // use custom command we have defined above
  cy.dataCy('greeting').should('be.visible')
})

模擬登錄

Cypress.addParentCommand("login", function(email, password){
  var email    = email || "joe@example.com"
  var password = password || "foobar"

  var log = Cypress.Log.command({
    name: "login",
    message: [email, password],
    consoleProps: function(){
      return {
        email: email,
        password: password
      }
    }
  })

  cy
    .visit("/login", {log: false})
    .contains("Log In", {log: false})
    .get("#email", {log: false}).type(email, {log: false})
    .get("#password", {log: false}).type(password, {log: false})
    .get("button", {log: false}).click({log: false}) //this should submit the form
    .get("h1", {log: false}).contains("Dashboard", {log: false}) //we should be on the dashboard now
    .url({log: false}).should("match", /dashboard/, {log: false})
    .then(function(){
      log.snapshot().end()
    })
})

模擬支付請(qǐng)求

Cypress.Commands.add("paySuccess", (kpoOrderId, overrides = {}) => {
  const log = overrides.log || true;
  const timeout = overrides.timeout || Cypress.config('defaultCommandTimeout'); 

  Cypress.log({
    name: 'paySuccess',
    message: 'KPO order id: ' + kpoOrderId
  });

  const options = {
    log: log,
    timeout: timeout,
    method: 'POST',
    url: Cypress.env('paymentUrl'),
    form: true,
    qs: {
      orderNo: kpoOrderId,
      notifyUrl: Cypress.env('notifyUrl'),
      returnUrl: Cypress.env('returnUrl') + kpoOrderId,
    },
  };
  Cypress._.extend(options, overrides);
  cy.request(options);
});
...
// 在spec文件中
// 從頁(yè)面獲取網(wǎng)關(guān)單
cy.get('input[name="orderNo"]').then(($id) => {
    kpoOrderId = $id.val();
    cy.log(kpoOrderId);
    // 自定義命令去成功支付 
    cy.paySuccess(kpoOrderId);
    // 校驗(yàn)訂單情況
    // 因?yàn)閏ypress所有命令都是異步的付呕,所以只能放在這,不能放到then之外跌捆!
    validateOrderItem(productName, gameCount);
});

Cookie處理

Cookies.debug() 允許在cookie被改變時(shí)徽职,會(huì)記錄日志在console上:

Cypress.Cookies.debug(true)

Cypress會(huì)在每個(gè)test運(yùn)行前自動(dòng)的清掉所有的cookie。但可以用 preserveOnce() 來(lái)在多個(gè)test之間保留cookie佩厚,這在有登錄要求的自動(dòng)化測(cè)試方面很方便姆钉。

describe('Dashboard', function () {
  before(function () {
    cy.login()
  })
  beforeEach(function () {
    // before each test, we can automatically preserve the
    // 'session_id' and 'remember_token' cookies. this means they
    // will not be cleared before the NEXT test starts.
    Cypress.Cookies.preserveOnce('session_id', 'remember_token')
  })
  it('displays stats', function () {
  })
  it('can do something', function () {
  })
})

最后,也可以用全局白名單來(lái)讓Cypress不在每個(gè)test前清cookie可款。

登錄的幾種方法

Cypress可以直接處理cookie育韩,所以直接表單登錄和模擬POST請(qǐng)求就可以登錄了。如果是cookie/session方式闺鲸,還要留意要在test之間手工保留cookie筋讨,請(qǐng)見Cookie處理部分。

    // 直接提交表單摸恍,憑證入cookie中悉罕,登錄成功
    it('redirects to /dashboard on success', function () {
      cy.get('input[name=username]').type(username)
      cy.get('input[name=password]').type(password)
      cy.get('form').submit()

      // we should be redirected to /dashboard
      cy.url().should('include', '/dashboard')
      cy.get('h1').should('contain', 'jane.lane')

      // and our cookie should be set to 'cypress-session-cookie'
      cy.getCookie('cypress-session-cookie').should('exist')
    })  
  // 自定義命令發(fā)請(qǐng)求赤屋,但還有csrf隱藏域
  Cypress.Commands.add('loginByCSRF', (csrfToken) => {
    cy.request({
        method: 'POST',
        url: '/login',
        failOnStatusCode: false, // dont fail so we can make assertions
        form: true, // we are submitting a regular form body
        body: {
          username,
          password,
          _csrf: csrfToken // insert this as part of form body
        }
      })
  })
  // csrf在返回的html中
  it('strategy #1: parse token from HTML', function(){
    // if we cannot change our server code to make it easier
    // to parse out the CSRF token, we can simply use cy.request
    // to fetch the login page, and then parse the HTML contents
    // to find the CSRF token embedded in the page
    cy.request('/login')
      .its('body')
      .then((body) => {
        // we can use Cypress.$ to parse the string body
        // thus enabling us to query into it easily
        const $html = Cypress.$(body)
        const csrf  = $html.find("input[name=_csrf]").val()

        cy.loginByCSRF(csrf)
          .then((resp) => {
            expect(resp.status).to.eq(200)
            expect(resp.body).to.include("<h2>dashboard.html</h2>")
          })
      })
    ...
  })
    // 如果csrf在響應(yīng)頭中
  it('strategy #2: parse token from response headers', function(){
    // if we embed our csrf-token in response headers
    // it makes it much easier for us to pluck it out
    // without having to dig into the resulting HTML
    cy.request('/login')
      .its('headers')
      .then((headers) => {
        const csrf = headers['x-csrf-token']

        cy.loginByCSRF(csrf)
          .then((resp) => {
            expect(resp.status).to.eq(200)
            expect(resp.body).to.include("<h2>dashboard.html</h2>")
          })
      })
    ...
    })  
// 登錄憑證不自動(dòng)存入cookie,需手工操作
describe('Logging in when XHR is slow', function(){

  const username = 'jane.lane'
  const password = 'password123'

  const sessionCookieName = 'cypress-session-cookie'

  // the XHR endpoint /slow-login takes a couple of seconds
  // we so don't want to login before each test
  // instead we want to get the session cookie just ONCE before the tests
  before(function () {
    cy.request({
      method: 'POST',
      url: '/slow-login',
      body: {
        username,
        password
      }
    })
    // cy.getCookie automatically waits for the previous
    // command cy.request to finish
    // we ensure we have a valid cookie value and
    // save it in the test context object "this.sessionCookie"
    // that's why we use "function () { ... }" callback form
    cy.getCookie(sessionCookieName)
      .should('exist')
      .its('value')
      .should('be.a', 'string')
      .as('sessionCookie')
  })

  beforeEach(function () {
    // before each test we just set the cookie value
    // making the login instant. Since we want to access
    // the test context "this.sessionCookie" property
    // we need to use "function () { ... }" callback form
    cy.setCookie(sessionCookieName, this.sessionCookie)
  })

  it('loads the dashboard as an authenticated user', function(){
    cy.visit('/dashboard')
    cy.contains('h1', 'jane.lane')
  })

  it('loads the admin view as an authenticated user', function(){
    cy.visit('/admin')
    cy.contains('h1', 'Admin')
  })
})

如果是jwt壁袄,可以手工設(shè)置jwt

// login just once using API
let user
before(function fetchUser () {
  cy.request('POST', 'http://localhost:4000/users/authenticate', {
    username: Cypress.env('username'),
    password: Cypress.env('password'),
  })
  .its('body')
  .then((res) => {
    user = res
  })
})

// but set the user before visiting the page
// so the app thinks it is already authenticated
beforeEach(function setUser () {
  cy.visit('/', {
    onBeforeLoad (win) {
      // and before the page finishes loading
      // set the user object in local storage
      win.localStorage.setItem('user', JSON.stringify(user))
    },
  })
  // the page should be opened and the user should be logged in
})
...

// use token
it('makes authenticated request', () => {
  // we can make authenticated request ourselves
  // since we know the token
  cy.request({
    url: 'http://localhost:4000/users',
    auth: {
      bearer: user.token,
    },
  })
  .its('body')
  .should('deep.equal', [
    {
      id: 1,
      username: 'test',
      firstName: 'Test',
      lastName: 'User',
    },
  ])
})

拖放處理

Cyprss中處理拖放很容易类早,在test runner中調(diào)試也很方便。

用mouse事件

// A drag and drop action is made up of a mousedown event,
// multiple mousemove events, and a mouseup event
// (we can get by with just one mousemove event for our test,
// even though there would be dozens in a normal interaction)
//
// For the mousedown, we specify { which: 1 } because dragula will
// ignore a mousedown if it's not a left click
//
// On mousemove, we need to specify where we're moving
// with clientX and clientY
function movePiece (number, x, y) {
  cy.get(`.piece-${number}`)
    .trigger('mousedown', { which: 1 })
    .trigger('mousemove', { clientX: x, clientY: y })
    .trigger('mouseup', {force: true})
}

用拖放事件

// 定義拖放方法
function dropBallInHoop (index) {
  cy.get('.balls img').first()
    .trigger('dragstart')
  cy.get('.hoop')
    .trigger('drop')
}

// 或手工處理
it('highlights hoop when ball is dragged over it', function(){
  cy.get('.hoop')
    .trigger('dragenter')
    .should('have.class', 'over')
})

it('unhighlights hoop when ball is dragged out of it', function(){
  cy.get('.hoop')
    .trigger('dragenter')
    .should('have.class', 'over')
    .trigger('dragleave')
    .should('not.have.class', 'over')
})

動(dòng)態(tài)生成test

動(dòng)態(tài)生成test嗜逻,經(jīng)常出現(xiàn)在數(shù)據(jù)驅(qū)動(dòng)的場(chǎng)合下

用不同的屏幕測(cè)試

// run the same test against different viewport resolution
const sizes = ['iphone-6', 'ipad-2', [1024, 768]]

sizes.forEach((size) => {
  it(`displays logo on ${size} screen`, () => {
    if (Cypress._.isArray(size)) {
      cy.viewport(size[0], size[1])
    } else {
      cy.viewport(size)
    }

    cy.visit('https://www.cypress.io')
    cy.get(logoSelector).should('be.visible')
  })
})
})

數(shù)據(jù)驅(qū)動(dòng)涩僻,數(shù)據(jù)可以來(lái)源于fixture,也可以來(lái)源于實(shí)時(shí)請(qǐng)求或task栈顷!

context('dynamic users', () => {
  // invoke:Invoke a function on the previously yielded subject.

  before(() => {
    cy.request('https://jsonplaceholder.cypress.io/users?limit=3')
    .its('body')
    .should('have.length', 10)
    .invoke('slice', 0, 3)
    .as('users')
    // the above lines "invoke" + "as" are equivalent to
    // .then((list) => {
    //   this.users = list.slice(0, 3)
    // })
  })

  describe('fetched users', () => {
    Cypress._.range(0, 3).forEach((k) => {
      it(`# ${k}`, function () {
        const user = this.users[k]

        cy.log(`user ${user.name} ${user.email}`)
        cy.wrap(user).should('have.property', 'name')
      })
    })
  })
})
// plugins/index.js中
module.exports = (on, config) => {
  on('task', {
    getData () {
      return ['a', 'b', 'c']
    },
  })
}
...
context('generated using cy.task', () => {
  before(() => {
    cy.task('getData').as('letters')
  })

  describe('dynamic letters', () => {
    it('has fetched letters', function () {
      expect(this.letters).to.be.an('array')
    })

    Cypress._.range(0, 3).forEach((k) => {
      it(`tests letter #${k}`, function () {
        const letter = this.letters[k]
        cy.wrap(letter).should('match', /a|b|c/)
      })
    })
  })
})

雜項(xiàng)

  • fixture文件不會(huì)被test runner 的"live mode" watched到逆日,修改它后,只能手工重新跑測(cè)試萄凤。
  • Cypress只能檢查到Ajax請(qǐng)求室抽,不能檢查到Fetch和其它比如<script>發(fā)出的請(qǐng)求。這是個(gè)很大的問(wèn)題靡努,一直沒(méi)有解決坪圾。要 polyfill 掉Fetch API才行。但這樣實(shí)際上對(duì)被測(cè)應(yīng)用有些改動(dòng)了惑朦。
// 先npm install cypress-unfetch
// 再在supports/index.js中import即可
import '@rckeller/cypress-unfetch'
  • Cypress的command返回的不是promise兽泄,而是包裝過(guò)的。但可以用 then 把yield的subject傳遞下去行嗤。而且在 then() 中得到的是最近的一個(gè)command里yield的subject已日《舛可以用 .end() 來(lái)結(jié)束這個(gè)yield鏈栅屏,讓其后的Command不收到前面yield的subject:
cy
  .contains('User: Cheryl').click().end() // yield null
  .contains('User: Charles').click() // contains looks for content in document now

其實(shí),完全可以都用 cy. 重新開始一個(gè)新的調(diào)用鏈

cy.contains('User: Cheryl').click()
cy.contains('User: Charles').click() // contains looks for content in document now
  • 當(dāng)指定baseUrl配置項(xiàng)后堂鲜,Cypress會(huì)忽略掉 cy.visit()cy.request() 中的url栈雳。當(dāng)沒(méi)有baseUrl配置項(xiàng)設(shè)定時(shí),Cypress會(huì)用localhost加隨機(jī)端口的方式來(lái)先運(yùn)行缔莲,然后遇一以了 cy.visit()cy.request() 會(huì)再變化請(qǐng)求的url哥纫,這樣會(huì)有一點(diǎn)點(diǎn)閃爍或reload的情況。所以指定的baseUrl后痴奏,能節(jié)省點(diǎn)啟動(dòng)時(shí)間蛀骇。
  • 錄制video現(xiàn)在只能被Electron這個(gè)瀏覽器支持。
  • 判斷元素是否存在读拆,走不同的條件邏輯擅憔,用Cypress內(nèi)置的JQuery:
const $el = Cypress.$('.greeting')
if ($el.length) {
  cy.log('Closing greeting')
  cy.get('.greeting')
    .contains('Close')
    .click()
}
cy.get('.greeting')
  .should('not.be.visible')
  • 測(cè)試對(duì)象可以用 this.test 訪問(wèn),測(cè)試的的名字可以用 this.test.title 獲得檐晕,但在hook中它卻是hook的名字暑诸!
  • 測(cè)試腳本是執(zhí)行在瀏覽器中的蚌讼,包括引入的javascript模塊。Cypress用babel和browserify預(yù)處理它們个榕。Cypress只能和 cy.task()cy.exec() 分別執(zhí)行Node和Shell篡石。但是要小心:只有 taskexec 執(zhí)行終止后,Cypress才會(huì)繼續(xù)運(yùn)行西采!
  • contains 斷言可以跨多個(gè)簡(jiǎn)單的文本形的元素凰萨,比如<span>或<strong>,也能成功
cy.get('.todo-count').contains('3 items left')
  • within 在定位層級(jí)多的元素時(shí)非常好用
cy.get(orderlistPage.orderItemRootSel, { timeout: Cypress.env('timeoutLevel3') }).first().within(($item) => {
  cy.get(orderlistPage.orderItemProductNameSel).should('have.text', productName);
  cy.get(orderlistPage.orderItemGameCountSel).first().should('have.text', gameCount);
  cy.get(orderlistPage.orderItemStateSel).should('have.text', orderState);
})
  • fixture 文檔可以當(dāng)作模塊來(lái)引入
const allProductes = require('../../fixtures/product');
  • 忽略頁(yè)面未處理異常
// ignore errors from the site itself
Cypress.on('uncaught:exception', () => {
  return false
})
  • hook可以放到 supports/*.js
  • 給window加入定制屬性械馆,test中可以用 cy.window 得到window對(duì)象
it('can modify window._bootstrappedData', function () {
  const data = {
    ...
  }
  cy.visit('/bootstrap.html', {
    onBeforeLoad: (win) => {
      win._bootstrappedData = data
    },
  })
  ...
  • 底層交互元素
it('updates range value when moving slider', function(){
    // To interact with a range input (slider), we need to set its value and
    // then trigger the appropriate event to signal it has changed

    // Here, we invoke jQuery's val() method to set the value
    // and trigger the 'change' event

    // Note that some implementations may rely on the 'input' event,
    // which is fired as a user moves the slider, but is not supported
    // by some browsers
    cy.get('input[type=range]').as('range')
      .invoke('val', 25)
      .trigger('change')
    cy.get('@range').siblings('p')
      .should('have.text', '25')
  })
  • 處理不可交互的元素時(shí)沟蔑,可以給Command傳遞 {force: true} 這個(gè)選項(xiàng)
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市狱杰,隨后出現(xiàn)的幾起案子瘦材,更是在濱河造成了極大的恐慌,老刑警劉巖仿畸,帶你破解...
    沈念sama閱讀 221,576評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件食棕,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡错沽,警方通過(guò)查閱死者的電腦和手機(jī)簿晓,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,515評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)千埃,“玉大人憔儿,你說(shuō)我怎么就攤上這事》趴桑” “怎么了谒臼?”我有些...
    開封第一講書人閱讀 168,017評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)耀里。 經(jīng)常有香客問(wèn)我蜈缤,道長(zhǎng),這世上最難降的妖魔是什么冯挎? 我笑而不...
    開封第一講書人閱讀 59,626評(píng)論 1 296
  • 正文 為了忘掉前任底哥,我火速辦了婚禮,結(jié)果婚禮上房官,老公的妹妹穿的比我還像新娘趾徽。我一直安慰自己,他們只是感情好翰守,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,625評(píng)論 6 397
  • 文/花漫 我一把揭開白布孵奶。 她就那樣靜靜地躺著,像睡著了一般潦俺。 火紅的嫁衣襯著肌膚如雪拒课。 梳的紋絲不亂的頭發(fā)上徐勃,一...
    開封第一講書人閱讀 52,255評(píng)論 1 308
  • 那天,我揣著相機(jī)與錄音早像,去河邊找鬼僻肖。 笑死,一個(gè)胖子當(dāng)著我的面吹牛卢鹦,可吹牛的內(nèi)容都是我干的臀脏。 我是一名探鬼主播,決...
    沈念sama閱讀 40,825評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼冀自,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼揉稚!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起熬粗,我...
    開封第一講書人閱讀 39,729評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤搀玖,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后驻呐,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體灌诅,經(jīng)...
    沈念sama閱讀 46,271評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,363評(píng)論 3 340
  • 正文 我和宋清朗相戀三年含末,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了猜拾。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,498評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡佣盒,死狀恐怖挎袜,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情肥惭,我是刑警寧澤盯仪,帶...
    沈念sama閱讀 36,183評(píng)論 5 350
  • 正文 年R本政府宣布,位于F島的核電站务豺,受9級(jí)特大地震影響磨总,放射性物質(zhì)發(fā)生泄漏嗦明。R本人自食惡果不足惜笼沥,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,867評(píng)論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望娶牌。 院中可真熱鬧奔浅,春花似錦、人聲如沸诗良。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,338評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)鉴裹。三九已至舞骆,卻和暖如春钥弯,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背督禽。 一陣腳步聲響...
    開封第一講書人閱讀 33,458評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工脆霎, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人狈惫。 一個(gè)月前我還...
    沈念sama閱讀 48,906評(píng)論 3 376
  • 正文 我出身青樓睛蛛,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親胧谈。 傳聞我的和親對(duì)象是個(gè)殘疾皇子忆肾,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,507評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容