summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMateus Cruz <mateuscolvr@gmail.com>2024-02-05 21:55:58 -0300
committerMateus Cruz <mateuscolvr@gmail.com>2024-02-05 21:55:58 -0300
commitbd700d5098c6b51db63cdabda81d676f5c354eda (patch)
treeaad4c459f89f602f2ebdc5aeb350893cba0d15b1
parent00ade6066e9dd88c65b176bf4788eea4c3e1a15d (diff)
test: add gatling test
-rw-r--r--flake.nix4
-rw-r--r--load-test/user-files/simulations/rinhabackend/RinhaBackendCrebitosSimulation.scala237
-rwxr-xr-xtest.sh28
3 files changed, 268 insertions, 1 deletions
diff --git a/flake.nix b/flake.nix
index f4b7ceb..1c549c0 100644
--- a/flake.nix
+++ b/flake.nix
@@ -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)
+}
diff --git a/test.sh b/test.sh
new file mode 100755
index 0000000..0bf0b33
--- /dev/null
+++ b/test.sh
@@ -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