Karate 的 @parallel=false 能否跨 Feature 生效?
结论
@parallel=false 可以标注在 Feature 层级,但它只能保证当前 Feature 内的 Scenario 按顺序执行,不能作为跨 Feature 的全局锁。多个 Feature 仍然可能同时运行。只要它们共同读写同一个 bookingid,数据竞争就依然存在。
示例中的问题不在断言,而在于 10 个 Feature 通过 karate.callSingle() 取得了同一个共享 bookingid。实际执行过程可能是:
- Feature A 将价格更新为
497。 - Feature B 随后将同一订单更新为
219。 - Feature A 再次查询时得到
219。 - Feature A 的断言失败。
要彻底避免这种冲突,最好让每个测试创建并使用自己的数据。确实无法隔离的数据,应放入单独的串行测试批次,并通过 parallel(1) 执行。
为什么 @parallel=false 解决不了这个问题
Karate 主要以 Scenario 为单位调度并行任务。下面的标记:
@parallel=false
Feature: Booking update tests
只会限制当前 Feature 内的场景并行,不会阻止其他 Feature 同时开始执行。
假设有两个文件:
feature-a.feature
feature-b.feature
即使两个文件都标记了 @parallel=false,仍可能出现以下执行顺序:
Feature A: PUT booking/123 -> totalprice = 497
Feature B: PUT booking/123 -> totalprice = 219
Feature A: GET booking/123 -> totalprice = 219
PUT 成功只说明更新请求已经完成,不代表该资源在后续 GET 前不会被其他测试再次修改。
首选方案:每个测试使用独立资源
不要通过 karate.callSingle() 创建一个供所有更新测试共用的 booking。认证信息可以共享,但会被修改的业务数据应该由每个 Scenario 单独创建。
例如,可以保留共享的认证初始化:
Feature: Shared authentication
Scenario:
* def testUrl = 'https://restful-booker.herokuapp.com'
* url testUrl
* path 'auth'
* request { username: 'admin', password: 'password123' }
* method post
* status 200
* def testHeaders = { Accept: 'application/json', Cookie: '#("token=" + response.token)' }
更新测试只复用认证信息,并为当前场景创建独立 booking:
Feature: Booking update test
Background:
* def auth = karate.callSingle('auth.feature')
* def testUrl = auth.testUrl
* def testHeaders = auth.testHeaders
* url testUrl
* configure headers = testHeaders
Scenario: update and verify price
* def booking =
"""
{
firstname: 'Mikes',
lastname: 'Test',
totalprice: 500,
depositpaid: true,
additionalneeds: 'SampleData',
bookingdates: {
checkin: '2024-07-18',
checkout: '2024-07-18'
}
}
"""
# 为当前场景创建独立资源
* path 'booking'
* request booking
* method post
* status 200
* match response contains { bookingid: '#number' }
* def bookingid = response.bookingid
# 更新当前场景自己的 booking
* def newPrice = function(max){ return Math.floor(Math.random() * max) }
* booking.totalprice = newPrice(1000)
* path 'booking', bookingid
* request booking
* method put
* status 200
# 验证同一个 booking
* path 'booking', bookingid
* method get
* status 200
* match response.totalprice == booking.totalprice
这样,即使 10 个 Feature 同时运行,它们操作的也是不同的 bookingid,不会互相覆盖。
如果接口支持删除资源,还应该在测试结束后清理数据。清理失败不应掩盖主要断言的失败,可以交给项目现有的 hook 或统一清理机制处理。
无法隔离数据时:建立串行测试批次
有些测试必须使用固定账户、固定客户或唯一业务实体。遇到这种情况,可以给相关 Feature 添加专用标签,例如:
@shared-resource
Feature: Update shared booking
然后将测试分成两批:
- 普通测试并行执行,同时排除
@shared-resource。 - 共享资源测试单独执行,并使用
parallel(1)。
JUnit 5 Runner 可以按下面的方式组织:
import com.intuit.karate.Results;
import com.intuit.karate.Runner;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class RegressionRunner {
@Test
void parallelTests() {
Results results = Runner.path("classpath:features")
.tags("~@shared-resource")
.parallel(5);
assertEquals(0, results.getFailCount(), results.getErrorMessages());
}
@Test
void sharedResourceTests() {
Results results = Runner.path("classpath:features")
.tags("@shared-resource")
.parallel(1);
assertEquals(0, results.getFailCount(), results.getErrorMessages());
}
}
这里真正控制串行执行的是 parallel(1),不能只在 Feature 上添加 @parallel=false。
还要确认构建工具没有再次并行启动这两个测试批次。如果 Maven Surefire、Gradle 或 CI Pipeline 同时运行两个 Runner,那么 Karate 内部的 parallel(1) 也无法阻止另一个 JVM 或流水线任务访问相同数据。必要时,应等并行批次完成后再启动串行批次。
不建议用进程内锁处理共享数据
虽然可以通过 Java 互操作加锁,但它通常不是可靠的测试数据隔离方案:
- 锁只能协调当前 JVM 内的线程。
- 普通 Java 锁无法在多个 JVM、容器或 CI Job 之间共享。
- 测试异常中断后,锁和清理逻辑更难维护。
- 测试会依赖执行顺序,单独运行和排查问题也更困难。
如果业务条件决定了测试只能操作同一个资源,更稳妥的做法是:
- 将这些测试归入明确的串行标签。
- 使用
parallel(1)运行。 - 避免其他 Runner 或 CI Job 同时操作该环境中的资源。
- 执行前建立已知状态,执行后恢复或清理状态。
callSingle() 的适用范围
karate.callSingle() 适合只需执行一次,而且结果能够安全复用的初始化操作,例如:
- 获取认证 token。
- 加载只读配置。
- 初始化后续测试不会修改的参考数据。
它不适合创建会被多个并行测试更新的共享业务对象。即使每个 Feature 中的 Karate 变量看起来彼此独立,变量保存的 bookingid 仍然指向服务端的同一条记录。
因此,下面的认证信息可以共享:
* def auth = karate.callSingle('auth.feature')
但如果后续测试会更新 sharedBooking.bookingid 对应的资源,就不应共享下面的数据:
* def sharedBooking = karate.callSingle('create-booking.feature')
建议的最终组织方式
完整的环境回归测试可以遵循这些规则:
- 认证、静态配置和只读参考数据可以共享。
- 创建、更新和删除类测试默认使用独立业务数据。
- 唯一字段使用场景级唯一值,例如 UUID,不要依赖随机数避免碰撞。
- 必须操作固定资源的 Feature 使用统一标签。
- 固定资源测试由独立 Runner 通过
parallel(1)执行。 - CI 层面也要保证同一个串行批次不会被多个进程同时启动。
@parallel=false 可以保留,用来说明 Feature 内部需要按顺序执行,但它不能替代跨 Feature 的数据隔离,也不能替代 Runner 层面的串行配置。
备注:内容仅供参考。
