📉 Pruebas de Rendimiento ​
¿Qué son? ​
Una Prueba de Rendimiento es una prueba de software apuntada a medir, en términos de rapidez, confiabilidad y eficiencia, cómo un sistema y sus componentes se comportan bajo a ciertas condiciones o cargas determinadas.
Importante
Cuando se habla de Pruebas de Carga, muchos lo utilizan como sinónimo de Pruebas de Rendimiento o Pruebas de Estrés. Es importante señalarles, como Ingenieros de Calidad, que tanto las de carga como las de estrés, son tipos de pruebas que se realizan dentro de las Pruebas de Rendimiento.
¿Por qué son importantes? ​
- Evalúan la eficacia y potencia de procesamiento del sistema, determinando si cumplen con los niveles de rendimiento esperados.
- Revelan diversos factores que pueden afectar positiva o negativamente la experiencia del usuario con un sistema.
- Ayudan a estimar las necesidades de capacidad futuras de un sistema.
- Pueden identificar problemas que podrÃan obstaculizar la finalización oportuna y presupuestada de un proyecto.
Proceso general ​
1. Diseño ​
- Escenarios: Decide cómo se realizarán las pruebas en el sistema. Determinan qué caracterÃsticas se probarán, simulan casos de usos/flujos y especifican los procedimientos de prueba.
- Planes: Implica delinear la estrategia general y los objetivos.
- Entornos: Implica establecer las condiciones y recursos necesarios para la realización de estas pruebas.
- Datos: Asegurar que los datos necesarios estén configurados correctamente y representen condiciones lo más reales posibles, teniendo en cuenta la distribución y las interacciones con las bases de datos.
2. Ejecución ​
En la ejecución de las pruebas, es necesario contar con un grupo de personas que cumplan roles especÃficos. No basta con que el Ingeniero de Calidad se siente frente a la pantalla, presione el botón "Iniciar" y espere a que el proceso finalice.
Estos roles deberán monitorear y analizar el comportamiento de los diferentes componentes de la infraestructura de la plataforma y los resultados que se generan, buenos o malos, con la finalidad de levantar un informe con todos los hallazgos, evidencias y decisiones que se tomaron o se tomarán.
Roles a tener presente
Arquitecto, LÃder Técnico, Ingeniero Cloud, Ingeniero DevOps, Desarrollador, Ingeniero de Redes, Ingeniero de Calidad
3. Análisis ​
Una vez ejecutadas las pruebas, se realizará un análisis de los resultados obtenidos para identificar posibles problemas de rendimiento e informarlos. Si se encuentran problemas, se crean planes para resolverlos y se vuelven a ejecutar las pruebas las veces que sea necesarias hasta que se llegue al resultado esperado.
Algunas consideraciones ​
1. Costo Monetario ​
Dada la cantidad de solicitudes y transacciones que se generan durante las ejecuciones de las pruebas, se debe tener presente que los costos monetarios pueden subir exponencialmente con respecto al uso normal de la plataforma.
2. Infraestructura ​
El entorno donde se realizará la prueba tiene que tener las mismas caracterÃsticas a nivel de memoria, cantidad de CPUs, capacidad de almacenamiento, configuración de escalamiento, regiones donde están albergados los componentes, conexiones simultáneas a bases de datos, cantidad de réplicas y otras caracterÃsticas que el ambiente Productivo. Si éste aún no existe, se puede realizar un estimado de lo que se va a requerir y, según los resultados obtenidos, se hacen ajustes a los parámetros según sea necesario.
3. Servicios externos ​
Muchas plataformas contienen integraciones con servicios externos, como, por ejemplo: envÃo de correos, sistemas de pagos, sistemas de geolocalización, almacenamiento de archivos, entre otros. Estos servicios pueden o no tener costos y/o cuotas de uso asociados a la interacción entre las plataformas, por lo que resulta necesario desactivar la integración o no ejecutar pruebas sobre aquellos elementos/componentes que interactúan con dichos servicios.
4. Cortafuegos (WAF) ​
Si la plataforma que se va a someter a pruebas tiene un cortafuegos, también conocido como WAF o Web Application Firewall, se deben agregar reglas y polÃticas a la configuración de éste, para que permita realizar un gran volumen de solicitudes, de lo contrario, las pruebas quedarán inválidas, ya que la aplicación y la infraestructura no las están recibiendo y, en consecuencia, no se está generando la carga esperada.
Tipos de Pruebas ​
1. Tabla comparativa TL;DR ​
Prueba | Objetivo | Duración | Carga | Patrón |
---|---|---|---|---|
Humo | Evaluar correcto funcionamiento de las pruebas | Corta (minutos) | MÃnima | Continuo |
Carga | Evaluar rendimiento frente a carga esperada | Corta a media (horas) | Normal de operación | Incremento gradual hasta nivel constante |
Estrés | Identificar puntos de quiebre bajo cargas extremas | Corta (minutos a horas) | Mayor a capacidad normal | Aumento repentino a nivel alto |
Resistencia | Garantizar estabilidad bajo carga sostenida | Larga (horas o dÃas) | Normal de operación | Nivel estable durante perÃodo prolongado |
Punta | Evaluar respuesta a aumentos de carga repentinos y extremos | Corta (minutos a horas) | Altas | Aumento y disminución rápidos |
2. Pruebas de Humo ​
Consisten en pruebas que se ejecutan cada vez que se crea o modifica un script dentro del set de pruebas, esto con el fin de validar que el funcionamiento sea el esperado y que la plataforma está respondiendo de acuerdo a los estándares mÃnimos.
#
# URL: https://www.artillery.io/
# Documentación: https://www.artillery.io/docs
#
# Ejecución:
# artillery run --solo performance.smoke.yml --output performance.smoke.json
# artillery report performance.smoke.json
#
config:
target: "https://dummyjson.com"
plugins:
expect: {}
# Obtener token
before:
flow:
- log: " [D] Obtener token de acceso"
- post:
url: "/auth/login"
json:
# https://dummyjson.com/users
username: "{{ $env.PLATFORM_USERNAME }}"
password: "{{ $env.PLATFORM_PASSWORD }}"
capture:
- json: $.accessToken
as: accessToken
# Casos de uso
scenarios:
- flow:
- log: " [D] Obtener listado de productos"
- get:
url: "/products"
headers:
authorization: "Bearer {{ accessToken }}"
expect:
- statusCode: 200
- hasProperty: "products"
- log: " [D] Obtener listado de recetas"
- get:
url: "/recipes"
headers:
authorization: "Bearer {{ accessToken }}"
expect:
- statusCode: 200
- hasProperty: "recipes"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/**
* URL: https://k6.io/
* Documentación: https://grafana.com/docs/k6/latest/
*
* Ejecución:
* k6 run performance.smoke.js
*/
import http from "k6/http";
import { check, fail } from "k6";
const HOST = "https://dummyjson.com";
/**
* Opciones generales de la prueba
*/
export const options = {
vus: 2, // Cantidad máxima de usuarios a crear durante la prueba
duration: "30s", // Duración de la prueba
};
/**
* Obtener token de acceso previo a iniciar las pruebas
*/
export function setup() {
let endpoint = `${HOST}/auth/login`;
let payload = {
// https://dummyjson.com/users
username: __ENV.PLATFORM_USERNAME,
password: __ENV.PLATFORM_PASSWORD,
};
let response = http.post(endpoint, payload);
return "accessToken" in response.json()
? response.json().accessToken
: null;
}
export default (data) => {
if (data == null) {
fail(
"Error en la obtención del token de acceso, no se ejecutarán los casos de uso."
);
}
// Obtener el listado de productos
let endpoint = `${HOST}/products`;
let params = {
headers: {
Authorization: `Bearer ${data}`,
},
};
let response = http.get(endpoint, params);
check(response, {
"El código de respuesta es el esperado (200)": (r) => r.status === 200,
"El cuerpo de la respuesta contiene el atributo esperado (products)": (
r
) => "products" in r.json(),
});
// Obtener listado de recetas
endpoint = `${HOST}/recipes`;
response = http.get(endpoint, params);
check(response, {
"El código de respuesta es el esperado (200)": (r) => r.status === 200,
"El cuerpo de la respuesta contiene el atributo esperado (recipes)": (
r
) => "recipes" in r.json(),
});
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
"""
URL: https://locust.io/
Documentación: https://docs.locust.io/en/stable/
Ejecución:
locust --headless -u 2 -t 30s -f performance.smoke.py
"""
from os import environ
from locust import HttpUser
from locust import task
class User(HttpUser):
"""
Representa un usuario de la plataforma.
"""
host: str = "https://dummyjson.com"
"""
Url de la plataforma que será evaluada.
"""
access_token: str | None = None
"""
Token de acceso para realizar solicitudes
"""
def on_start(self) -> None:
"""
Código a ejecutar cuando el usuario se crea.
"""
endpoint: str = self.host + "/auth/login"
credentials: dict[str, str] = {
"username": environ.get("PLATFORM_USERNAME", None),
"password": environ.get("PLATFORM_PASSWORD", None),
}
with self.client.post(
endpoint, data=credentials, catch_response=True
) as response:
if "accessToken" in response.json():
self.access_token = response.json()["accessToken"]
@task
def get_products(self) -> None:
"""Obtiene el listado de productos
Raises:
RuntimeError: cuando no se puede obtener el token de acceso y, en consecuencia, no se
pueden seguir haciendo solicitudes.
"""
if self.access_token is None:
raise RuntimeError(
"No se pudo obtener el token para realizar las solicitudes."
)
endpoint: str = self.host + "/products"
headers: dict[str, str] = {
"Authorization": self.access_token,
}
with self.client.get(
endpoint, headers=headers, catch_response=True
) as response:
if response.status_code != 200:
response.failure(
f"El código de la respuesta no es el esperado (200): {response.status_code} - {response.reason}"
)
if "products" not in response.json():
response.failure(
f"El cuerpo de la respuesta no contiene el atributo esperado (products): {response.text}"
)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
3. Pruebas de Carga ​
Es una prueba que ayuda a garantizar que un sistema pueda manejar la cantidad esperada de usuarios y transacciones de manera eficiente.
Beneficios
- Mejora el rendimiento: identifica los problemas de rendimiento antes de que afecten a los usuarios reales.
- Garantiza la escalabilidad: ayuda a comprender qué tan bien se adapta el sistema a una mayor carga.
- Mejora la confiabilidad: garantiza que el sistema siga siendo confiable en condiciones adversas.
#
# URL: https://www.artillery.io/
# Documentación: https://www.artillery.io/docs
#
# Ejecución:
# artillery run performance.load.yml --output performance.load.json
# artillery report performance.load.json
#
config:
target: "https://dummyjson.com"
http:
timeout: 30 # Máximo tiempo de espera de 30s
phases:
- name: "Carga gradual de usuarios"
duration: 300
rampTo: 40
arrivalRate: 1
- name: "Mantener carga final"
duration: 1800
arrivalRate: 40
rampTo: 40
- name: "Disminuir carga de usuarios"
duration: 300
rampTo: 0
arrivalRate: 1
plugins:
ensure: {}
expect: {}
ensure:
thresholds:
- http.response_time.p95: 250 # Tiempo de respuesta menor a 250ms para el percentil 95
- http.response_time.p99: 500 # Tiempo de respuesta menor a 500ms para el percentil 99
# Obtener token
before:
flow:
- log: " [D] Obtener token de acceso"
- post:
url: "/auth/login"
json:
# https://dummyjson.com/users
username: "{{ $env.PLATFORM_USERNAME }}"
password: "{{ $env.PLATFORM_PASSWORD }}"
capture:
- json: $.accessToken
as: accessToken
# Casos de uso
scenarios:
- flow:
- log: " [D] Obtener listado de productos"
- get:
url: "/products"
headers:
authorization: "Bearer {{ accessToken }}"
expect:
- statusCode: 200
- hasProperty: "products"
- log: " [D] Obtener listado de recetas"
- get:
url: "/recipes"
headers:
authorization: "Bearer {{ accessToken }}"
expect:
- statusCode: 200
- hasProperty: "recipes"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
/**
* URL: https://k6.io/
* Documentación: https://grafana.com/docs/k6/latest/
*
* Ejecución:
* k6 run performance.load.js
*/
import http from "k6/http";
import { check, fail } from "k6";
const HOST = "https://dummyjson.com";
/**
* Opciones generales de la prueba
*/
export const options = {
stages: [
{ duration: "5m", target: 40 }, // Carga gradual
{ duration: "30m", target: 40 }, // Mantener carga
{ duration: "5m", target: 0 }, // Disminuir carga
],
thresholds: {
http_req_failed: ["rate<0.01"], // Tasa de error debe ser menor al 1%
http_req_duration: [
"p(95)<250", // Tiempo de respuesta debe ser menor a 250ms para el percentil 95
"p(99)<500", // Tiempo de respuesta debe ser menor a 500ms para el percentil 99
],
},
};
/**
* Obtener token de acceso previo a iniciar las pruebas
*/
export function setup() {
let endpoint = `${HOST}/auth/login`;
let payload = {
// https://dummyjson.com/users
username: __ENV.PLATFORM_USERNAME,
password: __ENV.PLATFORM_PASSWORD,
};
let response = http.post(endpoint, payload);
return "accessToken" in response.json()
? response.json().accessToken
: null;
}
export default (data) => {
// Check de sanidad
if (data == null) {
fail(
"Error en la obtención del token de acceso, no se ejecutarán los casos de uso."
);
}
// Obtener el listado de productos
let endpoint = `${HOST}/products`;
let params = {
headers: {
Authorization: `Bearer ${data}`,
},
};
let response = http.get(endpoint, params);
check(response, {
"El código de respuesta es el esperado (200)": (r) => r.status === 200,
"El cuerpo de la respuesta contiene el atributo esperado (products)": (
r
) => "products" in r.json(),
});
// Obtener listado de recetas
endpoint = `${HOST}/recipes`;
response = http.get(endpoint, params);
check(response, {
"El código de respuesta es el esperado (200)": (r) => r.status === 200,
"El cuerpo de la respuesta contiene el atributo esperado (recipes)": (
r
) => "recipes" in r.json(),
});
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
"""
URL: https://locust.io/
Documentación: https://docs.locust.io/en/stable/
Ejecución:
locust --headless -f performance.load.py
"""
from os import environ
from locust import HttpUser
from locust import LoadTestShape
from locust import task
class TestShape(LoadTestShape):
"""
Configuración de las etapas de la prueba
"""
stages: list[dict[str, int]] = [
{"duration": 300, "users": 40, "spawn_rate": 1}, # Carga inicial de usuarios
{"duration": 2100, "users": 40, "spawn_rate": 40}, # Mantener carga
{"duration": 2400, "users": 0, "spawn_rate": 1}, # Disminuir carga
]
"""
Etapas de la prueba
"""
def tick(self) -> tuple[str, int] | None:
"""Devuelve una tupla con 2 elementos para controlar la carga de usuarios.
Returns:
tuple[str, int] | None: una tupla `(users, spawn_rate)` si el tiempo de ejecución total
es menor a la duración acumulada de la etapa, `None` en cualquier otro caso.
"""
run_time: float = self.get_run_time()
for stage in self.stages:
if stage["duration"] > run_time:
return (stage["users"], stage["spawn_rate"])
return None
class User(HttpUser):
"""
Representa un usuario de la plataforma.
"""
host: str = "https://dummyjson.com"
"""
Url de la plataforma que será evaluada.
"""
access_token: str | None = None
"""
Token de acceso para realizar solicitudes
"""
def on_start(self) -> None:
"""
Código a ejecutar cuando el usuario se crea.
"""
endpoint: str = self.host + "/auth/login"
credentials: dict[str, str] = {
"username": environ.get("PLATFORM_USERNAME", None),
"password": environ.get("PLATFORM_PASSWORD", None),
}
with self.client.post(
endpoint, data=credentials, catch_response=True
) as response:
if "accessToken" in response.json():
self.access_token = response.json()["accessToken"]
@task
def get_products(self) -> None:
"""Obtiene el listado de productos
Raises:
RuntimeError: cuando no se puede obtener el token de acceso y, en consecuencia, no se
pueden seguir haciendo solicitudes.
"""
if self.access_token is None:
raise RuntimeError(
"No se pudo obtener el token para realizar las solicitudes."
)
endpoint: str = self.host + "/products"
headers: dict[str, str] = {
"Authorization": self.access_token,
}
with self.client.get(
endpoint, headers=headers, catch_response=True
) as response:
if response.status_code != 200:
response.failure(
f"El código de la respuesta no es el esperado (200): {response.status_code} - {response.reason}"
)
if "products" not in response.json():
response.failure(
f"El cuerpo de la respuesta no contiene el atributo esperado (products): {response.text}"
)
@task
def get_recipes(self) -> None:
"""Obtiene el listado de recetas
Raises:
RuntimeError: cuando no se puede obtener el token de acceso y, en consecuencia, no se
pueden seguir haciendo solicitudes.
"""
if self.access_token is None:
raise RuntimeError(
"No se pudo obtener el token para realizar las solicitudes."
)
endpoint: str = self.host + "/recipes"
headers: dict[str, str] = {
"Authorization": self.access_token,
}
with self.client.get(
endpoint, headers=headers, catch_response=True
) as response:
if response.status_code != 200:
response.failure(
f"El código de la respuesta no es el esperado (200): {response.status_code} - {response.reason}"
)
if "recipes" not in response.json():
response.failure(
f"El cuerpo de la respuesta no contiene el atributo esperado (recipes): {response.text}"
)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
4. Pruebas de Estrés ​
A diferencia de las Pruebas de Carga, las de Estrés están diseñadas para determinar la solidez y estabilidad del sistema, llevándolo más allá de su capacidad operativa normal. Se trata de encontrar el punto de ruptura en donde el sistema deja de funcionar.
Beneficios
- Identifica los puntos débiles: ayuda a determinar dónde y cómo falla el sistema.
- Mejora la resiliencia: garantiza que el sistema pueda manejar una carga inesperada y recuperarse sin problemas.
- Mejora el rendimiento: destaca las áreas para optimizar el rendimiento y administrar mejor los recursos.
#
# URL: https://www.artillery.io/
# Documentación: https://www.artillery.io/docs
#
# Ejecución:
# artillery run performance.stress.yml --output performance.stress.json
# artillery report performance.stress.json
#
config:
target: "https://dummyjson.com"
http:
timeout: 30 # Máximo tiempo de espera de 30s
phases:
- name: "Carga gradual de usuarios"
duration: 300
rampTo: 1500
arrivalRate: 1
- name: "Mantener carga final"
duration: 1800
arrivalRate: 1500
rampTo: 1500
- name: "Disminuir carga de usuarios"
duration: 300
rampTo: 0
arrivalRate: 1
plugins:
ensure: {}
expect: {}
ensure:
thresholds:
- http.response_time.p95: 250 # Tiempo de respuesta menor a 250ms para el percentil 95
- http.response_time.p99: 500 # Tiempo de respuesta menor a 500ms para el percentil 99
# Obtener token
before:
flow:
- log: " [D] Obtener token de acceso"
- post:
url: "/auth/login"
json:
# https://dummyjson.com/users
username: "{{ $env.PLATFORM_USERNAME }}"
password: "{{ $env.PLATFORM_PASSWORD }}"
capture:
- json: $.accessToken
as: accessToken
# Casos de uso
scenarios:
- flow:
- log: " [D] Obtener listado de productos"
- get:
url: "/products"
headers:
authorization: "Bearer {{ accessToken }}"
expect:
- statusCode: 200
- hasProperty: "products"
- log: " [D] Obtener listado de recetas"
- get:
url: "/recipes"
headers:
authorization: "Bearer {{ accessToken }}"
expect:
- statusCode: 200
- hasProperty: "recipes"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
/**
* URL: https://k6.io/
* Documentación: https://grafana.com/docs/k6/latest/
*
* Ejecución:
* k6 run performance.stress.js
*/
import http from "k6/http";
import { check, fail } from "k6";
const HOST = "https://dummyjson.com";
/**
* Opciones generales de la prueba
*/
export const options = {
stages: [
{ duration: "5m", target: 1500 }, // Carga gradual
{ duration: "30m", target: 1500 }, // Mantener carga
{ duration: "5m", target: 0 }, // Disminuir carga
],
thresholds: {
http_req_failed: ["rate<0.01"], // Tasa de error debe ser menor al 1%
http_req_duration: [
"p(95)<250", // Tiempo de respuesta debe ser menor a 250ms para el percentil 95
"p(99)<500", // Tiempo de respuesta debe ser menor a 500ms para el percentil 99
],
},
};
/**
* Obtener token de acceso previo a iniciar las pruebas
*/
export function setup() {
let endpoint = `${HOST}/auth/login`;
let payload = {
// https://dummyjson.com/users
username: __ENV.PLATFORM_USERNAME,
password: __ENV.PLATFORM_PASSWORD,
};
let response = http.post(endpoint, payload);
return "accessToken" in response.json()
? response.json().accessToken
: null;
}
export default (data) => {
// Check de sanidad
if (data == null) {
fail(
"Error en la obtención del token de acceso, no se ejecutarán los casos de uso."
);
}
// Obtener el listado de productos
let endpoint = `${HOST}/products`;
let params = {
headers: {
Authorization: `Bearer ${data}`,
},
};
let response = http.get(endpoint, params);
check(response, {
"El código de respuesta es el esperado (200)": (r) => r.status === 200,
"El cuerpo de la respuesta contiene el atributo esperado (products)": (
r
) => "products" in r.json(),
});
// Obtener listado de recetas
endpoint = `${HOST}/recipes`;
response = http.get(endpoint, params);
check(response, {
"El código de respuesta es el esperado (200)": (r) => r.status === 200,
"El cuerpo de la respuesta contiene el atributo esperado (recipes)": (
r
) => "recipes" in r.json(),
});
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
"""
URL: https://locust.io/
Documentación: https://docs.locust.io/en/stable/
Ejecución:
locust --headless -f performance.stress.py
"""
from os import environ
from locust import HttpUser
from locust import LoadTestShape
from locust import task
class TestShape(LoadTestShape):
"""
Configuración de las etapas de la prueba
"""
stages: list[dict[str, int]] = [
{"duration": 300, "users": 1500, "spawn_rate": 1}, # Carga inicial de usuarios
{"duration": 2100, "users": 1500, "spawn_rate": 1500}, # Mantener carga
{"duration": 2400, "users": 0, "spawn_rate": 1}, # Disminuir carga
]
"""
Etapas de la prueba
"""
def tick(self) -> tuple[str, int] | None:
"""Devuelve una tupla con 2 elementos para controlar la carga de usuarios.
Returns:
tuple[str, int] | None: una tupla `(users, spawn_rate)` si el tiempo de ejecución total
es menor a la duración acumulada de la etapa, `None` en cualquier otro caso.
"""
run_time: float = self.get_run_time()
for stage in self.stages:
if stage["duration"] > run_time:
return (stage["users"], stage["spawn_rate"])
return None
class User(HttpUser):
"""
Representa un usuario de la plataforma.
"""
host: str = "https://dummyjson.com"
"""
Url de la plataforma que será evaluada.
"""
access_token: str | None = None
"""
Token de acceso para realizar solicitudes
"""
def on_start(self) -> None:
"""
Código a ejecutar cuando el usuario se crea.
"""
endpoint: str = self.host + "/auth/login"
credentials: dict[str, str] = {
"username": environ.get("PLATFORM_USERNAME", None),
"password": environ.get("PLATFORM_PASSWORD", None),
}
with self.client.post(
endpoint, data=credentials, catch_response=True
) as response:
if "accessToken" in response.json():
self.access_token = response.json()["accessToken"]
@task
def get_products(self) -> None:
"""Obtiene el listado de productos
Raises:
RuntimeError: cuando no se puede obtener el token de acceso y, en consecuencia, no se
pueden seguir haciendo solicitudes.
"""
if self.access_token is None:
raise RuntimeError(
"No se pudo obtener el token para realizar las solicitudes."
)
endpoint: str = self.host + "/products"
headers: dict[str, str] = {
"Authorization": self.access_token,
}
with self.client.get(
endpoint, headers=headers, catch_response=True
) as response:
if response.status_code != 200:
response.failure(
f"El código de la respuesta no es el esperado (200): {response.status_code} - {response.reason}"
)
if "products" not in response.json():
response.failure(
f"El cuerpo de la respuesta no contiene el atributo esperado (products): {response.text}"
)
@task
def get_recipes(self) -> None:
"""Obtiene el listado de recetas
Raises:
RuntimeError: cuando no se puede obtener el token de acceso y, en consecuencia, no se
pueden seguir haciendo solicitudes.
"""
if self.access_token is None:
raise RuntimeError(
"No se pudo obtener el token para realizar las solicitudes."
)
endpoint: str = self.host + "/recipes"
headers: dict[str, str] = {
"Authorization": self.access_token,
}
with self.client.get(
endpoint, headers=headers, catch_response=True
) as response:
if response.status_code != 200:
response.failure(
f"El código de la respuesta no es el esperado (200): {response.status_code} - {response.reason}"
)
if "recipes" not in response.json():
response.failure(
f"El cuerpo de la respuesta no contiene el atributo esperado (recipes): {response.text}"
)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
5. Pruebas de Resistencia o Remojo ​
Tienen como objetivo garantizar que un sistema pueda soportar una carga significativa durante un perÃodo de tiempo prolongado. Se trata de identificar problemas en la plataforma que sólo se presentan con el tiempo.
Beneficios
- Identifica problemas a largo plazo: detecta problemas como fugas de memoria y agotamiento lento de recursos.
- Garantiza estabilidad: verifica que el sistema se mantenga estable y confiable durante un uso prolongado.
- Optimiza la gestión de recursos: brinda información para una mejor asignación y gestión de recursos.
#
# URL: https://www.artillery.io/
# Documentación: https://www.artillery.io/docs
#
# Ejecución:
# artillery run performance.soak.yml --output performance.soak.json
# artillery report performance.soak.json
#
config:
target: "https://dummyjson.com"
http:
timeout: 30 # Máximo tiempo de espera de 30s
phases:
- name: "Carga gradual de usuarios"
duration: 300
rampTo: 40
arrivalRate: 1
- name: "Mantener carga final"
duration: 28800 # 8 horas
arrivalRate: 40
rampTo: 40
- name: "Disminuir carga de usuarios"
duration: 300
rampTo: 0
arrivalRate: 1
plugins:
ensure: {}
expect: {}
ensure:
thresholds:
- http.response_time.p95: 250 # Tiempo de respuesta menor a 250ms para el percentil 95
- http.response_time.p99: 500 # Tiempo de respuesta menor a 500ms para el percentil 99
# Obtener token
before:
flow:
- log: " [D] Obtener token de acceso"
- post:
url: "/auth/login"
json:
# https://dummyjson.com/users
username: "{{ $env.PLATFORM_USERNAME }}"
password: "{{ $env.PLATFORM_PASSWORD }}"
capture:
- json: $.accessToken
as: accessToken
# Casos de uso
scenarios:
- flow:
- log: " [D] Obtener listado de productos"
- get:
url: "/products"
headers:
authorization: "Bearer {{ accessToken }}"
expect:
- statusCode: 200
- hasProperty: "products"
- log: " [D] Obtener listado de recetas"
- get:
url: "/recipes"
headers:
authorization: "Bearer {{ accessToken }}"
expect:
- statusCode: 200
- hasProperty: "recipes"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
/**
* URL: https://k6.io/
* Documentación: https://grafana.com/docs/k6/latest/
*
* Ejecución:
* k6 run performance.soak.js
*/
import http from "k6/http";
import { check, fail } from "k6";
const HOST = "https://dummyjson.com";
/**
* Opciones generales de la prueba
*/
export const options = {
stages: [
{ duration: "5m", target: 40 }, // Carga gradual
{ duration: "8h", target: 40 }, // Mantener carga
{ duration: "5m", target: 0 }, // Disminuir carga
],
thresholds: {
http_req_failed: ["rate<0.01"], // Tasa de error debe ser menor al 1%
http_req_duration: [
"p(95)<250", // Tiempo de respuesta debe ser menor a 250ms para el percentil 95
"p(99)<500", // Tiempo de respuesta debe ser menor a 500ms para el percentil 99
],
},
};
/**
* Obtener token de acceso previo a iniciar las pruebas
*/
export function setup() {
let endpoint = `${HOST}/auth/login`;
let payload = {
// https://dummyjson.com/users
username: __ENV.PLATFORM_USERNAME,
password: __ENV.PLATFORM_PASSWORD,
};
let response = http.post(endpoint, payload);
return "accessToken" in response.json()
? response.json().accessToken
: null;
}
export default (data) => {
// Check de sanidad
if (data == null) {
fail(
"Error en la obtención del token de acceso, no se ejecutarán los casos de uso."
);
}
// Obtener el listado de productos
let endpoint = `${HOST}/products`;
let params = {
headers: {
Authorization: `Bearer ${data}`,
},
};
let response = http.get(endpoint, params);
check(response, {
"El código de respuesta es el esperado (200)": (r) => r.status === 200,
"El cuerpo de la respuesta contiene el atributo esperado (products)": (
r
) => "products" in r.json(),
});
// Obtener listado de recetas
endpoint = `${HOST}/recipes`;
response = http.get(endpoint, params);
check(response, {
"El código de respuesta es el esperado (200)": (r) => r.status === 200,
"El cuerpo de la respuesta contiene el atributo esperado (recipes)": (
r
) => "recipes" in r.json(),
});
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
"""
URL: https://locust.io/
Documentación: https://docs.locust.io/en/stable/
Ejecución:
locust --headless -f performance.soak.py
"""
from os import environ
from locust import HttpUser
from locust import LoadTestShape
from locust import task
class TestShape(LoadTestShape):
"""
Configuración de las etapas de la prueba
"""
stages: list[dict[str, int]] = [
{"duration": 300, "users": 40, "spawn_rate": 1}, # Carga inicial de usuarios
{"duration": 29100, "users": 40, "spawn_rate": 40}, # Mantener carga
{"duration": 29500, "users": 0, "spawn_rate": 1}, # Disminuir carga
]
"""
Etapas de la prueba
"""
def tick(self) -> tuple[str, int] | None:
"""Devuelve una tupla con 2 elementos para controlar la carga de usuarios.
Returns:
tuple[str, int] | None: una tupla `(users, spawn_rate)` si el tiempo de ejecución total
es menor a la duración acumulada de la etapa, `None` en cualquier otro caso.
"""
run_time: float = self.get_run_time()
for stage in self.stages:
if stage["duration"] > run_time:
return (stage["users"], stage["spawn_rate"])
return None
class User(HttpUser):
"""
Representa un usuario de la plataforma.
"""
host: str = "https://dummyjson.com"
"""
Url de la plataforma que será evaluada.
"""
access_token: str | None = None
"""
Token de acceso para realizar solicitudes
"""
def on_start(self) -> None:
"""
Código a ejecutar cuando el usuario se crea.
"""
endpoint: str = self.host + "/auth/login"
credentials: dict[str, str] = {
"username": environ.get("PLATFORM_USERNAME", None),
"password": environ.get("PLATFORM_PASSWORD", None),
}
with self.client.post(
endpoint, data=credentials, catch_response=True
) as response:
if "accessToken" in response.json():
self.access_token = response.json()["accessToken"]
@task
def get_products(self) -> None:
"""Obtiene el listado de productos
Raises:
RuntimeError: cuando no se puede obtener el token de acceso y, en consecuencia, no se
pueden seguir haciendo solicitudes.
"""
if self.access_token is None:
raise RuntimeError(
"No se pudo obtener el token para realizar las solicitudes."
)
endpoint: str = self.host + "/products"
headers: dict[str, str] = {
"Authorization": self.access_token,
}
with self.client.get(
endpoint, headers=headers, catch_response=True
) as response:
if response.status_code != 200:
response.failure(
f"El código de la respuesta no es el esperado (200): {response.status_code} - {response.reason}"
)
if "products" not in response.json():
response.failure(
f"El cuerpo de la respuesta no contiene el atributo esperado (products): {response.text}"
)
@task
def get_recipes(self) -> None:
"""Obtiene el listado de recetas
Raises:
RuntimeError: cuando no se puede obtener el token de acceso y, en consecuencia, no se
pueden seguir haciendo solicitudes.
"""
if self.access_token is None:
raise RuntimeError(
"No se pudo obtener el token para realizar las solicitudes."
)
endpoint: str = self.host + "/recipes"
headers: dict[str, str] = {
"Authorization": self.access_token,
}
with self.client.get(
endpoint, headers=headers, catch_response=True
) as response:
if response.status_code != 200:
response.failure(
f"El código de la respuesta no es el esperado (200): {response.status_code} - {response.reason}"
)
if "recipes" not in response.json():
response.failure(
f"El cuerpo de la respuesta no contiene el atributo esperado (recipes): {response.text}"
)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
6. Pruebas de Punta ​
Evalúan cómo un sistema maneja aumentos repentinos y extremos de carga, similares a picos de tráficos inesperados.
Beneficios
- Identifica puntos débiles: ayuda a determinar dónde falla el sistema bajo una tensión repentina.
- Mejora la resiliencia: garantiza que el sistema pueda manejar aumentos de tráfico inesperados.
- Mejora la recuperación: destaca la rapidez y eficacia con la que el sistema se recupera luego de ocurrido este tipo de evento.
#
# URL: https://www.artillery.io/
# Documentación: https://www.artillery.io/docs
#
# Ejecución:
# artillery run --solo performance.spike.yml --output performance.spike.json
# artillery report performance.spike.json
#
config:
target: "https://dummyjson.com"
http:
timeout: 30 # Máximo tiempo de espera de 30s
phases:
- name: "Carga explosiva"
duration: 60
rampTo: 10000
arrivalRate: 2
- name: "Reducción de carga"
duration: 30
rampTo: 0
arrivalRate: 5
plugins:
ensure: {}
expect: {}
ensure:
thresholds:
- http.response_time.p95: 250 # Tiempo de respuesta menor a 250ms para el percentil 95
- http.response_time.p99: 500 # Tiempo de respuesta menor a 500ms para el percentil 99
# Obtener token
before:
flow:
- log: " [D] Obtener token de acceso"
- post:
url: "/auth/login"
json:
# https://dummyjson.com/users
username: "{{ $env.PLATFORM_USERNAME }}"
password: "{{ $env.PLATFORM_PASSWORD }}"
capture:
- json: $.accessToken
as: accessToken
# Casos de uso
scenarios:
- flow:
- log: " [D] Obtener listado de productos"
- get:
url: "/products"
headers:
authorization: "Bearer {{ accessToken }}"
expect:
- statusCode: 200
- hasProperty: "products"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
/**
* URL: https://k6.io/
* Documentación: https://grafana.com/docs/k6/latest/
*
* Ejecución:
* k6 run performance.load.js
*/
import http from "k6/http";
import { check, fail } from "k6";
const HOST = "https://dummyjson.com";
/**
* Opciones generales de la prueba
*/
export const options = {
stages: [
{ duration: "2m", target: 10000 }, // Carga explosiva
{ duration: "1m", target: 0 }, // Disminuir carga
],
thresholds: {
http_req_failed: ["rate<0.01"], // Tasa de error debe ser menor al 1%
http_req_duration: [
"p(95)<250", // Tiempo de respuesta debe ser menor a 250ms para el percentil 95
"p(99)<500", // Tiempo de respuesta debe ser menor a 500ms para el percentil 99
],
},
};
/**
* Obtener token de acceso previo a iniciar las pruebas
*/
export function setup() {
let endpoint = `${HOST}/auth/login`;
let payload = {
// https://dummyjson.com/users
username: __ENV.PLATFORM_USERNAME,
password: __ENV.PLATFORM_PASSWORD,
};
let response = http.post(endpoint, payload);
return "accessToken" in response.json()
? response.json().accessToken
: null;
}
export default (data) => {
// Check de sanidad
if (data == null) {
fail(
"Error en la obtención del token de acceso, no se ejecutarán los casos de uso."
);
}
// Obtener el listado de productos
let endpoint = `${HOST}/products`;
let params = {
headers: {
Authorization: `Bearer ${data}`,
},
};
let response = http.get(endpoint, params);
check(response, {
"El código de respuesta es el esperado (200)": (r) => r.status === 200,
"El cuerpo de la respuesta contiene el atributo esperado (products)": (
r
) => "products" in r.json(),
});
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
"""
URL: https://locust.io/
Documentación: https://docs.locust.io/en/stable/
Ejecución:
locust --headless -f performance.spike.py
"""
from os import environ
from locust import HttpUser
from locust import LoadTestShape
from locust import task
class TestShape(LoadTestShape):
"""
Configuración de las etapas de la prueba
"""
stages: list[dict[str, int]] = [
{"duration": 120, "users": 10000, "spawn_rate": 2}, # Carga explosiva
{"duration": 180, "users": 0, "spawn_rate": 5}, # Disminuir carga
]
"""
Etapas de la prueba
"""
def tick(self) -> tuple[str, int] | None:
"""Devuelve una tupla con 2 elementos para controlar la carga de usuarios.
Returns:
tuple[str, int] | None: una tupla `(users, spawn_rate)` si el tiempo de ejecución total
es menor a la duración acumulada de la etapa, `None` en cualquier otro caso.
"""
run_time: float = self.get_run_time()
for stage in self.stages:
if stage["duration"] > run_time:
return (stage["users"], stage["spawn_rate"])
return None
class User(HttpUser):
"""
Representa un usuario de la plataforma.
"""
host: str = "https://dummyjson.com"
"""
Url de la plataforma que será evaluada.
"""
access_token: str | None = None
"""
Token de acceso para realizar solicitudes
"""
def on_start(self) -> None:
"""
Código a ejecutar cuando el usuario se crea.
"""
endpoint: str = self.host + "/auth/login"
credentials: dict[str, str] = {
"username": environ.get("PLATFORM_USERNAME", None),
"password": environ.get("PLATFORM_PASSWORD", None),
}
with self.client.post(
endpoint, data=credentials, catch_response=True
) as response:
if "accessToken" in response.json():
self.access_token = response.json()["accessToken"]
@task
def get_products(self) -> None:
"""Obtiene el listado de productos
Raises:
RuntimeError: cuando no se puede obtener el token de acceso y, en consecuencia, no se
pueden seguir haciendo solicitudes.
"""
if self.access_token is None:
raise RuntimeError(
"No se pudo obtener el token para realizar las solicitudes."
)
endpoint: str = self.host + "/products"
headers: dict[str, str] = {
"Authorization": self.access_token,
}
with self.client.get(
endpoint, headers=headers, catch_response=True
) as response:
if response.status_code != 200:
response.failure(
f"El código de la respuesta no es el esperado (200): {response.status_code} - {response.reason}"
)
if "products" not in response.json():
response.failure(
f"El cuerpo de la respuesta no contiene el atributo esperado (products): {response.text}"
)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107