diff options
Diffstat (limited to 'load-test/user-files')
-rw-r--r-- | load-test/user-files/simulations/rinhabackend/RinhaBackendCrebitosSimulation.scala | 237 |
1 files changed, 237 insertions, 0 deletions
diff --git a/load-test/user-files/simulations/rinhabackend/RinhaBackendCrebitosSimulation.scala b/load-test/user-files/simulations/rinhabackend/RinhaBackendCrebitosSimulation.scala new file mode 100644 index 0000000..1ad31cb --- /dev/null +++ b/load-test/user-files/simulations/rinhabackend/RinhaBackendCrebitosSimulation.scala @@ -0,0 +1,237 @@ +import scala.concurrent.duration._ + +import scala.util.Random + +import util.Try + +import io.gatling.commons.validation._ +import io.gatling.core.session.Session +import io.gatling.core.Predef._ +import io.gatling.http.Predef._ + + +class RinhaBackendCrebitosSimulation + extends Simulation { + + def randomClienteId() = Random.between(1, 5 + 1) + def randomValorTransacao() = Random.between(1, 10000 + 1) + def randomDescricao() = Random.alphanumeric.take(10).mkString + def randomTipoTransacao() = Seq("c", "d", "d")(Random.between(0, 2 + 1)) // not used + def toInt(s: String): Option[Int] = { + try { + Some(s.toInt) + } catch { + case e: Exception => None + } + } + + val validarConsistenciaSaldoLimite = (valor: Option[String], session: Session) => { + /* + Essa função é frágil porque depende que haja uma entrada + chamada 'limite' com valor conversível para int na session + e também que seja encadeada com com jmesPath("saldo") para + que 'valor' seja o primeiro argumento da função validadora + de 'validate(.., ..)'. + + ============================================================= + + Nota para quem não tem experiência em testes de performance: + O teste de lógica de saldo/limite extrapola o que é comumente + feito em testes de performance apenas por causa da natureza + da Rinha de Backend. Evite fazer esse tipo de coisa em + testes de performance, pois não é uma prática recomendada + normalmente. + */ + + val saldo = valor.flatMap(s => Try(s.toInt).toOption) + val limite = toInt(session("limite").as[String]) + + (saldo, limite) match { + case (Some(s), Some(l)) if s.toInt < l.toInt * -1 => Failure("Limite ultrapassado!") + case (Some(s), Some(l)) if s.toInt >= l.toInt * -1 => Success(Option("ok")) + case _ => Failure("WTF?!") + } + } + + val httpProtocol = http + .baseUrl("http://localhost:9999") + .userAgentHeader("Agente do Caos - 2024/Q1") + + val debitos = scenario("débitos") + .exec {s => + val descricao = randomDescricao() + val cliente_id = randomClienteId() + val valor = randomValorTransacao() + val payload = s"""{"valor": ${valor}, "tipo": "d", "descricao": "${descricao}"}""" + val session = s.setAll(Map("descricao" -> descricao, "cliente_id" -> cliente_id, "payload" -> payload)) + session + } + .exec( + http("débitos") + .post(s => s"/clientes/${s("cliente_id").as[String]}/transacoes") + .header("content-type", "application/json") + .body(StringBody(s => s("payload").as[String])) + .check( + status.in(200, 422), + status.saveAs("httpStatus")) + .checkIf(s => s("httpStatus").as[String] == "200") { jmesPath("limite").saveAs("limite") } + .checkIf(s => s("httpStatus").as[String] == "200") { + jmesPath("saldo").validate("ConsistenciaSaldoLimite - Transação", validarConsistenciaSaldoLimite) + } + ) + + val creditos = scenario("créditos") + .exec {s => + val descricao = randomDescricao() + val cliente_id = randomClienteId() + val valor = randomValorTransacao() + val payload = s"""{"valor": ${valor}, "tipo": "c", "descricao": "${descricao}"}""" + val session = s.setAll(Map("descricao" -> descricao, "cliente_id" -> cliente_id, "payload" -> payload)) + session + } + .exec( + http("créditos") + .post(s => s"/clientes/${s("cliente_id").as[String]}/transacoes") + .header("content-type", "application/json") + .body(StringBody(s => s("payload").as[String])) + .check( + status.in(200), + jmesPath("limite").saveAs("limite"), + jmesPath("saldo").validate("ConsistenciaSaldoLimite - Transação", validarConsistenciaSaldoLimite) + ) + ) + + val extratos = scenario("extratos") + .exec( + http("extratos") + .get(s => s"/clientes/${randomClienteId()}/extrato") + .check( + jmesPath("saldo.limite").saveAs("limite"), + jmesPath("saldo.total").validate("ConsistenciaSaldoLimite - Extrato", validarConsistenciaSaldoLimite) + ) + ) + + val saldosIniciaisClientes = Array( + Map("id" -> 1, "limite" -> 1000 * 100), + Map("id" -> 2, "limite" -> 800 * 100), + Map("id" -> 3, "limite" -> 10000 * 100), + Map("id" -> 4, "limite" -> 100000 * 100), + Map("id" -> 5, "limite" -> 5000 * 100), + ) + + val criteriosClientes = scenario("validações") + .feed(saldosIniciaisClientes) + .exec( + /* + Os valores de http(...) essão duplicados propositalmente + para que sejam agrupados no relatório e ocupem menos espaço. + O lado negativo é que, em caso de falha, pode não ser possível + saber sua causa exata. + */ + http("validações") + .get("/clientes/#{id}/extrato") + .check( + status.is(200), + jmesPath("saldo.limite").ofType[String].is("#{limite}"), + jmesPath("saldo.total").ofType[String].is("0") + ) + ) + .exec( + http("validações") + .get("/clientes/6/extrato") + .check(status.is(404)) + ) + .exec( + http("validações") + .post("/clientes/1/transacoes") + .header("content-type", "application/json") + .body(StringBody(s"""{"valor": 1, "tipo": "c", "descricao": "toma"}""")) + .check( + status.in(200), + jmesPath("limite").saveAs("limite"), + jmesPath("saldo").validate("ConsistenciaSaldoLimite - Transação", validarConsistenciaSaldoLimite) + ) + ) + .exec( + http("validações") + .post("/clientes/1/transacoes") + .header("content-type", "application/json") + .body(StringBody(s"""{"valor": 1, "tipo": "d", "descricao": "devolve"}""")) + .check( + status.in(200), + jmesPath("limite").saveAs("limite"), + jmesPath("saldo").validate("ConsistenciaSaldoLimite - Transação", validarConsistenciaSaldoLimite) + ) + ) + .exec( + http("validações") + .get("/clientes/1/extrato") + .check( + jmesPath("ultimas_transacoes[0].descricao").ofType[String].is("devolve"), + jmesPath("ultimas_transacoes[0].tipo").ofType[String].is("d"), + jmesPath("ultimas_transacoes[0].valor").ofType[Int].is("1"), + jmesPath("ultimas_transacoes[1].descricao").ofType[String].is("toma"), + jmesPath("ultimas_transacoes[1].tipo").ofType[String].is("c"), + jmesPath("ultimas_transacoes[1].valor").ofType[Int].is("1") + ) + ) + .exec( + http("validações") + .post("/clientes/1/transacoes") + .header("content-type", "application/json") + .body(StringBody(s"""{"valor": 1.2, "tipo": "d", "descricao": "devolve"}""")) + .check(status.in(422)) + ) + .exec( + http("validações") + .post("/clientes/1/transacoes") + .header("content-type", "application/json") + .body(StringBody(s"""{"valor": 1, "tipo": "x", "descricao": "devolve"}""")) + .check(status.in(422)) + ) + .exec( + http("validações") + .post("/clientes/1/transacoes") + .header("content-type", "application/json") + .body(StringBody(s"""{"valor": 1, "tipo": "c", "descricao": "123456789 e mais um pouco"}""")) + .check(status.in(422)) + ) + .exec( + http("validações") + .post("/clientes/1/transacoes") + .header("content-type", "application/json") + .body(StringBody(s"""{"valor": 1, "tipo": "c", "descricao": ""}""")) + .check(status.in(422)) + ) + .exec( + http("validações") + .post("/clientes/1/transacoes") + .header("content-type", "application/json") + .body(StringBody(s"""{"valor": 1, "tipo": "c", "descricao": null}""")) + .check(status.in(422)) + ) + + /* + Separar créditos e débitos dá uma visão + melhor sobre como as duas operações se + comportam individualmente. + */ + setUp( + criteriosClientes.inject( + atOnceUsers(1) + ).andThen( + debitos.inject( + rampUsersPerSec(1).to(220).during(2.minutes), + constantUsersPerSec(220).during(2.minutes) + ), + creditos.inject( + rampUsersPerSec(1).to(110).during(2.minutes), + constantUsersPerSec(110).during(2.minutes) + ), + extratos.inject( + rampUsersPerSec(1).to(10).during(2.minutes), + constantUsersPerSec(10).during(2.minutes) + ) + ) + ).protocols(httpProtocol) +} |