diff options
author | Mateus Cruz <mateuscolvr@gmail.com> | 2024-02-05 21:55:58 -0300 |
---|---|---|
committer | Mateus Cruz <mateuscolvr@gmail.com> | 2024-02-05 21:55:58 -0300 |
commit | bd700d5098c6b51db63cdabda81d676f5c354eda (patch) | |
tree | aad4c459f89f602f2ebdc5aeb350893cba0d15b1 | |
parent | 00ade6066e9dd88c65b176bf4788eea4c3e1a15d (diff) |
test: add gatling test
-rw-r--r-- | flake.nix | 4 | ||||
-rw-r--r-- | load-test/user-files/simulations/rinhabackend/RinhaBackendCrebitosSimulation.scala | 237 | ||||
-rwxr-xr-x | test.sh | 28 |
3 files changed, 268 insertions, 1 deletions
@@ -12,7 +12,8 @@ ocamlPackages = super.ocaml-ng.ocamlPackages_5_1.overrideScope' (oself: osuper: { pg_query = osuper.pg_query.overrideAttrs (prev: { - propagatedBuildInputs = prev.propagatedBuildInputs ++ [ osuper.cmdliner ]; + propagatedBuildInputs = prev.propagatedBuildInputs + ++ [ osuper.cmdliner ]; }); }); }) @@ -46,6 +47,7 @@ ocaml ocaml-lsp ocamlformat + pkgs.openjdk17 ]; buildInputs = rinha.buildInputs 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) +} @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +# Use este script para executar testes locais + +RESULTS_WORKSPACE="$(pwd)/load-test/user-files/results" +GATLING_BIN_DIR="$(pwd)/gatling/bin" +GATLING_WORKSPACE="$(pwd)/load-test/user-files" + +runGatling() { + sh $GATLING_BIN_DIR/gatling.sh -rm local -s RinhaBackendCrebitosSimulation \ + -rd "Rinha de Backend - 2024/Q1: Crébito" \ + -rf $RESULTS_WORKSPACE \ + -sf "$GATLING_WORKSPACE/simulations" +} + +startTest() { + for i in {1..20}; do + # 2 requests to wake the 2 api instances up :) + curl --fail http://localhost:9999/clientes/1/extrato && \ + echo "" && \ + curl --fail http://localhost:9999/clientes/1/extrato && \ + echo "" && \ + runGatling && \ + break || sleep 2; + done +} + +startTest |