¡FileSystem: El héroe anónimo de tus tests!

13-07-2026

¿Te has encontrado alguna vez en esa situación donde tus tests fallan misteriosamente solo porque el sistema de archivos decide trolearte? Quizá el archivo no existe, los permisos no cuadran o el entorno de CI se pone rebelde. Es como intentar hacer magia con una varita rota. El problema de depender directamente del sistema de archivos es que tus pruebas se vuelven frágiles, lentas y difíciles de mantener. ¡Y ni hablar de los bugs fantasma que aparecen solo en producción!

Pero no todo está perdido. Aquí entra en escena la interfaz FileSystem, el superhéroe que tu código necesita. Imagina que puedes controlar el sistema de archivos como si fuera un videojuego: puedes simular, espiar y manipular todo sin tocar el disco duro real. Así, tus tests dejan de depender del entorno y pasan a ser rápidos, fiables y hasta divertidos.

¿Cómo lo solucionamos? ¡Con una interfaz!

La clave está en abstraer el acceso al sistema de archivos mediante una interfaz. Así, tu código solo conoce los métodos que necesita y no le importa cómo se implementan por detrás. Puedes tener una versión real para producción y un mock para los tests. ¡Flexibilidad total!

Vamos a ver cómo sería esa interfaz y su implementación real. La idea es definir los métodos que realmente necesitas y que sean asíncronos, para que puedas usarlos en cualquier entorno moderno sin problemas de bloqueo.

// Definimos la interfaz FileSystem export interface FileSystem { readFile(path: string): Promise<string>; writeFile(path: string, content: string): Promise<void>; exists(path: string): Promise<boolean>; }

Esta interfaz es súper sencilla y directa. Solo tres métodos: leer, escribir y comprobar si existe un archivo. Así, cualquier clase que implemente FileSystem puede ser usada por tu lógica de negocio sin preocuparse de los detalles.

Implementación real con Node.js

Aquí tienes la implementación real usando el módulo fs de Node.js:

import { promises as fs } from 'fs'; class NodeFileSystem implements FileSystem { async readFile(path: string) { return await fs.readFile(path, 'utf-8'); } async writeFile(path: string, content: string) { await fs.writeFile(path, content, 'utf-8'); } async exists(path: string) { try { await fs.stat(path); return true; } catch { return false; } } }

Y listo, ya tienes una implementación funcional que puedes usar en producción.

Fake para tests: ¡Haz lo que quieras!

¿Y cómo probamos nuestro código sin tocar el disco? ¡Con un fake! Aquí tienes una implementación que simula el sistema de archivos en memoria:

// Fake de FileSystem para tests export class FakeFileSystem implements FileSystem { private files: Record<string, string> = {}; async readFile(path: string) { if (!(path in this.files)) throw new Error('Archivo no encontrado'); return this.files[path]; } async writeFile(path: string, content: string) { this.files[path] = content; } async exists(path: string) { return path in this.files; } }

Aunque en la industria solemos usar el término “mock” para cualquier doble de test, técnicamente esto es un fake porque implementa lógica funcional simplificada. Un mock verdadero se centraría en verificar que se llamaron ciertos métodos con ciertos parámetros, mientras que un fake realmente funciona: guarda datos en un diccionario en memoria y te los devuelve cuando los pides. Es una implementación alternativa que se comporta de forma similar a la real, pero más simple y rápida.

Este fake es la clave para tests rápidos y fiables. No hay acceso al disco, todo ocurre en memoria y puedes controlar exactamente qué archivos existen y cuáles no. Además, puedes simular errores fácilmente, lo que te permite probar casos límite y comportamientos inesperados sin miedo.

Testeando nivel pro

Ahora que tienes la interfaz y el fake, ¡es hora de probar! Imagina que tienes un caso de uso real para publicar artículos de un blog. Este caso de uso orquesta varias operaciones: verifica que no exista ya un artículo, añade metadata automática y guarda todo en el formato correcto:

// ArticlePublisher.ts - Caso de uso que orquesta lógica de negocio export class ArticlePublisher { constructor(private fs: FileSystem) {} async publish(slug: string, content: string) { const path = `articles/${slug}.md`; if (await this.fs.exists(path)) { throw new Error('Article already exists'); } const now = new Date().toISOString(); const articleWithMetadata = `--- published: ${now} --- ${content}`; await this.fs.writeFile(path, articleWithMetadata); return { path, publishedAt: now }; } }

Y así sería su test:

// ArticlePublisher.test.ts import { describe, it, expect } from 'vitest'; import { FakeFileSystem } from './FakeFileSystem'; import { ArticlePublisher } from './ArticlePublisher'; describe('ArticlePublisher', () => { it('should publish article with metadata', async () => { const fs = new FakeFileSystem(); const publisher = new ArticlePublisher(fs); const result = await publisher.publish('hello-world', '# Hello World\n\nMi primer artículo!'); expect(result.path).toBe('articles/hello-world.md'); expect(result.publishedAt).toBeDefined(); const savedContent = await fs.readFile('articles/hello-world.md'); expect(savedContent).toContain('published:'); expect(savedContent).toContain('# Hello World'); }); it('should throw error when article already exists', async () => { const fs = new FakeFileSystem(); await fs.writeFile('articles/existing-article.md', 'contenido previo'); const publisher = new ArticlePublisher(fs); await expect( publisher.publish('existing-article', 'nuevo contenido') ).rejects.toThrow('Article already exists'); }); it('should create article in correct path', async () => { const fs = new FakeFileSystem(); const publisher = new ArticlePublisher(fs); await publisher.publish('my-awesome-post', 'contenido'); expect(await fs.exists('articles/my-awesome-post.md')).toBe(true); }); });

¿Ves la diferencia? Ahora estamos testeando un caso de uso completo con lógica de negocio real. No solo guardamos archivos, sino que validamos duplicados, añadimos metadata automáticamente y retornamos información útil. Y todo esto sin tocar el disco: los tests son súper rápidos, no dependen del entorno y puedes probar todos los casos que quieras. Si quieres simular que un artículo ya existe, simplemente lo creas previamente en el fake. Si quieres verificar que la metadata se añadió correctamente, lees el archivo y lo compruebas. ¡Así de fácil!

Y aquí viene lo divertido: esta técnica no es exclusiva del sistema de archivos. Puedes usarla con cualquier dependencia externa que te dé dolores de cabeza en los tests. ¿Tu código habla con una base de datos? ¡Hazle una interfaz y un mock! ¿Consume APIs externas? ¡Interfaz y mock al rescate! ¿Usa caché, colas de mensajes, correo electrónico, servicios de autenticación, notificaciones push, almacenamiento en la nube, sensores o geolocalización? ¡Todo puede ser abstraído y mockeado! Así, tus tests se vuelven inmunes a los caprichos del entorno y tú puedes dormir tranquilo sabiendo que todo está bajo control.

La magia de este enfoque es que te da el poder de simular cualquier escenario, desde el más común hasta el más loco, sin depender de servicios reales ni de configuraciones imposibles. Tu código se vuelve más flexible, tus pruebas más fiables y tu vida como desarrollador mucho más divertida. Si algo falla, sabes que es por tu lógica y no por el universo conspirando contra ti.

Conclusión: ¡Tus tests nunca fueron tan felices!

En resumen, FileSystem es ese amigo que te cubre las espaldas en los tests y te da libertad para experimentar sin miedo. Si aún no lo usas, dale una oportunidad y verás cómo tus tests pasan de ser una pesadilla a una fiesta.

¡Larga vida a las interfaces, a los mocks y a los desarrolladores que no se rinden ante los bugs! Si tus tests fallan, que sea porque te quedaste sin café, no por culpa del sistema de archivos.