Skip to content
JoralmoPro
TwitterHomepage

Pruebas de carga con k6

load-test, tutorial, Go, Postman, jwt, programación, heroku8 min read

Hola mundo, bienvenidos a este tutorial donde voy a intentar explicar como aplicar pruebas de carga con k6 sobre una pequeña api que construiré en Go.

Como mencioné primero construiré una pequeña api en Go que es la que posteriormente se le aplicará las pruebas, entonces empecemos con el proceso de construcción de la api.

Para este caso vamos a usar Fiber una librería de Go que nos permite construir una api rápido y sencillo y además es bastante "similar" a Express para los que nos gusta usar Nodejs.

Entonces empezamos, creamos un directorio y dentro de el creamos nuestro archivo main.go y ejecutamos por consola los siguiente comandos:

1go mod init joralmo.pro/JoralmoPro/msExample
2go get -u github.com/gofiber/fiber/v2

Los anteriores comandos son para iniciar el "proyecto" en Go, y luego instalamos la librería de Fiber.

Ahora en el archivo main.goempezamos a escribir la api:

1package main
2
3import (
4 "os"
5
6 "github.com/gofiber/fiber/v2"
7)
8func main() {
9 app := fiber.New()
10 app.Get("/", func(c *fiber.Ctx) error {
11 return c.SendString("Hello, World!")
12 })
13 port := os.Getenv("PORT")
14 if port == "" {
15 port = "3000"
16 }
17 app.Listen(":" + port)
18}

Esto solo para probar que funciona, ejecutamos en consola go run main.go y vamos al navegador en localhost:3000 y vemos que nos devuelve el mensaje "Hello, World!".

Imagen de prueba inicial

Para colocar mas lógica a nuestra api agregaremos un nuevo endpoint que nos retornará un listado de usuarios generados con Faker (un paquete que genera data aleatoria) y en este caso la generará basada en un modelo de usuarios que crearemos, primero instalamos el paquete, desde la consola ejecutamos:

1go get -u github.com/bxcodec/faker/v3

Añadimos el modelo de usuarios a nuestro archivo main.go (después de los imports):

1type User struct {
2 Id int
3 FirstName string `faker:"first_name"`
4 LastName string `faker:"last_name"`
5 Email string `faker:"email"`
6 UserName string `faker:"username"`
7 IPV4 string `faker:"ipv4"`
8 DomainName string `faker:"domain_name"`
9 Latitude float32 `faker:"lat"`
10 Longitude float32 `faker:"long"`
11 Phone string `faker:"phone_number"`
12}

importamos el paquete de faker:

1"github.com/bxcodec/faker/v3"

y ahora el endpoint que retorna el listado de usuarios (antes de declarar la variable port):

1app.Get("/users", func(c *fiber.Ctx) error {
2 var users []User
3 err := faker.FakeData(&users)
4 if err != nil {
5 return err
6 }
7 return c.JSON(users)
8})

Corremos nuevamente el proyecto y vemos que nos devuelve un listado de usuarios aleatorios (la cantidad de usuarios varia en cada solicitud que enviamos).

Imagen de prueba de listado de usuarios

Para añadirle un poco mas de complejidad al momento de hacer las pruebas de carga, vamos a añadir autenticación a nuestra api, utilizaremos jwt para esto, instalaremos los siguientes paquetes:

1go get -u github.com/gofiber/jwt/v3
2go get -u github.com/golang-jwt/jwt/v4

y ahora en el archivo main.go importamos los paquetes instalados:

1jwtware "github.com/gofiber/jwt/v3"
2"github.com/golang-jwt/jwt/v4"

Añadimos un endpoint (después del primer endpoint que añadimos "/") para autenticarnos y obtener un token, solamente validará el password recibido que sea igual a "admin" construirá el token y lo retornará:

1app.Post("/login", func(c *fiber.Ctx) error {
2 user := c.FormValue("user")
3 password := c.FormValue("password")
4
5 if password != "admin" {
6 return c.SendStatus(fiber.StatusUnauthorized)
7 }
8
9 claims := jwt.MapClaims{
10 "name": user,
11 "password": password,
12 }
13
14 token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
15
16 t, err := token.SignedString([]byte("TheBestSecret"))
17 if err != nil {
18 return c.SendStatus(fiber.StatusInternalServerError)
19 }
20
21 return c.JSON(fiber.Map{"token": t})
22})

Seguido a este endpoint, le indicaremos a fiber que utilice un middleware para validar el token recibido, importante saber que todos los endpoints definidos antes de esto estarán sin protección, los definidos después validaran que se reciba el token para poder retornar una respuesta (el secret debe ser el mismo).

1app.Use(jwtware.New(jwtware.Config{
2 SigningKey: []byte("TheBestSecret"),
3}))

Una vez agregado el endpoint y el middleware corremos nuevamente el proyecto e intentamos de nuevo obtener los usuarios (sin ningún cambio aún) y vemos que nos devuelve un error.

Imagen de prueba de listado de usuarios sin token

Seguido haremos "inicio de sesión" y veremos que nos devuelve el token:

Imagen de prueba de login

Volvemos a testear el endpoint de usuarios enviando el token y veremos que nos devuelve el listado de usuarios:

Imagen de prueba de listado de usuarios con token


Esto sería todo por la parte de Go para nuestra api, el archivo main.goquedaría así:

1package main
2
3import (
4 "os"
5
6 "github.com/bxcodec/faker/v3"
7 "github.com/gofiber/fiber/v2"
8 jwtware "github.com/gofiber/jwt/v3"
9 "github.com/golang-jwt/jwt/v4"
10)
11
12type User struct {
13 Id int
14 FirstName string `faker:"first_name"`
15 LastName string `faker:"last_name"`
16 Email string `faker:"email"`
17 UserName string `faker:"username"`
18 IPV4 string `faker:"ipv4"`
19 DomainName string `faker:"domain_name"`
20 Latitude float32 `faker:"lat"`
21 Longitude float32 `faker:"long"`
22 Phone string `faker:"phone_number"`
23}
24
25func main() {
26 app := fiber.New()
27
28 app.Get("/", func(c *fiber.Ctx) error {
29 return c.SendString("Hello, World!")
30 })
31
32 app.Post("/login", func(c *fiber.Ctx) error {
33 user := c.FormValue("user")
34 password := c.FormValue("password")
35
36 if password != "admin" {
37 return c.SendStatus(fiber.StatusUnauthorized)
38 }
39
40 claims := jwt.MapClaims{
41 "name": user,
42 "password": password,
43 }
44
45 token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
46
47 t, err := token.SignedString([]byte("TheBestSecret"))
48 if err != nil {
49 return c.SendStatus(fiber.StatusInternalServerError)
50 }
51
52 return c.JSON(fiber.Map{"token": t})
53 })
54
55 // Colocar antes de las rutas que se requieren proteger
56
57 app.Use(jwtware.New(jwtware.Config{
58 SigningKey: []byte("TheBestSecret"),
59 }))
60
61 app.Get("/users", func(c *fiber.Ctx) error {
62 var users []User
63 err := faker.FakeData(&users)
64 if err != nil {
65 return err
66 }
67 return c.JSON(users)
68 })
69
70 port := os.Getenv("PORT")
71
72 if port == "" {
73 port = "3000"
74 }
75
76 app.Listen(":" + port)
77}

Solo para hacer el tutorial mas interesante (para mí xD) publicaremos esta pequeña app de go en heroku, gracias al cli de heroku esto es un poco sencillo, lo primero es instalar el cli de heroku (Instrucciones de instalación), una vez instalado el cli, ejecutar el comando:

1heroku login

Esto abre un navegador y te permite ingresar a heroku

Imagen heroku login navegador Imagen heroku login consola

Luego ejecutar el comando:

1heroku create load-test-api-jp

El nombre de la aplicación puede no estar disponible, pueden intentar con otro nombre

Imagen heroku crear app

Antes de hacer deploy en el archivo go.mod le especificaremos a heroku que versión de go va a utilizar para evitar errores al desplegar, en el archivo go.mod al final añadimos // +heroku goVersion go1.18, quedaría así:

Imagen go.mod

Y ahora solo para poder hacer deploy a heroku, iniciaremos un proyecto de git

1git init
2heroku git:remote -a load-test-api-jp
3git add .
4git commit -m "Make it better :fire:"
5git push heroku master

Esto iniciará el deploy y al final nos mostrará la url de nuestra aplicación ya desplegada

Imagen heroku deploy

si abrimos en el navegador nos mostrará el resultado que configuramos para el endpoint raíz

Imagen heroku url

Listo, una vez desplegada la aplicación empezamos con la parte de las pruebas por medio de k6, pero empecemos definiendo primero que es, la página oficial de k6 lo define así:

Grafana k6 es una herramienta de pruebas de carga de código libre que hace fácil a equipos de software testear el rendimiento de sus aplicaciones. Con k6, puedes testear la fiabilidad y rendimiento de aplicaciones e identificar regresiones y errores más tempranamente. k6 te ayudará a construir aplicaciones rápidas y robustas que puedan escalar.

Lo primero será instalar el cli k6, podemos ver aquí las instrucciones de instalación

Una vez instalado el cli, veremos la forma más rápida de ejecutar una prueba rápida de k6 utilizando un paquete de npm llamado postman-to-k6 este convierte una colección existente de postman a un script de k6 para correrlo como prueba de carga, lo primero será abrir postman, crear una colección nueva y configurar un llamado a nuestro endpoint de la api anteriormente construida

Nos logeamos y copiamos el token de respuesta

Imagen postman login

Hacemos una petición de usuarios enviando el token anterior

Imagen postman get users

Le añadí un test al endpoint para ver si respondió status 200 y si recibió data

1pm.test("Status code is 200, and have data", function () {
2 pm.response.to.have.status(200);
3 pm.expect(pm.response.json().length).to.gt(0)
4});
Imagen postman get users test

Exportamos la colección

Imagen postman export

Desde una consola nos situamos en el directorio donde exportamos la colección e instalamos globalmente el paquete de npm postman-to-k6

1npm i -g @apideck/postman-to-k6

y ejecutamos el comando

1postman-to-k6 k6Tutorial.postman_collection.json -o k6-script.js

Esto tendrá como resultado un archivo javascript y un folder "libs" donde hay una serie de archivos que utilizará k6 para ejecutar la prueba de carga sobre nuestra colección

Imagen postman to k6

Por ultimo ejecutamos la prueba usando el cli de k6 con el siguiente comando

1k6 run --vus 200 --duration 30s k6-script.js

Este comando ejecutará una prueba de carga con 200 usuarios virtuales con una duración de 30 segundos (siempre se ejecuta durante unos segundos mas, mientras configura la ejecución de la prueba), y nos mostrará el resultado de la prueba

Imagen k6 run

En la salida podemos ver los resultados de la prueba ejecutada e incluso del test que definimos en postman, explicaré lo que entiendo de los mas relevantes del listado

Las letras en rojo son el resultado del test que definimos en postman para cada petición, para este caso 24 de las peticiones trajeron 0 usuarios (no fallaron, solo no trajeron usuarios) y en el test preguntamos si la longitud del array de respuesta era mayor a 0, por eso salen 24 test fallidos.

http_req_duration..............: avg=2.28s min=85.7ms med=1.44s max=44.57s p(90)=4.48s p(95)=7.28s

En este resultado podemos observar el promedio del tiempo de respuesta por petición, tiempo mínimo, medio y máximo y los porcentajes de los tiempos de respuesta de los 90%, 95%

http_req_failed................: 0.00% ✓ 0 ✗ 2712

Este resultado puede ser un poco confuso (para mí) podríamos entender de las peticiones todas fallaron, pero las que salen con ✓ son las que fallaron y las que salen con ✗ son las que no fallaron, en este caso son 2712 peticiones exitosas, 0.00% de porcentaje de fallos

http_reqs......................: 2712 51.210708/s

iteration_duration.............: avg=2.41s min=86.4ms med=1.55s max=44.58s p(90)=4.8s p(95)=7.37s

iterations.....................: 2712 51.210708/s

Estas 3 nos dicen la cantidad de peticiones totales, el promedio de tiempo de respuesta por petición, el tiempo mínimo, medio y máximo y los porcentajes de los tiempos de respuesta de los 90%, 95%, y la cantidad de peticiones por segundo que para este caso fueron 51.210708 peticiones por segundo.

Esta fue una prueba sencilla y rápida de ejecutar construida desde un test que definimos para una colección en postman, pero vayamos a un escenario donde debemos construir los tokens desde una base de usuarios (json) obtener ese token y luego utilizarlo para realizar aparte la petición a nuestra api (incluso obtener mas datos del usuario), para esto construiremos ya un proyecto un poco mas estructurado como ejemplo.

Lo siguiente que haremos es crear un proyecto de nodejs, ya que k6 aunque internamente funciona con Golang, la lógica que escribimos para los test lo hacemos utilizando javascript (aunque typescript para este ejemplo), lo que para mi lo hace mas rápido de aprender y su curva de aprendizaje es menor, entonces iniciamos el proyecto de nodejs e instalaremos algunas dependencias

1npm init -y
2npm i k6
3npm i -D @babel/cli @babel/core @babel/node @babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators @babel/plugin-proposal-object-rest-spread @babel/preset-env @babel/preset-typescript @types/k6 @types/node babel-loader terser-webpack-plugin webpack webpack-cli

Como anteriormente mencionamos este es un caso en el que necesitamos primero obtener los tokens de los usuarios de acuerdo a una base de usuarios (json) previamente definida y luego si utilizar estos para hacer la prueba, entonces lo que haremos será construir dos archivos: uno para el test y otro para la base de usuarios.

1* src/seed.ts
2* src/test.ts

Previo a escribir estos archivos configuramos 3 archivos necesarios para poder usar typescript:

.babelrc
1{
2 "presets": [
3 [
4 "@babel/typescript"
5 ]
6 ],
7 "plugins": [
8 "@babel/proposal-object-rest-spread",
9 [
10 "@babel/plugin-proposal-decorators",
11 {
12 "legacy": true
13 }
14 ],
15 [
16 "@babel/plugin-proposal-class-properties",
17 {
18 "loose": true
19 }
20 ]
21 ]
22}

tsconfig.json
1{
2 "compilerOptions": {
3 "experimentalDecorators": true,
4 // Target ECMAScript.
5
6 "target": "ES6",
7 // Search under node_modules for non-relative imports.
8
9 "moduleResolution": "node",
10 // Process & infer models from .js files.
11
12 "allowJs": true,
13 // Don't emit; allow Babel to transform files.
14
15 "noEmit": true,
16 // Enable strictest settings like strictNullChecks & noImplicitAny.
17
18 "strict": true,
19 // Disallow features that require cross-file information for emit.
20
21 "isolatedModules": true,
22 // Import non-ES modules as default imports.
23
24 "esModuleInterop": true,
25 "skipLibCheck": true,
26 "noImplicitThis": true,
27 "strictNullChecks": true
28 },
29 "include": [
30 "src"
31 ]
32}

webpack.config.js
1const path = require('path');
2const TerserPlugin = require("terser-webpack-plugin");
3
4module.exports = {
5 resolve: {
6 extensions: ['.ts', '.js'],
7 },
8 mode: 'production',
9 entry: {
10 seed: './src/seed.ts',
11 },
12 optimization: {
13 minimize: true,
14 minimizer: [new TerserPlugin()],
15 },
16 output: {
17 path: path.resolve(__dirname, 'dist'),
18 libraryTarget: 'commonjs',
19 filename: '[name].js'
20 },
21 module: {
22 rules: [
23 {
24 test: /\.ts$/,
25 // exclude: /node_modules/,
26 loader: 'babel-loader',
27 options: {
28 presets: [['@babel/typescript']],
29 plugins: [
30 '@babel/proposal-class-properties',
31 '@babel/proposal-object-rest-spread'
32 ]
33 }
34 }
35 ]
36 },
37 stats: {
38 colors: true
39 },
40 // target: 'web',
41 externals: /k6(\/.*)?/,
42 devtool: 'source-map',
43};

Teniendo estos tres archivos creados, procedemos a crear nuestro archivo json que usaremos como base para obtener los tokens (agregaré en total 100 usuarios, pero lo simplificaré)

data.json
1[
2 {
3 "user": "test1"
4 },
5 ...
6 {
7 "user": "test100"
8 }
9]

En este punto vale la pena mencionar que k6 cuenta con una serie de extensiones que nos ayudan en el desarrollo de nuestra prueba, como para nuestro caso necesitamos guardar un archivo con los tokens ya generados por cada usuario para posteriormente utilizarlo en el test, para este tutorial utilizaremos una extensión para escribir un archivo, muy parecido a lo que hace el paquete "fs" de node, para poder usar algunas extensiones de k6 necesitamos instalar un builder de paquetes para k6 (xk6) que nos ayudará con la extensión que necesitamos ahora, para instalarlo usaremos el comando:

1go install go.k6.io/xk6/cmd/xk6@latest

y luego para instalar la extensión

1xk6 build v0.36.0 --with github.com/avitalique/xk6-file@latest

En caso de error (como me sucedió a mi) esto me ayudó a solucionar el error

Los anteriores comandos instalarán la extensión file y construirán un binario que será el que usaremos para correr la prueba y usar la extension, una vez corrido los comandos dará una salida como esta

resultado xk6

Luego nuestro archivo seed.ts importamos la extensión instalada, el paquete http que viene dentro de k6, declararemos una variable para el password que definimos, y recorreremos uno por uno los usuarios para obtener su token, lo guardamos en un array y al final esto lo guardamos en un nuevo archivo json, quedará con el siguiente contenido:

src/seed.ts
1// @ts-ignore
2import file from 'k6/x/file';
3import http from "k6/http";
4
5const DEFAULT_PASSWORD = "admin";
6
7const data = require("../data.json");
8const resultDataFile = "resultData.json";
9
10export function setup() {
11 const authData: any = [];
12 data.forEach((userData: any) => {
13 const { user } = userData;
14 const resp = http.post("https://load-test-api-jp.herokuapp.com/login", {
15 user,
16 password: DEFAULT_PASSWORD,
17 });
18 userData.token = (<any>resp.json()).token;
19 authData.push(userData);
20 });
21 try {
22 file.writeString(resultDataFile, JSON.stringify(authData));
23 } catch (error) {
24 console.error(error);
25 }
26}
27
28export default () => {};

En nuestro package.json definiremos 3 scripts

1"scripts": {
2 "seed": "npm run build && ./k6 run ./dist/seed.js",
3 "test": "npm run build && ./k6 run ./dist/test.js",
4 "build": "webpack"
5}

y corremos el proceso de seed con el comando

1npm run seed

esto generará el archivo resultData.json con los tokens generados por cada usuario, adicionalmente podríamos guardar cualquier otra info necesaria para posteriormente usarla en la prueba

resultado seed

Como resultado tendríamos un archivo listo para ahora proceder a ejecutar nuestra prueba, por lo que procedemos ahora a crear un archivo para ejecutar el test.

Los archivos para k6, como pudimos tal vez observar en el archivo seed.ts se componen de una función setup que se ejecuta antes de nuestro test y una función exportada por defecto que es la que en teoría se encarga de ejecutar nuestras pruebas, pero además de esto podemos definir ciertas opciones para ejecutar nuestra prueba con distintos escenarios, k6 cuenta con una serie de "ejecutores" (executors) que nos ayuda a definir estos escenarios y no tener que definir en el comando de ejecución cuantos usuarios virtuales usar o cuanto tiempo ejecutar la prueba, sino que los definimos en estas opciones que k6 tendrá en cuenta para correr las pruebas, podemos definir varios escenarios pero para este caso definiremos solo uno que lo que haces es ejecutar un número fijo de iteraciones en un periodo de tiempo determinado, de igual modo haremos uso del json de usuarios generados en el seed para enviar solicitudes constantes a nuestra api, el archivo quedaría así:

src/test.ts
1import http from "k6/http";
2
3const users = require("../resultData.json");
4
5export const options = {
6 scenarios: {
7 constant_request_rate: {
8 executor: "constant-arrival-rate",
9 rate: 1000,
10 timeUnit: "1s",
11 duration: "30s",
12 preAllocatedVUs: 100,
13 maxVUs: 200,
14 },
15 },
16};
17
18export function setup() {}
19
20export default () => {
21 for (let user of users) {
22 if (user.token) {
23 const url = `https://load-test-api-jp.herokuapp.com/users`;
24 const params = {
25 headers: {
26 Authorization: `Bearer ${user.token}`,
27 },
28 };
29 http.get(url, params);
30 }
31 }
32};

Ya en el package.json colocamos previamente el script de build y el de test, pero nos falta añadir en la línea 10 del webpack.config.js la instrucción para transpilar el archivo de test

test: './src/test.ts',

webpack config

Ahora podemos correr nuestra prueba con el comando

1npm run test

Esto empezaría a ejecutar nuestro test

resultado test resultado test

El warning que aparece es porque mi maquina no soporta la cantidad de usuarios virtuales que definí xD

En los resultados podemos observar que en total hizo 6308 request, 105 request por segundo aproximadamente y cero por ciento de porcentaje de error.

En conclusión y desde mi punto de vista k6 es una forma rápida de ejecutar pruebas de carga, bastante sencilla ya que utiliza javascript para definir las pruebas y escenarios, con las extensiones también nos da la posibilidad y el paquete postman-to-k6 nos permite definir y ejecutar una prueba aún mas rápido en caso de estar familiarizado con postman.

Y bueno, espero haber sido lo bastante claro y explicativo durante el post y que se haya entendido todo, cualquier duda o sugerencia no dudes en contactarme, estoy en las redes como @JoralmoPro.

Nos vemos en línea.