Skip to main content

session

缓存并恢复 cookieslocalStoragesessionStorage (即会话数据),以便在测试之间重建一致的浏览器上下文。

cy.session() 命令会继承 testIsolation 值来决定在缓存和恢复浏览器上下文时是否清除页面。

语法

cy.session(id, setup)
cy.session(id, setup, options)

用法

正确用法

// 通过页面访问登录时缓存会话
cy.session(name, () => {
cy.visit('/login')
cy.get('[data-test=name]').type(name)
cy.get('[data-test=password]').type('s3cr3t')
cy.get('form').contains('Log In').click()
cy.url().should('contain', '/login-successful')
})

// 通过 API 登录时缓存会话
cy.session(username, () => {
cy.request({
method: 'POST',
url: '/login',
body: { username, password },
}).then(({ body }) => {
window.localStorage.setItem('authToken', body.token)
})
})

错误用法

// 在调用 cy.session() 之前访问是多余的,需要在 setup 函数内部完成
cy.visit('/login')
cy.session(name, () => {
// 需要在这里调用 cy.visit(),因为 setup 函数运行时页面是空白的
cy.get('[data-test=name]').type(name)
cy.get('[data-test=password]').type('s3cr3t')
cy.get('form').contains('Log In').click()
// 应该在这里断言登录成功,以确保登录过程在缓存前完成
})
// 应该在 cy.session() 的 setup 函数内部断言,因为这里的页面是空白的
cy.url().should('contain', '/login-successful')

参数

id (String, Array, Object)

用于缓存和恢复给定会话的唯一标识符。简单情况下,String 值足够。为了简化更复杂 id 的生成,如果传递 ArrayObject,Cypress 会通过确定性字符串化你传递的值来生成 id。例如,如果传递 ['Jane', '123', 'admin'],将为你生成 ["Jane","123","admin"] 的 id。

info

查看 选择正确的 id 来缓存会话 部分获取更详细的解释和示例。

caution

注意,大型或循环数据结构可能会变慢或难以序列化为标识符,因此要小心处理你指定的数据。

setup (Function)

当给定 id 的会话尚未缓存或不再有效时调用此函数(参见 validate 选项)。在 setupvalidate 第一次运行后,Cypress 会保留所有 cookies、sessionStoragelocalStorage,以便后续使用相同 id 调用 cy.session() 时绕过 setup,仅恢复和验证缓存的会话数据。

testIsolation 启用时,setup 前会清除页面;禁用时不会清除。

无论 testIsolation 配置如何,所有域的 cookies、local storage 和 session storage 在 setup 运行前总是会被清除。

options (Object)

选项默认值描述
validateundefined验证新创建或恢复的会话。在会话创建后和 setup 函数运行后,或会话恢复后页面清除后立即运行的函数。如果抛出异常、包含任何失败的 Cypress 命令、返回一个拒绝或解析为 false 的 Promise,或最后一个 Cypress 命令产生 false,会话被视为无效。

- 如果在 setup 后立即验证失败,测试将失败。
- 如果在恢复会话后验证失败,setup 将重新运行。
cacheAcrossSpecsfalse启用后,新创建的会话被视为“全局”会话,可以在同一台机器上同一 Cypress 运行的任何 spec 中恢复。将此选项用于将在多个 spec 中多次使用的会话。

生成结果 了解主题管理

  • cy.session() 产生 null

示例

更新现有的登录自定义命令

你可以将会话缓存添加到你的登录 自定义命令。用 cy.session() 调用包装命令内部。

之前

Cypress.Commands.add('login', (username, password) => {
cy.visit('/login')
cy.get('[data-test=name]').type(username)
cy.get('[data-test=password]').type(password)
cy.get('form').contains('Log In').click()
cy.url().should('contain', '/login-successful')
})

之后

Cypress.Commands.add('login', (username, password) => {
cy.session([username, password], () => {
cy.visit('/login')
cy.get('[data-test=name]').type(username)
cy.get('[data-test=password]').type(password)
cy.get('form').contains('Log In').click()
cy.url().should('contain', '/login-successful')
})
})

带会话验证

Cypress.Commands.add('login', (username, password) => {
cy.session(
[username, password],
() => {
cy.visit('/login')
cy.get('[data-test=name]').type(username)
cy.get('[data-test=password]').type(password)
cy.get('form').contains('Log In').click()
cy.url().should('contain', '/login-successful')
},
{
validate() {
cy.request('/whoami').its('status').should('eq', 200)
},
}
)
})

更新现有的登录辅助函数

你可以通过用 cy.session() 调用包装函数内部来添加会话缓存到登录辅助函数。

之前

const login = (name, password) => {
cy.visit('/login')
cy.get('[data-test=name]').type(name)
cy.get('[data-test=password]').type(password)
cy.get('#submit').click()
cy.url().should('contain', '/home')
}

之后

const login = (name, password) => {
cy.session([name, password], () => {
cy.visit('/login')
cy.get('[data-test=name]').type(name)
cy.get('[data-test=password]').type(password)
cy.get('#submit').click()
cy.url().should('contain', '/home')
})
}

带会话验证

const login = (name, password) => {
cy.session(
[name, password],
() => {
cy.visit('/login')
cy.get('[data-test=name]').type(name)
cy.get('[data-test=password]').type(password)
cy.get('#submit').click()
cy.url().should('contain', '/home')
},
{
validate() {
// 受保护的 URL 在用户未授权时应返回 40x http 代码,
// 默认情况下这会导致 cy.visit() 失败
cy.visit('/account-details')
},
}
)
}

在测试中切换会话

因为 cy.session() 在运行 setup 前会清除页面和所有会话数据,你可以用它轻松切换会话而无需先登出前一个用户。这使得测试更准确地模拟真实场景并有助于保持测试运行时间短。

const login = (name) => {
cy.session(name, () => {
cy.request({
method: 'POST',
url: '/login',
body: { name, password: 's3cr3t' },
}).then(({ body }) => {
window.localStorage.setItem('authToken', body.token)
})
})
}

it('should transfer money between users', () => {
login('user')
cy.visit('/transfer')
cy.get('#amount').type('100.00')
cy.get('#send-money').click()

login('other-user')
cy.visit('/account_balance')
cy.get('#balance').should('eq', '100.00')
})

验证会话

validate 函数用于确保会话已正确建立。这在恢复缓存会话时特别有用,因为如果会话无效,cy.session() 会通过重新运行 setup 来重新创建会话。

以下情况会标记会话为无效:

  • validate 函数抛出异常
  • validate 函数返回一个解析为 false 或拒绝的 Promise
  • validate 函数包含失败的 Cypress 命令
  • validate 函数中最后一个 Cypress 命令产生 false

以下是几个 validate 示例:

// 尝试访问只有登录用户才能看到的页面
function validate() {
cy.visit('/private')
}

// 发出一个仅在登录时返回 200 的 API 请求
function validate() {
cy.request('/api/user').its('status').should('eq', 200)
}

// 运行任何在用户未登录时会失败的 Cypress 命令
function validate() {
cy.visit('/account', { failOnStatusCode: false })
cy.url().should('match', /^/account/)
}

在缓存前修改会话数据

如果你想更改缓存的会话数据,可以在 setup 中根据需要修改 cookies、localStoragesessionStorage

cy.session('user', () => {
cy.visit('/login')
cy.get('name').type('user')
cy.get('password').type('p4ssw0rd123')
cy.get('#submit').click()
cy.url().should('contain', '/home')
// 移除我们不想缓存的会话数据
cy.clearCookie('authId')
cy.window().then((win) => {
win.localStorage.removeItem('authToken')
})
// 添加我们想缓存的会话数据
cy.setCookie('session_id', '189jd09sufh33aaiidhf99d09')
})

跨 spec 缓存会话数据

如果你想在同一台机器上的同一 Cypress 运行中的多个 spec 中使用相同的会话,添加 cacheAcrossSpecs=true 到会话选项以在整个运行中利用会话。

const login = (name = 'user1') => {
cy.session(
name,
() => {
cy.request({
method: 'POST',
url: '/login',
body: { name, password: 's3cr3t' },
}).then(({ body }) => {
window.localStorage.setItem('authToken', body.token)
})
},
{
validate() {
cy.visit('/user_profile')
cy.contains(`Hello ${name}`)
},
cacheAcrossSpecs: true,
}
)
}

// profile.cy.js
it('can view profile', () => {
login()
})

// add_blog.cy.js
it('can create a blog post', () => {
login()
})

多个登录命令

更复杂的应用可能需要多个登录命令,这可能需要多次使用 cy.session()。然而,因为 id 值用作保存和恢复会话的唯一标识符,确保它实际上是每个会话唯一的非常重要。

在以下示例中,如果 loginByFormloginByApi 创建的会话数据在任何方面不同,将 [name, password] 作为两者的 id 是错误的,因为无法区分 loginByForm("user", "p4ssw0rd")loginByApi("user", "p4ssw0rd") 创建的会话。相反,你可以修改 id 以区分两者的值,确保每个会话总是被唯一缓存。

const loginByForm = (name, password) => {
cy.session(['loginByForm', name], () => {
cy.visit('/login')
cy.get('[data-test=name]').type(name)
cy.get('[data-test=password]').type(password)
cy.get('#submit').click()
cy.url().should('contain', '/home')
})
}

const loginByApi = (name, password) => {
cy.session(['loginByApi', name], () => {
cy.request({
method: 'POST',
url: '/api/login',
body: { name, password },
}).then(({ body }) => {
window.localStorage.setItem('authToken', body.token)
})
})
}

在哪里调用 cy.visit()

直觉上似乎应该在登录函数或自定义命令中的 cy.session() 后立即调用 cy.visit(),使其行为(从后续测试的角度看)与没有 cy.session() 的登录函数完全相同。

const login = (name) => {
cy.session(name, () => {
cy.visit('/login')
cy.get('[data-test=name]').type(name)
cy.get('[data-test=password]').type('s3cr3t')
cy.get('#submit').click()
cy.url().should('contain', '/home')
})
cy.visit('/home')
}

beforeEach(() => {
login('user')
})

it('should test something on the /home page', () => {
// 断言
})

it('should test something else on the /home page', () => {
// 断言
})

然而,如果你想测试不同页面的内容,你需要在测试开始时调用 cy.visit(),这意味着你将在测试中 第二次 调用 cy.visit()。因为 cy.visit() 会等待访问的页面变为活动状态后才继续,这可能会浪费不可接受的时间。

// ...继续...

it('should test something on the /other page', () => {
cy.visit('/other')
// 断言
})

如果仅在必要时调用 cy.visit(),测试显然会更快。这可以通过 将测试组织成套件 和在 beforeEach 钩子中登录后调用 cy.visit() 轻松实现。

const login = (name) => {
cy.session(name, () => {
cy.visit('/login')
cy.get('[data-test=name]').type(name)
cy.get('[data-test=password]').type('s3cr3t')
cy.get('#submit').click()
cy.url().should('contain', '/home')
})
// 这里不访问
}

describe('home page tests', () => {
beforeEach(() => {
login('user')
cy.visit('/home')
})

it('should test something on the /home page', () => {
// 断言
})

it('should test something else on the /home page', () => {
// 断言
})
})

describe('other page tests', () => {
beforeEach(() => {
login('user')
cy.visit('/other')
})

it('should test something on the /other page', () => {
// 断言
})
})

更新返回值的登录函数

如果你的自定义登录命令返回一个用于测试中断言的值,用 cy.session() 包装它会破坏该测试。然而,通常可以通过重构登录代码在 setup 内部直接断言来解决。

之前

Cypress.Commands.add('loginByApi', (username, password) => {
return cy.request('POST', `/api/login`, {
username,
password,
})
})

it('should return the correct value', () => {
cy.loginByApi('user', 's3cr3t').then((response) => {
expect(response.status).to.eq(200)
})
})

之后

Cypress.Commands.add('loginByApi', (username, password) => {
cy.session(username, () => {
cy.request('POST', `/api/login`, {
username,
password,
}).then((response) => {
expect(response.status).to.eq(200)
})
})
})

it('is a redundant test', () => {
/* 现在你可以删除它了! */
})

跨域会话

可以在缓存会话时切换域,只需确保在登录命令中显式访问域后再调用 cy.session()

const login = (name) => {
if (location.hostname !== 'cypress.io') {
cy.visit('https://example.cypress.io')
}
cy.session(name, () => {
cy.visit('/login')
// 等等
}, {
validate() {
cy.request('/whoami', {
headers: { 'Authorization' : localStorage.token }
method: 'POST'
}).its('status').should('equal', 200)
}
})
}

it('t1', () => {
login('bob')
// 在 cypress.io 上操作
})

it('t2', () => {
cy.visit('http://www.cypress-dx.com')
// 在 anotherexample.com 上操作
})

it('t3', () => {
login('bob')
// 在 cypress.io 上操作
})

注意事项

页面和会话数据何时清除

测试隔离启用

cy.session() 运行且测试隔离启用 testIsolation=true(Cypress 12 默认)时,页面会被清除,所有域的 cookies、local storage 和 session storage(会话数据)会自动清除。这保证了无论会话是创建还是恢复时行为一致,并允许你无需先显式登出即可切换会话。

何时清除?页面清除(测试)会话数据清除
setup
cy.session() 结束前

注意:之后必须显式调用 cy.visit() 以确保加载测试页面。

测试隔离禁用

当测试隔离禁用 testIsolation=false 时,页面不会清除,但会话数据会在 cy.session() 运行时清除。

何时清除页面清除(测试)会话数据清除
setup
cy.session() 结束前

之后无需调用 cy.visit() 以确保加载测试页面。

注意:禁用测试隔离可能会提高端到端测试的性能,但之前的测试可能会影响下一个测试的浏览器状态,并在使用 .only() 时导致不一致。禁用测试隔离时要小心编写隔离测试。

当测试隔离禁用时,鼓励在 before 钩子或第一个测试中设置会话以确保干净的设置。

会话缓存

一旦创建,给定 id 的会话会在 spec 文件期间缓存。你无法在缓存后修改存储的会话,但总是可以使用不同的 id 创建新会话。

为了减少开发时间,当 Cypress 在“打开”模式下运行时,会话会在 spec 文件重新运行时 缓存。

要在多个 spec 中持久化会话,使用选项 cacheAcrossSpecs=true

显式清除会话

当 Cypress 在“打开”模式下运行时,你可以通过点击 仪表盘 中的“Clear All Sessions”按钮显式清除所有 spec 和全局会话并重新运行 spec 文件。

会话仪表盘

出于调试目的,可以使用 Cypress.session.clearAllSavedSessions() 方法清除所有 spec 和全局会话。

在哪里调用 cy.session()

虽然可以在测试或 beforeEach 中显式调用 cy.session(),但最佳实践是在登录 自定义命令 或可重用包装函数中调用 cy.session()。参见 更新现有的登录自定义命令更新现有的登录辅助函数 示例获取更多细节。

选择正确的 id 来缓存会话

为了唯一缓存会话,id 参数 必须对每个新创建的会话唯一。提供给 cy.session()id 会在报告器中显示,因此我们不建议使用敏感数据如密码或令牌作为唯一标识符。

// 如果你的会话设置代码使用字符串变量,将字符串作为 id 传递
const login = (name) => {
cy.session(name, () => {
loginWith(name)
})
}

// 如果你的会话设置代码使用单个对象,将对象作为 id 传递,它会被序列化为标识符
const login = (params = {}) => {
cy.session(params, () => {
loginWith(params)
})
}

// 如果你的会话设置代码使用多个变量,传递这些变量的数组,它会被序列化为标识符
const login = (name, email, params = {}) => {
cy.session([name, email, params], () => {
loginWith(name, email, params)
})
}

// 如果你的会话设置代码使用外部常量,它们不需要包含在 id 中,因为它们永远不会改变
const API_KEY = 'I_AM_AN_API_KEY'
const login = (name, email) => {
cy.session([name, email], () => {
loginWith(name, email, API_KEY)
})
}

错误用法

如果你的自定义 login 代码使用多个参数(本例中为 name、token 和 password)来登录许多不同用户,但 id 只包含其中一个(本例中为 name):

const login = (name, token, password) => {
cy.session(name, () => {
cy.visit('/login')
cy.get('[data-test=name]').type(name)
cy.get('[data-test=token]').type(token)
cy.get('[data-test=password]').type(password)
cy.get('#submit').click()
})
}

如果你运行这个,user1 会用 token1p4ssw0rd 登录,并使用 "user1" 作为 id 创建和缓存会话。

login('user1', 'token1', 'p4ssw0rd')

现在假设你想尝试用不同的 token 和/或密码登录同一用户,并期望创建和缓存不同的会话。你运行这个,但因为 cy.session() 只传递 name 作为 id,它不会创建新会话,而是加载 "user1" 的保存会话。

login('user1', 'different-token', 'p4ssw0rd')

总之,你需要确保 id 是唯一的。从 setup 函数内部使用的所有可能变化的参数创建它,否则 id 值可能会冲突并产生意外结果。

正确用法

在本例中,将 id 设置为 [name, uniqueKey] 确保用不同的 nametokenpassword 值调用 login() 会创建和缓存唯一会话。

const login = (name, token, password, uniqueKey) => {
cy.session([name, uniqueKey], () => {
cy.visit('/login')
cy.get('[data-test=name]').type(name)
cy.get('[data-test=token]').type(token)
cy.get('[data-test=password]').type(password)
cy.get('#submit').click()
})
}

uuid npm 包可用于生成随机唯一 id,如果任意命名空间不满足你的需求。

常见问题

为什么调用 cy.session() 后所有 Cypress 命令都失败?

测试隔离 启用时,确保在调用 cy.session() 后调用 cy.visit(),否则你的测试会在空白页上运行。

为什么调用 cy.session() 后看到 401 错误?

可能你的会话无效或在会话保存和命令结束前未完全建立。确保指定 validate 函数,以便 cy.session() 可以在必要时验证和重新创建会话。

命令日志

仪表盘

每当在测试中创建或恢复会话时,会在测试顶部显示一个额外的仪表盘,提供有关会话状态的更多信息。

点击仪表盘中的任何会话 id 会将该会话的详细信息打印到控制台,点击“Clear All Sessions”按钮会清除所有保存的 spec 和全局会话并重新运行 spec 文件(参见 会话缓存 获取更多细节)。

会话仪表盘

每当调用 cy.session() 时,命令日志会显示以下行之一,包括会话调用的状态和会话 id 值:

  • 未找到保存的会话,因此创建并保存了新会话:

    新会话(折叠)
  • 找到并使用了保存的会话:

    保存的会话(折叠)
  • 找到保存的会话,但 validate 函数失败,因此重新创建并保存了会话:

    重新创建的会话(折叠)

注意,如果 validate 函数在 setup 创建会话后立即失败,测试会失败并报错。

展开命令日志中的会话组会显示创建和/或验证会话时运行的所有命令。

在此图像中,恢复了一个保存的会话,但当在 validate 函数中访问 /personal 时,应用重定向到 /signin,这使会话无效。通过访问 /signin 创建新会话,用户登录后,验证成功,会话可用于测试的其余部分。

重新创建的会话(展开)

点击仪表盘中的会话 id 或命令日志中展开的会话组下的第一行会将该会话的详细信息打印到控制台。此信息包含 id 以及任何缓存的会话数据,包括 cookies、localStoragesessionStorage

会话控制台输出

历史

版本变更
12.0.0移除 experimentalSessionAndOrigin 并使命令默认可用。
11.0.0setup 选项现在必需。
10.9.0添加 cacheAcrossSpecs 属性。
9.6.0添加对 experimentalSessionAndOrigin 的支持并移除 experimentalSessionSupport
8.2.0添加 cy.session() 命令,当 experimentalSessionSupport 启用时可用。

另请参阅