Parte: 15 — Herramientas, editores y automatización (tooling) · Fuente: Documentación de GUT (Godot Unit Test) y Godot 4 (Unit testing / GDScript) ⏱️ Duración estimada: 80 min · Nivel: Avanzado
Un juego tiene mucho arte y física difíciles de testear, pero también tiene lógica pura —daño, inventario, economía— que se puede verificar de forma automática y barata. En esta clase adoptamos GUT (Godot Unit Test), el framework de pruebas unitarias más usado en Godot, para blindar esa lógica: escribimos tests que fallan en rojo cuando alguien rompe una regla de negocio, mucho antes de que el bug llegue al jugador.
Aprenderás a instalar GUT como addon, a escribir clases de test que extends GutTest con métodos test_*() y aserciones (assert_eq, assert_true, assert_null), y a ejecutarlos por línea de comandos con -s addons/gut/gut_cmdln.gd. Cerraremos el círculo de la parte: integraremos esta ejecución en el workflow de CI para que cada push corra la batería de tests automáticamente.
Al finalizar, el alumno podrá:
extends GutTest, métodos test_*() y aserciones adecuadas.gut_cmdln.gd y leer el resumen de resultados.| # | Tema | Por qué importa |
|---|---|---|
| 1 | Por qué testear juegos | Protege reglas de negocio (daño, economía) ante cambios. |
| 2 | Qué testear y qué no | La lógica pura sí; el render y el feel, no con unit tests. |
| 3 | GUT como addon | Aporta el runner, las aserciones y la integración con el editor. |
| 4 | Estructura de un test | extends GutTest, métodos test_*, before_each. |
| 5 | Aserciones | assert_eq, assert_true, assert_null expresan lo esperado. |
| 6 | Arrange-Act-Assert | Patrón que hace legible cada test. |
| 7 | Ejecución por CLI | Permite correr sin editor y en CI. |
| 8 | Integración en CI | Un test rojo debe romper el build. |
extends GutTest: clase base de la que hereda todo archivo de test. Clave: aporta las aserciones y el ciclo de vida.test_*(): función cuyo nombre empieza por test_; GUT la ejecuta como caso. Clave: cada uno prueba una cosa.assert_eq, assert_true, assert_null). Clave: define el criterio de éxito.before_each / after_each: ganchos que preparan y limpian el estado antes/después de cada test. Clave: aíslan casos entre sí.gut_cmdln.gd: script que ejecuta GUT sin abrir el editor. Clave: se invoca con godot -s.Necesitas Godot 4.x y el addon GUT compatible con Godot 4, disponible en el AssetLib del editor o en su repositorio de GitHub. Se instala copiando addons/gut/ al proyecto y activándolo en Project → Project Settings → Plugins. Los tests viven por convención en res://test/ (o res://tests/), cada archivo un conjunto de casos.
Escribiremos una lógica sencilla y comprobable —un sistema de salud e inventario— en res://src/ y sus tests en res://test/. La documentación de GUT está en https://gut.readthedocs.io/ y su repositorio en https://github.com/bitwes/Gut. Conviene añadir un .gutconfig.json en la raíz para fijar directorios y opciones de ejecución.
Vamos a testear la lógica de salud e inventario y a correr los tests por CLI.
res://src/salud.gd, una clase pura sin nodos visuales:class_name Salud
extends RefCounted
var maxima: int
var actual: int
func _init(maxima_inicial: int) -> void:
maxima = max(1, maxima_inicial)
actual = maxima
func recibir_dano(cantidad: int) -> void:
actual = clampi(actual - max(0, cantidad), 0, maxima)
func curar(cantidad: int) -> void:
actual = clampi(actual + max(0, cantidad), 0, maxima)
func esta_viva() -> bool:
return actual > 0
res://test/test_salud.gd. Cada método prueba una regla concreta:extends GutTest
var salud: Salud
func before_each() -> void:
salud = Salud.new(100) # Arrange comun a todos los casos
func test_empieza_a_vida_maxima() -> void:
assert_eq(salud.actual, 100, "Debe iniciar con la vida maxima")
func test_el_dano_resta_vida() -> void:
salud.recibir_dano(30)
assert_eq(salud.actual, 70)
func test_la_vida_no_baja_de_cero() -> void:
salud.recibir_dano(9999)
assert_eq(salud.actual, 0)
assert_false(salud.esta_viva(), "Con 0 de vida no debe seguir viva")
func test_curar_no_supera_el_maximo() -> void:
salud.recibir_dano(50)
salud.curar(9999)
assert_eq(salud.actual, 100)
res://test/test_inventario.gd (asumiendo un Inventario con agregar, buscar y total):extends GutTest
var inv: Inventario
func before_each() -> void:
inv = Inventario.new()
func test_inventario_vacio_no_encuentra_nada() -> void:
assert_null(inv.buscar("pocion"), "Sin items, buscar devuelve null")
func test_agregar_apila_cantidades() -> void:
inv.agregar("pocion", 2)
inv.agregar("pocion", 3)
assert_eq(inv.total("pocion"), 5, "Deben apilarse en la misma ranura")
func test_agregar_item_distinto_ocupa_ranura_nueva() -> void:
inv.agregar("espada", 1)
inv.agregar("escudo", 1)
assert_true(inv.total("espada") == 1 and inv.total("escudo") == 1)
-gexit hace que Godot cierre al terminar y devuelva el código de salida:godot --headless -s addons/gut/gut_cmdln.gd -gdir=res://test -gexit
# ...
# ---- Totals ----
# Scripts 2
# Tests 7
# Passing 7 Failing 0
# Codigo de salida: 0
- name: Correr tests GUT
run: |
./godot --headless -s addons/gut/gut_cmdln.gd -gdir=res://test -gexit
La lección observable: rompe una regla a propósito (haz que recibir_dano sume en vez de restar) y vuelve a correr; el test pasa a rojo, el código de salida deja de ser 0 y —en CI— el build entero se marca en rojo antes de exportar nada. La red de seguridad funciona.
Salud un método porcentaje() y un test que verifique assert_almost_eq con tolerancia.assert_signal_emitted para comprobar que Salud emite una señal murio al llegar a 0.gastar que no permita saldo negativo) y sus tests.before_all/after_all para preparar un recurso costoso una sola vez por script..gutconfig.json que fije dirs, include_subdirs y ejecuta con -gconfig.Escribe una lógica de juego pura (salud, inventario o economía) con al menos dos clases y una batería GUT de un mínimo de 8 tests que cubra casos normales y límite (cero, desbordamiento, entrada inválida). Ejecútala por CLI e intégrala como paso de CI que rompa el build ante un fallo.
Criterio de aceptación: godot --headless -s addons/gut/gut_cmdln.gd -gdir=res://test -gexit reporta todos los tests en verde y devuelve código de salida 0; al introducir un bug deliberado en la lógica, al menos un test falla y el código de salida deja de ser 0, lo que en el workflow de CI marca la ejecución en rojo.
| Síntoma | Causa y arreglo |
|---|---|
| GUT no encuentra los tests | El nombre del archivo o método no empieza por test_, o el -gdir apunta mal. Revisa convención y ruta. |
| El plugin GUT no aparece en el editor | No se activó en Project Settings → Plugins. Actívalo tras copiar addons/gut/. |
| CI pasa aunque un test falle | Falta -gexit; sin él Godot no propaga el código de salida. Añádelo al comando. |
| Tests que dependen del orden | Estado compartido no reiniciado. Usa before_each para recrear los objetos en cada caso. |
assert_eq con floats falla por decimales |
Comparación exacta de flotantes. Usa assert_almost_eq con una tolerancia. |
❓ ¿Debo testear el render y las animaciones? No con unit tests: son costosos y frágiles. Testea la lógica determinista; para lo visual, pruebas manuales o de integración.
❓ ¿GUT sirve para tests de integración? Sí, puede instanciar escenas y simular frames con await, pero empieza por unit tests de lógica pura, que dan más valor por menos esfuerzo.
❓ ¿Dónde coloco los tests? Por convención en res://test/, separados de res://src/. Así puedes excluir la carpeta de la exportación final.
❓ ¿Cuántos tests son suficientes? No busques un número: cubre las reglas de negocio y los casos límite (cero, máximos, entradas inválidas). La cobertura útil vale más que la total.
GUT cubre los tests unitarios, que son la base. La Parte 19 amplía la batería con las técnicas que encuentran lo que no se te ocurrió comprobar:
-s): https://docs.godotengine.org/en/4.3/tutorials/editor/command_line_tutorial.html · uso: lectura de respaldo del objetivo; no se usa en el procedimientoClase 263 - Control de versiones avanzado para equipos