22. Probar lectura y escritura de archivos temporales

22.1 Objetivo del tema

Muchas aplicaciones leen y escriben archivos: configuraciones, reportes, exportaciones, logs o datos en formato JSON. Para probar ese código no conviene usar archivos reales del proyecto, porque una prueba podría modificar información importante o dejar archivos basura.

En este tema usaremos tmp_path, una fixture de pytest que crea una carpeta temporal para cada prueba.

Idea clave: una prueba con archivos debe crear sus propios datos, trabajar en una carpeta temporal y verificar el resultado sin depender de archivos externos.

22.2 Crear una carpeta de práctica

Crea un proyecto nuevo:

mkdir pytest-archivos-demo
cd pytest-archivos-demo

Si pytest no está instalado en el entorno activo:

python -m pip install pytest

22.3 Qué es tmp_path

tmp_path es una fixture incorporada de pytest. Cuando la agregamos como parámetro de una prueba, pytest entrega una ruta temporal representada como objeto Path.

def test_ejemplo(tmp_path):
    archivo = tmp_path / "datos.txt"
    archivo.write_text("Hola", encoding="utf-8")

    assert archivo.read_text(encoding="utf-8") == "Hola"

Cada prueba recibe su propia carpeta temporal. Eso ayuda a que las pruebas no se afecten entre sí.

22.4 Crear el módulo a probar

Crea un archivo llamado archivos.py:

import json
from pathlib import Path


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


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


def agregar_linea(ruta, linea):
    ruta = Path(ruta)
    with ruta.open("a", encoding="utf-8") as archivo:
        archivo.write(linea + "\n")


def contar_lineas(ruta):
    contenido = leer_texto(ruta)
    if contenido == "":
        return 0
    return len(contenido.splitlines())


def guardar_json(ruta, datos):
    ruta = Path(ruta)
    texto = json.dumps(datos, ensure_ascii=False, indent=2)
    ruta.write_text(texto, encoding="utf-8")


def leer_json(ruta):
    ruta = Path(ruta)
    texto = ruta.read_text(encoding="utf-8")
    return json.loads(texto)


def listar_archivos_txt(carpeta):
    carpeta = Path(carpeta)
    return sorted(archivo.name for archivo in carpeta.glob("*.txt"))

El módulo trabaja con Path para poder recibir rutas como texto o como objetos de pathlib.

22.5 Probar escritura de texto

Crea un archivo llamado test_archivos.py y agrega esta prueba:

from archivos import guardar_texto


def test_guardar_texto_crea_archivo(tmp_path):
    ruta = tmp_path / "saludo.txt"

    guardar_texto(ruta, "Hola Python")

    assert ruta.exists()
    assert ruta.read_text(encoding="utf-8") == "Hola Python"

La prueba verifica dos cosas: que el archivo exista y que su contenido sea correcto.

22.6 Probar lectura de texto

Para probar lectura, primero preparamos el archivo dentro de la carpeta temporal:

from archivos import leer_texto


def test_leer_texto_devuelve_contenido(tmp_path):
    ruta = tmp_path / "mensaje.txt"
    ruta.write_text("Contenido de prueba", encoding="utf-8")

    resultado = leer_texto(ruta)

    assert resultado == "Contenido de prueba"

La prueba no depende de un archivo creado manualmente. Ella misma prepara el escenario.

22.7 Probar agregado de líneas

Para código que abre un archivo en modo agregado:

from archivos import agregar_linea


def test_agregar_linea_al_final_del_archivo(tmp_path):
    ruta = tmp_path / "notas.txt"
    ruta.write_text("Primera\n", encoding="utf-8")

    agregar_linea(ruta, "Segunda")

    assert ruta.read_text(encoding="utf-8") == "Primera\nSegunda\n"

Este tipo de prueba detecta errores frecuentes con saltos de línea.

22.8 Probar conteo de líneas

Probemos una función que lee y procesa contenido:

from archivos import contar_lineas


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

    assert contar_lineas(ruta) == 3

También conviene cubrir el archivo vacío:

def test_contar_lineas_con_archivo_vacio(tmp_path):
    ruta = tmp_path / "vacio.txt"
    ruta.write_text("", encoding="utf-8")

    assert contar_lineas(ruta) == 0

22.9 Probar errores esperados

Si intentamos leer un archivo inexistente, Python debe lanzar FileNotFoundError:

import pytest


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

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

La prueba documenta el comportamiento esperado ante una ruta inválida.

22.10 Probar escritura y lectura de JSON

Los archivos JSON son muy comunes en configuraciones y datos de intercambio:

from archivos import guardar_json, leer_json


def test_guardar_y_leer_json(tmp_path):
    ruta = tmp_path / "producto.json"
    datos = {
        "nombre": "Teclado",
        "precio": 50000,
        "activo": True,
    }

    guardar_json(ruta, datos)
    resultado = leer_json(ruta)

    assert resultado == datos

La prueba valida el ciclo completo: guardar datos y volver a leerlos.

22.11 Probar carpetas temporales

tmp_path también permite crear subcarpetas:

from archivos import listar_archivos_txt


def test_listar_archivos_txt(tmp_path):
    carpeta = tmp_path / "reportes"
    carpeta.mkdir()
    (carpeta / "enero.txt").write_text("Enero", encoding="utf-8")
    (carpeta / "febrero.txt").write_text("Febrero", encoding="utf-8")
    (carpeta / "datos.csv").write_text("1,2,3", encoding="utf-8")

    resultado = listar_archivos_txt(carpeta)

    assert resultado == ["enero.txt", "febrero.txt"]

La función ignora el archivo datos.csv porque solo busca archivos con extensión .txt.

22.12 Crear fixtures con archivos preparados

Si varias pruebas necesitan el mismo archivo, podemos preparar una fixture:

@pytest.fixture
def archivo_con_tareas(tmp_path):
    ruta = tmp_path / "tareas.txt"
    ruta.write_text("comprar\nestudiar\npracticar\n", encoding="utf-8")
    return ruta


def test_contar_lineas_con_fixture(archivo_con_tareas):
    assert contar_lineas(archivo_con_tareas) == 3

La fixture devuelve la ruta lista para usar.

22.13 Archivo completo de pruebas

El archivo test_archivos.py puede quedar así:

import pytest

from archivos import (
    agregar_linea,
    contar_lineas,
    guardar_json,
    guardar_texto,
    leer_json,
    leer_texto,
    listar_archivos_txt,
)


def test_guardar_texto_crea_archivo(tmp_path):
    ruta = tmp_path / "saludo.txt"

    guardar_texto(ruta, "Hola Python")

    assert ruta.exists()
    assert ruta.read_text(encoding="utf-8") == "Hola Python"


def test_leer_texto_devuelve_contenido(tmp_path):
    ruta = tmp_path / "mensaje.txt"
    ruta.write_text("Contenido de prueba", encoding="utf-8")

    resultado = leer_texto(ruta)

    assert resultado == "Contenido de prueba"


def test_agregar_linea_al_final_del_archivo(tmp_path):
    ruta = tmp_path / "notas.txt"
    ruta.write_text("Primera\n", encoding="utf-8")

    agregar_linea(ruta, "Segunda")

    assert ruta.read_text(encoding="utf-8") == "Primera\nSegunda\n"


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

    assert contar_lineas(ruta) == 3


def test_contar_lineas_con_archivo_vacio(tmp_path):
    ruta = tmp_path / "vacio.txt"
    ruta.write_text("", encoding="utf-8")

    assert contar_lineas(ruta) == 0


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

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


def test_guardar_y_leer_json(tmp_path):
    ruta = tmp_path / "producto.json"
    datos = {
        "nombre": "Teclado",
        "precio": 50000,
        "activo": True,
    }

    guardar_json(ruta, datos)
    resultado = leer_json(ruta)

    assert resultado == datos


def test_listar_archivos_txt(tmp_path):
    carpeta = tmp_path / "reportes"
    carpeta.mkdir()
    (carpeta / "enero.txt").write_text("Enero", encoding="utf-8")
    (carpeta / "febrero.txt").write_text("Febrero", encoding="utf-8")
    (carpeta / "datos.csv").write_text("1,2,3", encoding="utf-8")

    resultado = listar_archivos_txt(carpeta)

    assert resultado == ["enero.txt", "febrero.txt"]


@pytest.fixture
def archivo_con_tareas(tmp_path):
    ruta = tmp_path / "tareas.txt"
    ruta.write_text("comprar\nestudiar\npracticar\n", encoding="utf-8")
    return ruta


def test_contar_lineas_con_fixture(archivo_con_tareas):
    assert contar_lineas(archivo_con_tareas) == 3

22.14 Ejecutar las pruebas

Desde la raíz del proyecto, ejecuta:

python -m pytest

La salida esperada será similar a:

collected 9 items

test_archivos.py .........                                      [100%]

9 passed in 0.05s

22.15 Inspeccionar la ruta temporal

Mientras aprendes, puedes imprimir la ruta temporal para entender dónde trabaja pytest:

def test_mostrar_tmp_path(tmp_path):
    print(tmp_path)
    assert tmp_path.exists()

Ejecuta con -s para ver la salida de print:

python -m pytest -s

En pruebas reales evita dejar print innecesarios.

22.16 tmp_path frente a archivos reales

Evita pruebas que dependan de rutas fijas como C:\datos\archivo.txt o de archivos creados manualmente. Esas pruebas suelen fallar en otra computadora o cuando cambia la ubicación del proyecto.

Una buena prueba con archivos debe poder ejecutarse desde cero en cualquier equipo, sin preparación manual.

22.17 Errores frecuentes

  • Olvidar encoding: usa encoding="utf-8" al leer y escribir texto.
  • Usar archivos reales: puede ensuciar el proyecto o fallar en otra máquina.
  • No probar archivo vacío: muchas funciones fallan con contenido vacío.
  • Confundir texto y JSON: JSON debe guardarse y leerse con conversión explícita.
  • Depender del orden del sistema de archivos: si necesitas orden, usa sorted.

22.18 Comandos usados en este tema

mkdir pytest-archivos-demo
cd pytest-archivos-demo
python -m pip install pytest
python -m pytest
python -m pytest -v
python -m pytest -s
python -m pytest test_archivos.py::test_guardar_y_leer_json -v

22.19 Qué debes recordar de este tema

  • tmp_path crea una carpeta temporal por prueba.
  • Las pruebas deben preparar sus propios archivos.
  • Path facilita construir rutas portables.
  • Conviene probar lectura, escritura, errores y archivos vacíos.
  • Para JSON, prueba el ciclo completo de guardar y leer.
  • Si el orden importa, ordénalo explícitamente antes de comparar.

22.20 Conclusión

En este tema probamos código que lee y escribe archivos usando carpetas temporales. Con tmp_path, cada prueba puede crear sus propios archivos sin afectar el proyecto real.

En el próximo tema veremos cómo probar entrada y salida por consola, incluyendo texto impreso con print y datos ingresados por el usuario.