Как использовать один токен OAuth2.0 для нескольких виртуальных пользователей в нагрузочном тесте Gatling

#scala #load-testing #gatling #jsonpath

#scala #нагрузочное тестирование #gatling #jsonpath

Вопрос:

Мне нужно выполнить нагрузочный тест API, для которого требуется токен OAuth2.0 через Gatling (в котором я полный новичок!) но хотелось бы, чтобы каждый виртуальный пользователь использовал один и тот же токен. Я извлекаю токен нормально (я думаю) и помещаю его в переменную с именем ‘access’, но я продолжаю получать ‘атрибут с именем ‘access’ не определен’ при запуске самого теста.

Мой поиск токена выглядит следующим образом (вместе с httpConf, используемым ниже):

  class MySimulation extends Simulation {

 val httpConf = http        
    .baseUrl("https://MyBaseUrl.Com/")
    .acceptHeader("application/json") 
    .doNotTrackHeader("1")
    .acceptLanguageHeader("en-UK,en;q=0.5")
    .acceptEncodingHeader("gzip, deflate")
    .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:16.0) Gecko/20100101 Firefox/16.0")
    .shareConnections

 val header = Map("Content-Type" -> """application/x-www-form-urlencoded""")

 al auth = scenario("Retrieve Token")
 .exec(http("POST OAuth Req")
 .post("https://SomeTokenUrl")
 .formParam("resource", "someresource")
 .formParam("grant_type", "somegranttype")
 .formParam("client_secret", "someclientsecret")
 .formParam("client_id", "someclientid")
 .headers(header).check(status.is(200)).check(jsonPath("$.access_token").find.saveAs("access"))) 
  

Затем я попытался настроить нагрузочный тест как (Примечание: я изначально поставил ‘Map’, а не изменяемый вариант, но где-то прочитал, что значение по умолчанию было неизменяемым, и задался вопросом, не из-за этого ли заголовок не может обновиться):

  val headers_10 = scala.collection.mutable.Map("Content-Type" -> "application/json; charset=ISO-8859-1", "Authorization" -> "Bearer ${access}")

 val scn = scenario("MyService Gatling test run")       
           .exec(http("")               
           .post("Myservice/api")
           .headers(headers_10.toMap)                
           .body(StringBody("""{"SomeProperty": "Some Value"}"""))
           .asJson
           .check(status.is(200)))

 setUp(
    auth.inject(constantUsersPerSec(1) during (2 seconds)),
    scn.inject(nothingFor(2 seconds),
    constantUsersPerSec(10) during (10 seconds) 
    ).protocols(httpConf))
    .assertions(global.responseTime.max.lt(500)) 
    .assertions(forAll.failedRequests.percent.lte(1)) 
    .assertions(global.responseTime.mean.lte(100)) 
  

Идея заключалась в том, что извлечение токена завершилось бы до запуска нагрузочного теста, и переменная ‘access’ затем использовалась бы в сценарии нагрузочного теста, но это дает следующий результат:

   ERROR : Failed to build request: No attribute named 'access' is defined 
  

Я достиг конца своей привязки к нему. Я предполагаю, что это может быть как-то связано с областями видимости, и, возможно, переменная не переносится в сценарий нагрузочного тестирования, но я видел примеры в другом месте, которые, кажется, рекомендуют именно такую настройку, поэтому я не знаю, являются ли эти другие примеры частично завершенными или что.

Ответ №1:

Сегодня я реализовал этот сценарий для своего проекта. Пожалуйста, ознакомьтесь с приведенным ниже кодом, и он также будет работать для вас.

Примечание: Я написал этот код с обязательными параметрами моего API. Вы можете изменить этот код в соответствии с вашими требованиями. На данный момент этот код написан в одном классе. Я также реализовал этот код в надлежащем формате с использованием различных классов и файлов свойств. Я также опубликую этот.

Для одного класса код выглядит следующим образом:

 `

package aapi

    import io.gatling.core.Predef._
    import io.gatling.http.Predef._

     class manifestSimulation extends Simulation {    

    private var token = ""

    object authAvi{
     // This is the request(API) which we are going to use for generating the auth token 1 time and then will feed this token into subsequent request.
     var postBody = "{"username":"devusername”,”password”:”devpassword”}”

    val auth = scenario("Retrieve our auth Token which will be used in the subsequent request“)
        .exec(
            http("POST OAuth Req")
            .post(“User Post URL“)
            .body(StringBody(postBody))
            .header("ClientId", “test”)
    .header("DSN", “devDB”)
    .header("accept", "application/json")
    .header("Accept-Language", "en-us")
    .header("Content-Type", "application/json")
            .check(status.is(200))
    .check(jsonPath("$.token")
    .saveAs("token")))
            .exitHereIfFailed
            .exec{session => { token = session("token").as[String]
                             session}}       
    }
    object manifest {
     // This is the request(API) which we are going to hit multiple times using the token which we generated from the previous auth API

        var manifestHeaders = Map("ClientId" -> “test”, "DSN" -> "devDB", "Token" -> "${token}")

        val manifestMethod = exec(session => session.set("token", token))            
            .exec(http("Manifest Details")              
                .get(“Your get URL“)
                .headers(manifestHeaders)
                .check(status.is(200))
                 )
    }   

    val scn = scenario(“**********This is your actual load test*******************”)
        .exec(manifest.manifestMethod)
       setUp(
      authAvi.auth.inject(constantUsersPerSec(1) during (1 seconds)), // fire 1 requests per second for 1 second to retrieve token
    scn.inject(nothingFor(4 seconds), // waits 4 seconds as a margin to process token and this time varies for every user
        constantUsersPerSec(5) during (5 seconds))) // fire 5 requests per second for 5 seconds which will result in 25 (5*5) requests and overall 26 requests when the report gets generated (because we have 1 request for auth token and 25 requests of our intended API (25 1 = 26)

     }`
  

Ответ №2:

Сеансы назначаются для каждого пользователя, и данные сеанса не распределяются между пользователями. Итак, пока у вас есть 1 пользователь, выполняющий ваш сценарий ‘auth’ и сохраняющий токен, это два разных пользователя, которые запускают ‘scn’, и у них нет доступа к значениям сеанса пользователя auth.

Это не рекомендуемая практика, но вы можете решить эту проблему, поместив токен аутентификации в обычный scala var и установив это в сценарии аутентификации и прочитав его в основном сценарии — вам просто нужно убедиться, что аутентификация всегда завершается, прежде чем вводить других пользователей.

 var token: String = ""
  

затем в сценарии аутентификации в конце выполните такой шаг, как

 .exec(session => {
    token = session("access").as[String]
    session
})
  

затем в начале сценария scn выполните шаг для установки переменной сеанса

 .exec(session.set("access", token))
  

Я использовал этот шаблон в прошлом, и он работает, но я уверен, что есть более приятные способы сделать это

Комментарии:

1. Спасибо за ваш ответ, Джеймс, но я все еще не могу заставить его работать. Моя проблема заключается в бите ‘token = session.get(«access»).as[Строка]’. Он не распознает ‘get’. Затем я попробовал ‘.exec(session => { token = session(«access»)})’, что, по понятным причинам, привело к несоответствию типов, поскольку токен был строковым, и он возвращал тип атрибута SessionAttribute. Затем я снова прочитал ‘.as[String]’ до конца, и теперь я получаю несоответствие другого типа — теперь говорится, что он нашел тип ‘unit’, но ожидал ‘Validation’ ?!?!

2. Привет, Джеймс — нашел проблему. ‘get’ больше не является частью session API в более поздних версиях Gatling, которые я использую (плюс, я думаю, что это должны были быть фигурные скобки, а не обычные вокруг сеанса!). Если вы хотите отредактировать свой ответ, указав, что он может вам понадобиться .exec{session => { token = session("access").as[String] session}} , если вы используете более позднюю версию Gatling, я отмечу его как правильный ответ. Спасибо за вашу помощь!

Ответ №3:

@Tarun,

Когда я это делал, в моем сценарии был ‘exec’, а не setup, и использовался следующий синтаксис:

  val dataToUse = feed(testData)
 .exec(session => session.set("access", token))            
 .exec(http("")             
 .post("*the_URL_to_send_to)*")
 .headers(headers_10.toMap)
 .body(RawFileBody("${filePath}")).asJson
 .check(status.is(200))
             )
  

Как упоминалось в комментариях в предыдущем обсуждении, это произошло потому, что я использовал более позднюю версию gatling и метод ‘get’ больше не был частью session api.

Мое полное решение было следующим — обратите внимание, что есть ряд вещей, на которые следует обратить внимание, которые могут не относиться к вашему решению. Я использовал объект, поскольку это просто прояснило в моем сознании то, что я пытался сделать! Кроме того, некоторые импортированные файлы, вероятно, избыточны, поскольку я включил их как часть подхода scattergun к поиску чего-то, что сработало!

Наконец, я в основном перечисляю содержимое каталога и перебираю перечисленные в нем файлы, используя каждый из них в качестве фидера. Вы выглядите так, как будто используете буквальный шаблон json, поэтому, вероятно, это не нужно, но я подумал, что включу его для полноты картины, поскольку это довольно удобно — если вы измените формат вашего json, вам не нужно возиться с изменением шаблона в симуляции, вы просто очищаете каталог и помещаете туда примеры нового формата, и все готово! :

  package myTest

 import io.gatling.core.Predef._
 import io.gatling.http.Predef._
 import scala.concurrent.duration._
 import scala.collection.JavaConversions._
 import java.io.File
 import java.io.FileNotFoundException


 class myTestSimulation extends Simulation {    


 val httpConf = http
    .baseUrl("*your_base_URL*")
.acceptHeader("application/json") // Here are the common headers
.doNotTrackHeader("1")
.acceptLanguageHeader("en-UK,en;q=0.5")
.acceptEncodingHeader("gzip, deflate")
.userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:16.0) Gecko/20100101 Firefox/16.0")
.shareConnections

val header = Map("Content-Type" -> """application/x-www-form-urlencoded""");
private var token = ""

val auth = scenario("Retrieve Token")
    .exec(
        http("POST OAuth Req")
        .post("*URL_for_Token*")
        .formParam("resource", "*your_resource_value*")
        .formParam("grant_type", "*your_grant_type*")
        .formParam("client_secret", "*your_client_secret_value*")
        .formParam("client_id", "*your_client_id_value*")
        .headers(header)
        .check(status.is(200)).check(jsonPath("$.access_token").find.saveAs("access")))  
        .exec{session => { token = session("access").as[String]
                         session}}       

object myTestObject {

    var headers_10 = scala.collection.mutable.Map("Content-Type" -> "application/json; charset=ISO-8859-1", "Authorization" -> "Bearer ${access}")     
    val testData = Iterator.continually(
    new File("*pathway_to_file*") match {
      case d if d.isDirectory => d.listFiles.map(f => Map("filePath" -> f.getPath))
      case _ => throw new FileNotFoundException("Samples path must point to directory")
    }).flatten

    val myTestObjectMethod = feed(testData)
        .exec(session => session.set("access", token))            
        .exec(http("")              
            .post("*the_URL_to_send_to(don't_forget_that_base_URL_above_is_automatically_stuck_to_the_front_of_this!)*")
            .headers(headers_10.toMap)
            .body(RawFileBody("${filePath}")).asJson
            .check(status.is(200))
             )
}   

val scn = scenario("my_actual_load_test")
    .exec(myTestSimulation.myTestObject)
   setUp(
  auth.inject(constantUsersPerSec(1) during (1 seconds)), // fire 1 requests per second for 1 second to retrieve token
scn.inject(nothingFor(2 seconds), // waits 2 seconds as a margin to process token
    constantUsersPerSec(50) during (300 seconds) // fire 50 requests per second for 300 seconds
   ).protocols(httpConf))                             
   .assertions(global.responseTime.max.lt(500)) // set max acceptable response time
   .assertions(forAll.failedRequests.percent.lte(1)) // less than 1% of tests should fail
   .assertions(global.responseTime.mean.lte(100)) // set average response time
 }
  

Я имею в виду, я, вероятно, допустил опечатку где-то в строке, когда я удалил из нее конфиденциальные данные, но, надеюсь, это сделает то, что вам нужно.