El modelo puro: la maquina dice QUE, la implementacion dice COMO
La pieza que corona el nivel: una maquina de XState bien escrita no contiene sus efectos, solo los NOMBRA. El grafo de estados, las transiciones, los guards y las actions referenciados por nombre forman una descripcion pura —el QUE hacer— que es datos: portable, visualizable y verificable. Las implementaciones de esos nombres —la peticion real, el log real, la validacion real— viven aparte, se declaran en setup y se pueden intercambiar con provide —el COMO hacerlo—. Esta separacion no es cosmetica: es exactamente lo que permite ejercitar toda la logica de la maquina en un test sin que ni una peticion salga de verdad, sustituyendo el COMO por dobles y afirmando sobre el QUE. Esta leccion demuestra por que la pureza del modelo es la fuente de su testabilidad.
Todo lo que aprendiste en este nivel —el context, el assign, los guards, las actions— converge en una sola idea que separa a quien usa XState de quien lo entiende: una maquina no ejecuta su logica, la DESCRIBE. Cuando escribes una maquina bien hecha, el grafo de estados y sus aristas no contienen la peticion de red ni el log ni la validacion; contienen NOMBRES que apuntan a ellos. Ese grafo con nombres es una especificacion pura —el QUE debe ocurrir— hecha de datos planos. Las implementaciones concretas de esos nombres —el COMO— viven en otro sitio, se enchufan con setup y se pueden cambiar con provide. Esta frontera entre la descripcion y su realizacion es la razon ultima por la que una maquina de estados es testeable, visualizable y verificable de formas que ningun amasijo de if y useEffect permite jamas. Es el pago final de haber sido disciplinado con la pureza.
- Ver la maquina como una descripcion pura que referencia actions, guards y actores por NOMBRE, no por implementacion.
- Usar
setuppara declarar los tipos y las implementaciones —el COMO— de esos nombres. - Usar
providepara intercambiar implementaciones y sustituir efectos reales por dobles en los tests. - Ejercitar la logica de una maquina sin ejecutar efectos, con actores mockeados o con la funcion de transicion pura.
QUE frente a COMO: el grafo solo nombra
Observa una maquina de carga y fijate en que NO aparece: no hay ningun fetch, ningun console.log, ninguna validacion escrita en linea. Hay nombres —cargarDatos, guardaResultado, notifica— que apuntan a implementaciones definidas aparte. El cuerpo de createMachine es pura estructura: estados, transiciones y referencias.
import { setup, assign, fromPromise } from 'xstate'
const maquina = setup({
types: {
context: {} as { datos: string | null; error: string | null },
events: {} as { type: 'CARGA' },
},
// el COMO: implementaciones de cada nombre
actors: {
cargarDatos: fromPromise(async () => {
const r = await fetch('/api/datos')
return r.text()
}),
},
actions: {
guardaResultado: assign({ datos: ({ event }: any) => event.output }),
notifica: () => console.log('carga completa'),
},
}).createMachine({
// el QUE: estructura pura que solo nombra
context: { datos: null, error: null },
initial: 'inactivo',
states: {
inactivo: { on: { CARGA: 'cargando' } },
cargando: {
invoke: {
src: 'cargarDatos', // nombre, no implementacion
onDone: { target: 'exito', actions: ['guardaResultado', 'notifica'] },
onError: 'error',
},
},
exito: {},
error: {},
},
})
El cuerpo de la maquina se lee como un contrato: al recibir CARGA pasa a cargando, invoca lo que sea que cargarDatos haga, y si termina bien guarda y notifica. NO dice como se cargan los datos ni que significa notificar. Esa ignorancia deliberada es su fuerza: la misma descripcion sirve con datos reales, con datos falsos o con datos que fallan a proposito, segun que enchufes detras de cada nombre.
Reconoce el patron: la maquina depende de comportamientos abstractos —nombres— y alguien externo le inyecta las implementaciones concretas. Es exactamente la inyeccion de dependencias que viste en TCA con sus dependencies y en Elm con su runtime de efectos. La familia unidireccional entera comparte esta idea: la logica de negocio se escribe contra interfaces, no contra implementaciones, y el entorno se enchufa por fuera. XState lo materializa con setup para declarar y provide para sustituir. Que la misma solucion reaparezca en Swift, en Elm y en TypeScript no es casualidad: es la unica forma conocida de que la logica sea a la vez pura y util.
setup y provide: declarar e intercambiar el COMO
setup es donde declaras los tipos y las implementaciones de todos los nombres que la maquina usara. Todo lo que pongas ahi es el COMO por defecto. Pero la pieza que desbloquea la testabilidad es provide: llamar a .provide(...) sobre una maquina devuelve una maquina NUEVA, identica en estructura, con las implementaciones que indiques sustituidas. La descripcion no cambia; solo cambia el relleno de los nombres.
// para un test: misma maquina, otro COMO
const maquinaBajoTest = maquina.provide({
actors: {
cargarDatos: fromPromise(async () => 'datos de prueba'), // no toca la red
},
actions: {
notifica: () => {}, // efecto silenciado
},
})
Con esto, maquinaBajoTest recorre EXACTAMENTE la misma logica que la de produccion —los mismos estados, guards y transiciones— pero sin que salga una sola peticion ni se imprima nada. Has cambiado el COMO sin tocar el QUE.
Aqui esta la regla que decide si tu maquina sera testeable: provide solo puede reemplazar implementaciones REFERENCIADAS POR NOMBRE. Si escribes un fetch en linea dentro de un invoke, o una funcion anonima como action directamente en la transicion, provide no tiene nada que agarrar y no podras sustituirlo en un test. La disciplina, por tanto, es tajante: nombra en el cuerpo de la maquina, implementa en setup. Cada efecto en linea es una dependencia que acabas de soldar y que ya no podras mockear. Escribir maquinas testeables es, en gran parte, el habito de no dejar nunca una implementacion anonima donde deberia haber un nombre.
flowchart LR Q[maquina: el QUE estructura pura] --> P[provide] H1[COMO real: fetch y log] --> P H2[COMO de test: mocks] --> P P --> R[maquina lista para ejecutar o testear] style Q fill:#cba6f7,color:#11111b style H1 fill:#89b4fa,color:#11111b style H2 fill:#a6e3a1,color:#11111b
Testabilidad: ejercitar la logica sin el mundo
La separacion rinde su fruto en tres estilos de test, de menos a mas puro. El primero es de integracion ligera: creas un actor con la maquina mockeada por provide, le envias eventos y afirmas sobre el snapshot.
import { createActor } from 'xstate'
const actor = createActor(maquinaBajoTest).start()
actor.send({ type: 'CARGA' })
// tras resolver el actor mockeado:
actor.getSnapshot().matches('exito') // true
actor.getSnapshot().context.datos // 'datos de prueba'
El segundo estilo prescinde del actor por completo. XState expone la transicion como FUNCION PURA: dada una maquina, un snapshot y un evento, calcula el siguiente snapshot sin arrancar nada y sin disparar efectos. Es la encarnacion literal de que la maquina es una funcion de estado y evento.
import { getInitialSnapshot, getNextSnapshot } from 'xstate'
const s0 = getInitialSnapshot(maquina, undefined)
const s1 = getNextSnapshot(maquina, s0, { type: 'CARGA' })
s1.value // 'cargando' -> transicion calculada, cero efectos ejecutados
El tercer estilo es el testing basado en modelo: como el grafo es datos, una herramienta puede RECORRERLO sola y generar todos los caminos posibles hasta cada estado, convirtiendo tu maquina en su propia bateria de pruebas.
import { getShortestPaths } from '@xstate/graph'
const caminos = getShortestPaths(maquina) // un camino de eventos hasta cada estado alcanzable
// ejecuta cada camino y afirma que la maquina llega donde el modelo predice
La estrategia se resume en una frase. Lo que quieres verificar es la LOGICA —que ante estos eventos la maquina llega a este estado con este context, que este guard bloquea aquella transicion, que este orden de actions se respeta—: eso es el QUE, y se testea enviando eventos y afirmando sobre snapshots, o calculando transiciones puras. Lo que NO quieres ejecutar es el COMO —la red, el reloj, el disco—: eso se sustituye con provide por dobles deterministas. Un buen test de maquina no comprueba que el fetch funciona; comprueba que la maquina reacciona correctamente a que el fetch resuelva o falle. Separar ambas cosas es lo que hace que tus tests sean rapidos, deterministas y que no se rompan cuando cambia la implementacion sin cambiar la logica.
El giro final del nivel es dejar de ver la maquina como codigo que resulta estar bien organizado, y empezar a verla como lo que de verdad es: una ESPECIFICACION FORMAL de un comportamiento, que ademas se puede ejecutar. Todo lo que hiciste en las lecciones previas apuntaba aqui. Guardar los datos cuantitativos en el context en vez de en estados mantuvo el grafo finito y por tanto analizable. Actualizar solo con assign inmutable hizo que cada transicion produjera un estado nuevo y reproducible. Poner las condiciones en guards puros concentro todas las decisiones en las aristas, donde una herramienta puede leerlas. Aislar los efectos y nombrarlos separo lo que la maquina ES de lo que la maquina PROVOCA. Sumadas, estas disciplinas convierten tu logica en un objeto matematico: una funcion pura del par estado mas context y del evento, con los efectos inyectados por fuera. Y sobre un objeto asi puedes hacer lo que sobre el codigo imperativo es imposible: dibujarlo automaticamente, recorrerlo para generar sus propios tests, calcular cualquier transicion sin ejecutarla, y sustituir el mundo entero por dobles con una linea. La maquina describe QUE debe pasar; setup y provide deciden COMO; y esa frontera, mantenida con rigor, es lo que eleva el estado de una app de “algo que funciona y nadie se atreve a tocar” a “una especificacion que puedes leer, verificar y cambiar con confianza”. Ese es, al final, el sentido entero de modelar el estado como una maquina: no organizar mejor el codigo, sino convertir el comportamiento en algo sobre lo que se puede razonar.
- Toma la maquina de carga de la leccion y comprueba que su cuerpo no contiene ningun efecto en linea: solo nombres. Si encuentras uno, extraelo a
setup. - Escribe un test que use
providepara sustituir el actorcargarDatospor uno que resuelva con datos fijos, enviaCARGAy afirma que la maquina termina enexitocon esos datos. - Escribe una variante del test donde el actor mockeado RECHACE, y afirma que la maquina termina en
errorsin que salga ninguna peticion real. - Verifica una transicion con
getNextSnapshotsin crear ningun actor, y explica por que ese estilo no ejecuta ni un solo efecto. - Usa
getShortestPathsde@xstate/graphpara listar los caminos hasta cada estado y convierte uno de ellos en un test que recorra la maquina evento a evento. - Argumenta en tres frases por que un
fetchescrito en linea dentro de la maquina destruye las cinco propiedades —visualizar, recorrer, calcular, mockear, verificar— que la pureza te regala.