当前位置: 首页 > 测试知识 > Gatling用户行为建模:使用Scenario定义真实用户操作流程
Gatling用户行为建模:使用Scenario定义真实用户操作流程
2026-01-19 作者cwb 浏览次数89

一、Scenario基础概念

Scenario定义

Scenario是Gatling中的概念,用于模拟真实用户在系统中的完整操作流程。每个Scenario代表一类用户的行为模式。


scala

import io.gatling.core.Predef._

import io.gatling.http.Predef._

import scala.concurrent.duration._


class RealUserSimulation extends Simulation {

  

  // HTTP配置

  val httpProtocol = http

    .baseUrl("https://api.example.com")

    .acceptHeader("application/json")

    .userAgentHeader("Gatling/3.9")

    .disableCaching

  

  // 定义用户操作流程

  val userJourney = scenario("真实用户操作流")

    .exec(http("首页访问")

      .get("/home")

      .check(status.is(200))

      .check(jsonPath("$.userId").saveAs("userId")))

    .pause(2, 5) // 随机思考时间2-5秒

    .exec(http("登录操作")

      .post("/login")

      .formParam("username", "test_user")

      .formParam("password", "password123")

      .check(status.is(200))

      .check(jsonPath("$.authToken").saveAs("authToken")))

    .pause(1, 3)

}


二、复杂用户行为建模


多步骤业务流程


scala

val ecommerceUser = scenario("电商购物用户")

  .exec(

    // 第一阶段:浏览阶段

    group("浏览阶段") {

      exec(http("访问首页")

        .get("/")

        .check(status.is(200)))

        .pause(3, 7)

        .exec(http("浏览商品列表")

          .get("/products?category=electronics")

          .check(status.is(200))

          .check(css(".product-item", "data-id").findAll.saveAs("productIds")))

        .pause(2, 4)

        .exec(http("查看商品详情")

          .get("/products/${productIds.random()}")

          .check(status.is(200)))

        .pause(5, 10) // 仔细阅读商品详情

    }

  )

  .exec(

    // 第二阶段:购买阶段

    group("购买阶段") {

      exec(http("添加购物车")

        .post("/cart/add")

        .header("Authorization", "Bearer ${authToken}")

        .body(StringBody("""{"productId": "${productId}", "quantity": 1}"""))

        .asJson

        .check(status.is(201)))

        .pause(1, 2)

        .exec(http("查看购物车")

          .get("/cart")

          .header("Authorization", "Bearer ${authToken}")

          .check(status.is(200))

          .check(jsonPath("$.totalPrice").saveAs("cartTotal")))

        .pause(2, 3)

        .exec(http("结算")

          .post("/checkout")

          .header("Authorization", "Bearer ${authToken}")

          .body(StringBody("""{"paymentMethod": "credit_card"}"""))

          .asJson

          .check(status.is(200))

          .check(jsonPath("$.orderId").saveAs("orderId")))

    }

  )


条件分支逻辑


scala

val conditionalUser = scenario("条件流程用户")

  .exec(http("访问网站")

    .get("/")

    .check(status.is(200)))

  .pause(2)

  // 条件分支:检查用户是否登录

  .doIf(session => !session("userId").asOption[String].isDefined) {

    exec(http("新用户注册")

      .post("/register")

      .formParam("email", "user_${userId}@test.com")

      .formParam("password", "Test123!")

      .check(status.is(201))

      .check(jsonPath("$.userId").saveAs("userId")))

      .pause(1)

  }

  // 根据用户类型执行不同操作

  .doIfEquals("${userType}", "premium") {

    exec(http("访问VIP专区")

      .get("/vip/content")

      .check(status.is(200)))

      .pause(5, 10)

  }

  .doIfEquals("${userType}", "regular") {

    exec(http("浏览普通内容")

      .get("/regular/content")

      .check(status.is(200)))

      .pause(2, 5)

  }

  // 循环浏览多个页面

  .repeat(5, "pageNum") {

    exec(http("浏览第${pageNum}页")

      .get("/content?page=${pageNum}")

      .check(status.is(200)))

      .pause(1, 2)

  }


三、高级用户行为模式

数据驱动测试


scala

// 使用Feeder提供测试数据

val userFeeder = csv("data/users.csv").circular

val productFeeder = jsonFile("data/products.json").random


val dataDrivenUser = scenario("数据驱动用户")

  .feed(userFeeder)

  .feed(productFeeder)

  .exec(http("使用动态数据登录")

    .post("/login")

    .formParam("username", "${username}")

    .formParam("password", "${password}")

    .check(status.is(200))

    .check(jsonPath("$.sessionId").saveAs("sessionId")))

  .pause(2)

  .exec(http("查看个性化推荐")

    .get("/recommendations?userId=${userId}")

    .header("Session-Id", "${sessionId}")

    .check(status.is(200))

    .check(jsonPath("$.recommendations[*].productId").findAll.saveAs("recommendedProducts")))

  .pause(3)

  .foreach("${recommendedProducts}", "product") {

    exec(http("查看推荐产品${product}")

      .get("/products/${product}")

      .check(status.is(200)))

      .pause(1)

  }


复杂业务流程


scala

val businessWorkflowUser = scenario("复杂业务流程用户")

  .tryMax(3) { // 重试机制

    exec(

      group("文档处理流程") {

        exec(http("上传文档")

          .post("/documents/upload")

          .header("Authorization", "Bearer ${token}")

          .bodyPart(RawFileBodyPart("file", "test.pdf"))

          .check(status.is(202))

          .check(jsonPath("$.documentId").saveAs("docId")))

          .pause(1)

          

          .asLongAs(session => 

            session("processingStatus").asOption[String].exists(_ != "COMPLETED"),

            "processingStep"

          ) {

            exec(http("检查处理状态")

              .get("/documents/${docId}/status")

              .check(status.is(200))

              .check(jsonPath("$.status").saveAs("processingStatus")))

              .pause(2)

          }

          

          .exec(http("下载处理结果")

            .get("/documents/${docId}/download")

            .check(status.is(200)))

          .pause(3)

      }

    )

  }.exitHereIfFailed // 如果失败则退出场景


四、真实用户模拟配置

多用户类型混合


scala

// 定义不同用户类型的场景

val casualUser = scenario("轻度用户")

  .exec(commonActions.randomPageVisits(1, 3))

  .pause(30, 60)


val powerUser = scenario("重度用户")

  .exec(commonActions.deepEngagementFlow)

  .pause(10, 20)

  .exec(commonActions.complexOperations)

  .pause(5, 15)


val adminUser = scenario("管理员用户")

  .exec(adminActions.dashboardAccess)

  .pause(2, 5)

  .exec(adminActions.userManagement)

  .pause(1, 3)

  .exec(adminActions.systemMonitoring)

  .pause(5, 10)


// 混合用户注入策略

setUp(

  casualUser.inject(

    rampUsers(100).during(2.minutes),    // 前2分钟逐步增加100个轻度用户

    constantUsersPerSec(2).during(5.minutes) // 然后保持每秒2个用户

  ),

  

  powerUser.inject(

    rampUsers(20).during(1.minute),      // 逐步增加20个重度用户

    constantUsersPerSec(0.5).during(10.minutes)

  ),

  

  adminUser.inject(

    constantUsersPerSec(0.1).during(15.minutes) // 持续有管理员操作

  )

).protocols(httpProtocol)

  .maxDuration(15.minutes)

  .assertions(

    global.responseTime.max.lt(2000),

    global.successfulRequests.percent.gt(95)

  )


用户思考时间和行为随机化


scala

val realisticUser = scenario("带思考时间的真实用户")

  .exec(api.login)

  // 正态分布思考时间

  .pause(normalPauses(5.seconds, 2.seconds))

  

  .exec(api.browseProducts)

  // 均匀分布思考时间

  .pause(uniformPauses(3.seconds, 7.seconds))

  

  .randomSwitch(

    70.0 -> exec(api.addToCart),    // 70%概率添加购物车

    20.0 -> exec(api.saveForLater), // 20%概率收藏

    10.0 -> exec(api.continueBrowsing) // 10%概率继续浏览

  )

  

  .pause(2, 5)

  .randomSwitch(

    60.0 -> exec(api.checkout),     // 60%概率结账

    40.0 -> exec(api.abandonCart)   // 40%概率放弃购物车

  )

  

  // 使用会话变量控制流程

  .doIf(session => session("cartValue").as[Double] > 100) {

    exec(api.applyCoupon)  // 购物车价值大于100时使用优惠券

    .pause(1)

  }

  

  .uniformRandomSwitch(

    exec(api.standardShipping),  // 随机选择配送方式

    exec(api.expressShipping),

    exec(api.pickupInStore)

  )


五、性能测试


模块化设计


scala

object UserActions {

  

  val browseProducts = exec(

    group("产品浏览") {

      exec(http("获取产品列表")

        .get("/api/products")

        .queryParam("page", "${currentPage}")

        .check(status.is(200))

        .check(jsonPath("$[*].id").findAll.saveAs("productIds")))

        .pause(1)

        

        .foreach("${productIds}", "productId") {

          exec(http("查看产品详情")

            .get("/api/products/${productId}")

            .check(status.is(200)))

            .pause(0.5, 1.5)

        }

    }

  )

  

  val searchProducts = exec(

    group("产品搜索") {

      feed(searchKeywordsFeeder)

        .exec(http("搜索产品")

          .get("/api/search")

          .queryParam("q", "${keyword}")

          .queryParam("sort", "${sortBy}")

          .check(status.is(200)))

          .pause(2, 4)

    }

  )

  

  val purchaseFlow = exec(

    group("购买流程") {

      exec(http("创建订单")

        .post("/api/orders")

        .body(StringBody(

          """{

            |  "items": ${cartItems},

            |  "shippingAddress": "${address}",

            |  "paymentMethod": "${paymentMethod}"

            |}""".stripMargin))

        .asJson

        .check(status.is(201))

        .check(jsonPath("$.orderId").saveAs("orderId")))

        .pause(1)

        

        .exec(http("确认订单")

          .post("/api/orders/${orderId}/confirm")

          .check(status.is(200)))

    }

  )

}


// 组合使用模块

val completeUserJourney = scenario("完整用户旅程")

  .exec(UserActions.browseProducts)

  .pause(3)

  .exec(UserActions.searchProducts)

  .pause(2)

  .exec(UserActions.purchaseFlow)


错误处理和恢复


scala

val resilientUser = scenario("弹性用户")

  .exec(http("访问首页")

    .get("/")

    .check(status.is(200))

    .check(header("X-Request-ID").saveAs("requestId")))

  .pause(2)

  

  // 处理可能的错误

  .tryMax(2, "登录重试") {

    exec(http("用户登录")

      .post("/login")

      .body(StringBody("""{"username":"${user}", "password":"${pass}"}"""))

      .asJson

      .check(status.in(200, 201))

      .check(jsonPath("$.token").saveAs("authToken")))

  }.exitHereIfFailed

  

  .doIf(session => session("authToken").asOption[String].isDefined) {

    exec(http("获取用户资料")

      .get("/profile")

      .header("Authorization", "Bearer ${authToken}")

      .check(status.is(200))

      // 如果失败,回退到基础数据

      .checkIf(status.is(200)) {

        jsonPath("$.premiumFeatures").saveAs("features")

      }.checkIf(status.is(404)) {

        jsonPath("$.basicFeatures").saveAs("features")

      })

  }

  

  // 记录错误但不中断测试

  .exec(session => {

    if (session.isFailed) {

      println(s"请求失败但继续执行: ${session("requestId").asOption[String]}")

    }

    session

  })


六、监控验证


验证和断言


scala

val validatedScenario = scenario("带验证的用户流程")

  .exec(

    http("创建资源")

      .post("/api/resources")

      .body(StringBody("""{"name": "test-resource", "type": "document"}"""))

      .asJson

      .check(status.is(201))

      .check(jsonPath("$.id").saveAs("resourceId"))

      .check(jsonPath("$.createdAt").transform(dateStr => 

        java.time.Instant.parse(dateStr).toEpochMilli

      ).saveAs("creationTime"))

      .check(header("Location").saveAs("resourceLocation"))

  )

  .pause(1)

  

  .exec(

    http("验证资源创建")

      .get("${resourceLocation}")

      .check(status.is(200))

      .check(jsonPath("$.id").is("${resourceId}"))

      .check(jsonPath("$.status").is("active"))

      .check(jsonPath("$.name").is("test-resource"))

      // 验证时间戳在合理范围内

      .check(jsonPath("$.createdAt").transform(dateStr => 

        val created = java.time.Instant.parse(dateStr).toEpochMilli

        val now = System.currentTimeMillis()

        (now - created) < 60000 // 创建时间应在1分钟内

      ).is(true))

  )

  

  .exec(

    http("执行复杂操作")

      .put("/api/resources/${resourceId}/process")

      .check(status.is(202))

      .check(header("X-Operation-Id").notNull)

      .check(bodyString.saveAs("responseBody"))

      // 自定义验证

      .check(bodyString.transform(body => {

        val json = io.circe.parser.parse(body).getOrElse(io.circe.Json.Null)

        val status = json.hcursor.downField("status").as[String].getOrElse("")

        status == "processing" || status == "queued"

      }).is(true))

  )


性能监控点


scala

val monitoredScenario = scenario("带监控的用户流程")

  .exec(

    group("关键业务事务") {

      exec(

        http("事务开始")

          .get("/api/transaction/start")

          .check(status.is(200))

      ).pause(1)

      

      .exec(

        http("处理步骤1")

          .post("/api/transaction/step1")

          .check(status.is(200))

          .check(responseTimeInMillis.lt(1000)) // 响应时间断言

      )

      

      .exec(

        http("处理步骤2")

          .post("/api/transaction/step2")

          .check(status.is(200))

          .check(responseTimeInMillis.lt(2000))

      )

      

      .exec(

        http("事务提交")

          .post("/api/transaction/commit")

          .check(status.is(200))

          .check(jsonPath("$.transactionId").saveAs("txId"))

      )

    }.resources(

      // 监控相关资源加载

      http("加载CSS")

        .get("/static/css/app.css"),

      http("加载JS")

        .get("/static/js/app.js")

    )

  )


七、电商平台用户模拟案例


scala

class ECommerceSimulation extends Simulation {

  

  val httpProtocol = http

    .baseUrl("https://ecommerce.example.com")

    .acceptHeader("application/json")

    .acceptEncodingHeader("gzip, deflate")

    .userAgentHeader("Mozilla/5.0")

    .disableCaching

  

  // 数据供给

  val userAccounts = csv("data/users.csv").circular

  val products = jsonFile("data/products.json").random

  val searchTerms = csv("data/search_terms.csv").random

  

  // 用户行为对象

  object Browse {

    val homepage = exec(http("访问首页")

      .get("/")

      .check(status.is(200))

      .check(css("meta[name='csrf-token']", "content").saveAs("csrfToken")))

      .pause(2, 5)

    

    val category = exec(

      feed(products)

        .exec(http("浏览分类")

          .get("/category/${categoryId}")

          .check(status.is(200))

          .check(css(".product-card", "data-id").findRandom.saveAs("viewedProduct")))

        .pause(3, 8)

    )

    

    val productDetail = exec(

      exec(http("查看产品详情")

        .get("/product/${viewedProduct}")

        .check(status.is(200))

        .check(css("#product-price").saveAs("productPrice")))

        .pause(5, 15) // 仔细查看产品

    )

  }

  

  object Search {

    val performSearch = exec(

      feed(searchTerms)

        .exec(http("搜索产品")

          .get("/search")

          .queryParam("q", "${searchTerm}")

          .check(status.is(200))

          .check(css(".search-result-item").count.saveAs("resultCount")))

        .pause(1, 3)

    )

    

    val filterResults = exec(

      exec(http("筛选结果")

        .get("/search/filter")

        .queryParam("q", "${searchTerm}")

        .queryParam("priceMin", "0")

        .queryParam("priceMax", "100")

        .check(status.is(200)))

        .pause(2, 4)

    )

  }

  

  object Cart {

    val addToCart = exec(

      exec(http("添加到购物车")

        .post("/cart/add")

        .header("X-CSRF-Token", "${csrfToken}")

        .formParam("productId", "${viewedProduct}")

        .formParam("quantity", "1")

        .check(status.is(200))

        .check(jsonPath("$.cartTotal").saveAs("cartTotal")))

        .pause(1, 2)

    )

    

    val viewCart = exec(

      exec(http("查看购物车")

        .get("/cart")

        .check(status.is(200))

        .check(css(".cart-item").count.gt(0)))

        .pause(2, 3)

    )

  }

  

  object Checkout {

    val startCheckout = exec(

      exec(http("开始结账")

        .post("/checkout/start")

        .header("X-CSRF-Token", "${csrfToken}")

        .check(status.is(200))

        .check(jsonPath("$.checkoutId").saveAs("checkoutId")))

        .pause(1, 2)

    )

    

    val submitOrder = exec(

      exec(http("提交订单")

        .post("/checkout/${checkoutId}/complete")

        .header("X-CSRF-Token", "${csrfToken}")

        .body(StringBody("""{

          "shippingMethod": "standard",

          "paymentMethod": "credit_card",

          "billingAddress": ${addressJson}

        }""")).asJson

        .check(status.is(200))

        .check(jsonPath("$.orderNumber").saveAs("orderNumber")))

        .pause(2, 5)

    )

  }

  

  // 定义完整用户流程

  val standardUser = scenario("标准用户")

    .feed(userAccounts)

    .exec(Browse.homepage)

    .randomSwitch(

      60.0 -> exec(Browse.category),

      40.0 -> exec(Search.performSearch)

    )

    .pause(2, 5)

    .exec(Browse.productDetail)

    .randomSwitch(

      30.0 -> exec(Cart.addToCart), // 30%的用户添加购物车

      70.0 -> exec(Search.performSearch) // 70%继续浏览

    )

    .doIf(session => session("cartTotal").asOption[String].isDefined) {

      exec(Cart.viewCart)

        .randomSwitch(

          50.0 -> exec(Checkout.startCheckout), // 50%结账

          50.0 -> exec(session => {

            println(s"用户放弃购物车: ${session("userId").as[String]}")

            session

          })

        )

    }

  

  // 设置负载模式

  setUp(

    standardUser.inject(

      nothingFor(5.seconds), // 热身期

      rampUsers(50).during(30.seconds), // 逐步增加

      constantUsersPerSec(2).during(5.minutes), // 稳定负载

      rampUsersPerSec(2).to(10).during(2.minutes), // 压力测试

      constantUsersPerSec(10).during(3.minutes), // 峰值负载

      rampUsersPerSec(10).to(2).during(1.minute) // 恢复期

    )

  ).protocols(httpProtocol)

    .maxDuration(15.minutes)

    .assertions(

      global.responseTime.percentile3.lt(800), // 99%请求<800ms

      global.responseTime.max.lt(3000),

      global.failedRequests.percent.lt(1.0),

      forAll.responseTime.percentile4.lt(500) // 所有请求的95%<500ms

    )

}


八、总结

设计原则

真实:模拟真实用户行为,包括思考时间、错误处理、随机选择

模块化:将常用操作封装为可重用组件

数据驱动:使用外部数据源模拟多样化用户

渐进加载:从低负载开始,逐步增加压力

监控验证:添加断言和监控点


调试技巧


scala

// 添加调试信息

val debugScenario = scenario("调试场景")

  .exec(session => {

    println(s"当前用户: ${session("userId").asOption[String]}")

    println(s"会话属性: ${session.attributes}")

    session

  })

  .exec(http("测试请求")

    .get("/debug")

    .check(bodyString.saveAs("response"))

    .check(status.is(200)))

  .exec(session => {

    println(s"响应内容: ${session("response").as[String]}")

    session

  })


// 使用Gatling Recorder记录真实用户操作

// gatling-recorder --help


性能优化建议

连接池配置:优化HTTP连接重用

资源清理:及时关闭不用的连接

缓存策略:合理使用缓存减少重复请求

断言优化:避免过于复杂的断言影响性能


文章标签: 软件测试 测试工具
咨询软件测试