PAYATHON 2026

Karate 的 @parallel=false 能否跨 Feature 生效?

支付老李

结论

@parallel=false 可以标注在 Feature 层级,但它只能保证当前 Feature 内的 Scenario 按顺序执行,不能作为跨 Feature 的全局锁。多个 Feature 仍然可能同时运行。只要它们共同读写同一个 bookingid,数据竞争就依然存在。

示例中的问题不在断言,而在于 10 个 Feature 通过 karate.callSingle() 取得了同一个共享 bookingid。实际执行过程可能是:

  1. Feature A 将价格更新为 497。
  2. Feature B 随后将同一订单更新为 219。
  3. Feature A 再次查询时得到 219。
  4. 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 之间共享。
  • 测试异常中断后,锁和清理逻辑更难维护。
  • 测试会依赖执行顺序,单独运行和排查问题也更困难。

如果业务条件决定了测试只能操作同一个资源,更稳妥的做法是:

  1. 将这些测试归入明确的串行标签。
  2. 使用 parallel(1) 运行。
  3. 避免其他 Runner 或 CI Job 同时操作该环境中的资源。
  4. 执行前建立已知状态,执行后恢复或清理状态。

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 层面的串行配置。

备注:内容仅供参考。