17. Automatizar pruebas con archivos temporales y directorios de trabajo

17.1 Objetivo del tema

Muchas aplicaciones leen o escriben archivos. Para probar ese comportamiento no conviene usar archivos reales del proyecto, porque podríamos modificarlos, borrarlos o dejar basura entre ejecuciones.

En este tema usaremos archivos temporales y directorios de trabajo controlados con pytest. Trabajaremos principalmente con la fixture tmp_path y con pathlib.

Objetivo práctico: probar código que lee y escribe archivos sin afectar archivos reales del proyecto.

17.2 Por qué usar archivos temporales

Los archivos temporales permiten que cada prueba tenga su propio espacio de trabajo. Así evitamos que una prueba dependa de archivos dejados por otra.

Beneficios:

  • Las pruebas son más aisladas.
  • No se modifican archivos reales del proyecto.
  • La limpieza es automática o más sencilla.
  • Los escenarios se preparan desde la prueba.
  • Se reducen errores por rutas absolutas o archivos faltantes.

17.3 Crear un módulo para archivos

Crea app/archivos.py:

def leer_texto(ruta):
    return ruta.read_text(encoding="utf-8")


def escribir_texto(ruta, contenido):
    ruta.write_text(contenido, encoding="utf-8")


def contar_lineas(ruta):
    contenido = ruta.read_text(encoding="utf-8")
    return len(contenido.splitlines())

Estas funciones reciben una ruta y trabajan con archivos de texto.

17.4 Primera prueba con tmp_path

Crea tests/test_archivos.py:

from app.archivos import leer_texto


def test_leer_texto_devuelve_contenido_de_archivo_temporal(tmp_path):
    ruta = tmp_path / "mensaje.txt"
    ruta.write_text("Hola pytest", encoding="utf-8")

    assert leer_texto(ruta) == "Hola pytest"

tmp_path es una fixture de pytest que entrega una carpeta temporal como objeto Path.

17.5 Ejecutar la prueba

Ejecuta:

python -m pytest tests/test_archivos.py

pytest crea una carpeta temporal para la prueba. No necesitas crear ni borrar manualmente mensaje.txt en el proyecto.

17.6 Probar escritura de archivos

Agrega esta prueba:

from app.archivos import escribir_texto


def test_escribir_texto_crea_archivo_con_contenido(tmp_path):
    ruta = tmp_path / "salida.txt"

    escribir_texto(ruta, "Contenido generado")

    assert ruta.read_text(encoding="utf-8") == "Contenido generado"

La prueba verifica que la función escriba el contenido esperado.

17.7 Probar conteo de líneas

Agrega una prueba para contar_lineas:

from app.archivos import contar_lineas


def test_contar_lineas_devuelve_cantidad_de_lineas(tmp_path):
    ruta = tmp_path / "lineas.txt"
    ruta.write_text("uno\ndos\ntres\n", encoding="utf-8")

    assert contar_lineas(ruta) == 3

17.8 Crear subdirectorios temporales

También podemos crear carpetas dentro de tmp_path:

def test_escribir_texto_en_subdirectorio_temporal(tmp_path):
    carpeta = tmp_path / "reportes"
    carpeta.mkdir()
    ruta = carpeta / "reporte.txt"

    escribir_texto(ruta, "Reporte generado")

    assert ruta.exists()
    assert ruta.read_text(encoding="utf-8") == "Reporte generado"

Esto permite probar código que organiza archivos en subcarpetas.

17.9 Crear una función que guarda reportes

Agrega esta función en app/archivos.py:

def guardar_reporte(carpeta, nombre, contenido):
    carpeta.mkdir(parents=True, exist_ok=True)
    ruta = carpeta / nombre
    ruta.write_text(contenido, encoding="utf-8")
    return ruta

La función crea la carpeta si no existe, guarda el archivo y devuelve la ruta generada.

17.10 Probar creación automática de carpetas

Prueba guardar_reporte:

from app.archivos import guardar_reporte


def test_guardar_reporte_crea_carpeta_y_archivo(tmp_path):
    carpeta = tmp_path / "reportes"

    ruta = guardar_reporte(carpeta, "resultado.txt", "Todo correcto")

    assert ruta.exists()
    assert ruta.name == "resultado.txt"
    assert ruta.read_text(encoding="utf-8") == "Todo correcto"

17.11 Usar fixtures propias con tmp_path

Podemos crear una fixture que prepare un archivo temporal:

import pytest


@pytest.fixture
def archivo_con_lineas(tmp_path):
    ruta = tmp_path / "lineas.txt"
    ruta.write_text("uno\ndos\ntres\n", encoding="utf-8")
    return ruta

Uso:

def test_contar_lineas_con_fixture_devuelve_cantidad(archivo_con_lineas):
    assert contar_lineas(archivo_con_lineas) == 3

17.12 Ubicar fixtures compartidas en conftest.py

Si varias pruebas necesitan el mismo archivo temporal, puedes mover la fixture a tests/conftest.py:

import pytest


@pytest.fixture
def archivo_con_lineas(tmp_path):
    ruta = tmp_path / "lineas.txt"
    ruta.write_text("uno\ndos\ntres\n", encoding="utf-8")
    return ruta

Luego cualquier prueba puede recibir archivo_con_lineas como parámetro.

17.13 Probar archivos JSON temporales

Podemos crear JSON temporal dentro de una prueba:

import json


def test_leer_json_temporal(tmp_path):
    ruta = tmp_path / "usuario.json"
    datos = {"nombre": "Ana", "activo": True}
    ruta.write_text(json.dumps(datos), encoding="utf-8")

    contenido = json.loads(ruta.read_text(encoding="utf-8"))

    assert contenido["nombre"] == "Ana"
    assert contenido["activo"] is True

Esto sirve para probar lectores de configuración, importadores o validadores de archivos JSON.

17.14 Probar archivo inexistente

También podemos probar errores esperados:

import pytest

from app.archivos import leer_texto


def test_leer_texto_con_archivo_inexistente_lanza_file_not_found_error(tmp_path):
    ruta = tmp_path / "no_existe.txt"

    with pytest.raises(FileNotFoundError):
        leer_texto(ruta)

La ruta apunta a un archivo que no creamos dentro del directorio temporal.

17.15 Cambiar el directorio de trabajo con monkeypatch

Algunos códigos trabajan con rutas relativas al directorio actual. pytest ofrece monkeypatch.chdir para cambiarlo durante una prueba.

from pathlib import Path


def test_archivo_en_directorio_actual(tmp_path, monkeypatch):
    monkeypatch.chdir(tmp_path)
    Path("entrada.txt").write_text("dato", encoding="utf-8")

    assert Path("entrada.txt").read_text(encoding="utf-8") == "dato"

Al terminar la prueba, pytest restaura el estado correspondiente.

17.16 Evitar rutas absolutas en pruebas

No conviene escribir pruebas que dependan de rutas absolutas del equipo:

ruta = "C:/usuarios/ana/documentos/datos.txt"

Ese archivo puede no existir en otra computadora. Es mejor usar tmp_path o rutas relativas controladas dentro del proyecto.

17.17 No escribir en la raíz del proyecto

Evita que una prueba genere archivos como salida.txt directamente en la raíz del proyecto. Eso ensucia el directorio y puede afectar otras ejecuciones.

En lugar de esto:

ruta = Path("salida.txt")

usa:

ruta = tmp_path / "salida.txt"

17.18 Datos fijos vs archivos temporales

Usa archivos fijos en tests/data cuando representan casos de prueba estables que quieres conservar. Usa tmp_path cuando el archivo se crea durante la prueba o cuando quieres verificar escritura.

  • tests/data: datos conocidos y versionados.
  • tmp_path: archivos generados, modificados o descartables.

17.19 Problemas frecuentes

  • La prueba falla en otro equipo: revisa si usa rutas absolutas.
  • Quedan archivos basura: usa tmp_path en lugar de escribir en la raíz.
  • No se encuentra el archivo: verifica que la prueba lo cree antes de leerlo.
  • Problemas con caracteres: especifica encoding="utf-8".
  • Una prueba depende de otra: cada prueba debe crear sus propios archivos.

17.20 Ejercicio práctico

Crea en app/archivos.py una función copiar_archivo que reciba una ruta de origen y una ruta de destino. La función debe leer el texto del origen, escribirlo en el destino y devolver la ruta de destino.

Luego crea pruebas con tmp_path para verificar:

  • Que el archivo destino se crea.
  • Que el contenido copiado es correcto.
  • Que si el origen no existe se lanza FileNotFoundError.

17.21 Solución propuesta

Función en app/archivos.py:

def copiar_archivo(origen, destino):
    contenido = origen.read_text(encoding="utf-8")
    destino.write_text(contenido, encoding="utf-8")
    return destino

Pruebas:

import pytest

from app.archivos import copiar_archivo


def test_copiar_archivo_crea_archivo_destino(tmp_path):
    origen = tmp_path / "origen.txt"
    destino = tmp_path / "destino.txt"
    origen.write_text("contenido", encoding="utf-8")

    copiar_archivo(origen, destino)

    assert destino.exists()


def test_copiar_archivo_copia_el_contenido(tmp_path):
    origen = tmp_path / "origen.txt"
    destino = tmp_path / "destino.txt"
    origen.write_text("contenido", encoding="utf-8")

    copiar_archivo(origen, destino)

    assert destino.read_text(encoding="utf-8") == "contenido"


def test_copiar_archivo_con_origen_inexistente_lanza_error(tmp_path):
    origen = tmp_path / "no_existe.txt"
    destino = tmp_path / "destino.txt"

    with pytest.raises(FileNotFoundError):
        copiar_archivo(origen, destino)

Ejecuta:

python -m pytest tests/test_archivos.py

17.22 Lista de verificación

Antes de continuar con el próximo tema, verifica lo siguiente:

  • Usas tmp_path para archivos generados durante la prueba.
  • No escribes archivos temporales en la raíz del proyecto.
  • Usas Path para construir rutas.
  • Especificas encoding="utf-8" al leer y escribir texto.
  • Cada prueba prepara sus propios archivos.
  • Pruebas también casos de error, como archivos inexistentes.
  • La suite se ejecuta correctamente con python -m pytest.

17.23 Conclusión

En este tema automatizamos pruebas con archivos temporales y directorios de trabajo. Usar tmp_path permite crear escenarios seguros y aislados sin afectar archivos reales del proyecto.

En el próximo tema trabajaremos con variables de entorno y configuración por ambiente, una necesidad común en suites automatizadas que deben comportarse de forma distinta según el contexto.