session
缓存并恢复 cookies、
localStorage
和
sessionStorage
(即会话数据),以便在测试之间重建一致的浏览器上下文。
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 的生成,如果传递 Array
或 Object
,Cypress 会通过确定性字符串化你传递的值来生成 id。例如,如果传递 ['Jane', '123', 'admin']
,将为你生成 ["Jane","123","admin"]
的 id。
查看 选择正确的 id 来缓存会话 部分获取更详细的解释和示例。
注意,大型或循环数据结构可能会变慢或难以序列化为标识符,因此要小心处理你指定的数据。
setup (Function)
当给定 id
的会话尚未缓存或不再有效时调用此函数(参见 validate
选项)。在 setup
和 validate
第一次运行后,Cypress 会保留所有 cookies、sessionStorage
和 localStorage
,以便后续使用相 同 id
调用 cy.session()
时绕过 setup,仅恢复和验证缓存的会话数据。
当 testIsolation
启用时,setup
前会清除页面;禁用时不会清除。
无论 testIsolation
配置如何,所有域的 cookies、local storage 和 session storage 在 setup
运行前总是会被清除。
options (Object)
选项 | 默认值 | 描述 |
---|---|---|
validate | undefined | 验证新创建或恢复的会话。在会话创建后和 setup 函数运行后,或会话恢复后页面清除后立即运行的函数。如果抛出异常、包含任何失败的 Cypress 命令、返回一个拒绝或解析为 false 的 Promise,或最后一个 Cypress 命令产生 false ,会话被视为无效。- 如果在 setup 后立即验证失败,测试将失败。- 如果在恢复会话后验证失败, setup 将重新运行。 |
cacheAcrossSpecs | false | 启用后,新创建的会话被视为“全局”会话,可以在同一台机器上同一 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
或拒绝的 Promisevalidate
函数包含失败的 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、localStorage
、sessionStorage
。
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
值用作保存和恢复会话的唯一标识符,确保它实际上是每个会话唯一的非常重要。
在以下示例中,如果 loginByForm
和 loginByApi
创建的会话数据在任何方面不同,将 [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
会用 token1
和 p4ssw0rd
登录,并使用 "user1"
作为 id
创建和缓存会话。
login('user1', 'token1', 'p4ssw0rd')
现在假设你想尝试用不同的 token 和/或密码登录同一用户,并期望创建和缓存不同的会话。你运行这个,但因为 cy.session()
只传递 name
作为 id
,它不会创建新会话,而是加载 "user1"
的保存会话。
login('user1', 'different-token', 'p4ssw0rd')
总之,你需要确保 id
是唯一的。从 setup
函数内部使用的所有可能变化的参数创建它,否则 id
值可能会冲突并产生意外结果。
正确用法
在本例中,将 id
设置为 [name, uniqueKey]
确保用不同的 name
、token
和 password
值调用 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()
,否则你的测试会在空白页上运行。