Muchos programas leen y escriben archivos. En una prueba unitaria, no siempre queremos depender de archivos reales del sistema: pueden no existir, cambiar de contenido o dejar residuos después de la ejecución.
En este tema veremos cómo probar lectura y escritura usando mock_open, patch, funciones inyectadas y tmp_path de pytest.
Supongamos esta función:
def leer_configuracion(ruta):
with open(ruta, "r", encoding="utf-8") as archivo:
return archivo.read()
Si la prueba usa un archivo real, depende de que exista en una ruta concreta. Podemos evitarlo reemplazando open.
mock_open crea un mock preparado para comportarse como open en muchos casos simples:
from unittest.mock import mock_open, patch
from configuracion import leer_configuracion
def test_leer_configuracion():
abrir_archivo = mock_open(read_data="modo=debug")
with patch("builtins.open", abrir_archivo):
contenido = leer_configuracion("config.txt")
assert contenido == "modo=debug"
abrir_archivo.assert_called_once_with("config.txt", "r", encoding="utf-8")
La prueba no crea ni lee un archivo real.
Si el código llama directamente a open(...), normalmente ese nombre se resuelve desde builtins. Por eso usamos:
patch("builtins.open", abrir_archivo)
Si el módulo importara open de otra forma o usara una función propia, habría que parchear el nombre usado por ese módulo.
Función:
def leer_usuarios(ruta):
with open(ruta, "r", encoding="utf-8") as archivo:
return [linea.strip() for linea in archivo if linea.strip()]
Prueba:
def test_leer_usuarios():
abrir_archivo = mock_open(read_data="Ana\n\nLuis\n")
with patch("builtins.open", abrir_archivo):
usuarios = leer_usuarios("usuarios.txt")
assert usuarios == ["Ana", "Luis"]
mock_open puede simular contenido línea por línea para casos simples.
Función:
def guardar_reporte(ruta, contenido):
with open(ruta, "w", encoding="utf-8") as archivo:
archivo.write(contenido)
Prueba:
def test_guardar_reporte():
abrir_archivo = mock_open()
with patch("builtins.open", abrir_archivo):
guardar_reporte("reporte.txt", "resultado final")
abrir_archivo.assert_called_once_with("reporte.txt", "w", encoding="utf-8")
archivo = abrir_archivo()
archivo.write.assert_called_once_with("resultado final")
Verificamos que se intentó escribir el contenido correcto.
Si el código abre en modo append:
def agregar_log(ruta, mensaje):
with open(ruta, "a", encoding="utf-8") as archivo:
archivo.write(mensaje + "\n")
La prueba debe verificar el modo "a":
def test_agregar_log():
abrir_archivo = mock_open()
with patch("builtins.open", abrir_archivo):
agregar_log("app.log", "Inicio")
abrir_archivo.assert_called_once_with("app.log", "a", encoding="utf-8")
abrir_archivo().write.assert_called_once_with("Inicio\n")
Podemos hacer que open lance FileNotFoundError:
def cargar_configuracion_o_default(ruta):
try:
with open(ruta, "r", encoding="utf-8") as archivo:
return archivo.read()
except FileNotFoundError:
return "modo=prod"
Prueba:
def test_cargar_configuracion_o_default_si_no_existe():
abrir_archivo = mock_open()
abrir_archivo.side_effect = FileNotFoundError
with patch("builtins.open", abrir_archivo):
contenido = cargar_configuracion_o_default("config.txt")
assert contenido == "modo=prod"
Función:
import json
def leer_usuario_json(ruta):
with open(ruta, "r", encoding="utf-8") as archivo:
return json.load(archivo)
Prueba:
def test_leer_usuario_json():
abrir_archivo = mock_open(read_data='{"id": 1, "nombre": "Ana"}')
with patch("builtins.open", abrir_archivo):
usuario = leer_usuario_json("usuario.json")
assert usuario == {"id": 1, "nombre": "Ana"}
El parser JSON trabaja con el objeto de archivo simulado.
Función:
def guardar_usuario_json(ruta, usuario):
with open(ruta, "w", encoding="utf-8") as archivo:
json.dump(usuario, archivo)
Prueba verificando que se abrió el archivo:
def test_guardar_usuario_json():
abrir_archivo = mock_open()
usuario = {"id": 1, "nombre": "Ana"}
with patch("builtins.open", abrir_archivo):
guardar_usuario_json("usuario.json", usuario)
abrir_archivo.assert_called_once_with("usuario.json", "w", encoding="utf-8")
Si queremos verificar el contenido exacto generado por json.dump, a veces tmp_path resulta más claro.
Otra opción es recibir la función para abrir archivos como dependencia:
def leer_configuracion(ruta, abrir_archivo=open):
with abrir_archivo(ruta, "r", encoding="utf-8") as archivo:
return archivo.read()
La prueba puede pasar mock_open sin usar patch:
def test_leer_configuracion_con_open_inyectado():
abrir_archivo = mock_open(read_data="modo=test")
contenido = leer_configuracion("config.txt", abrir_archivo)
assert contenido == "modo=test"
Esta técnica hace explícita la dependencia, aunque no siempre se usa para funciones muy simples.
Usar abrir_archivo=open captura el valor de open al definir la función. Si luego intentas parchear builtins.open, puede que esa función siga usando el valor capturado.
Una alternativa más flexible es:
def leer_configuracion(ruta, abrir_archivo=None):
if abrir_archivo is None:
abrir_archivo = open
with abrir_archivo(ruta, "r", encoding="utf-8") as archivo:
return archivo.read()
En pruebas, pasamos abrir_archivo. En producción, usa open.
tmp_path es una fixture de pytest que crea una carpeta temporal para la prueba. Técnicamente toca el sistema de archivos, pero lo hace en un espacio aislado y controlado.
Es útil cuando quieres probar integración real con archivos sin afectar archivos del proyecto.
Prueba de lectura real en archivo temporal:
def test_leer_configuracion_con_tmp_path(tmp_path):
ruta = tmp_path / "config.txt"
ruta.write_text("modo=test", encoding="utf-8")
contenido = leer_configuracion(ruta)
assert contenido == "modo=test"
Esta prueba no usa mocks. Verifica que el código funciona con archivos reales, pero dentro de una carpeta temporal.
Ejemplo:
def test_guardar_reporte_con_tmp_path(tmp_path):
ruta = tmp_path / "reporte.txt"
guardar_reporte(ruta, "resultado final")
assert ruta.read_text(encoding="utf-8") == "resultado final"
Para validar el contenido final de un archivo, tmp_path suele ser más directo que inspeccionar llamadas a write.
mock_open cuando quieras aislar la prueba del sistema de archivos.mock_open para simular errores como FileNotFoundError.tmp_path cuando quieras verificar lectura y escritura real en archivos temporales.patch y controlar open directamente.Prueba esta función con mock_open:
def contar_usuarios_activos(ruta):
with open(ruta, "r", encoding="utf-8") as archivo:
lineas = archivo.readlines()
return sum(1 for linea in lineas if linea.strip().endswith(",activo"))
Usa contenido con tres usuarios, dos activos y uno inactivo.
Una solución:
from unittest.mock import mock_open, patch
from usuarios import contar_usuarios_activos
def test_contar_usuarios_activos():
contenido = (
"ana,activo\n"
"luis,inactivo\n"
"marta,activo\n"
)
abrir_archivo = mock_open(read_data=contenido)
with patch("builtins.open", abrir_archivo):
cantidad = contar_usuarios_activos("usuarios.csv")
assert cantidad == 2
abrir_archivo.assert_called_once_with("usuarios.csv", "r", encoding="utf-8")
Para probar código que trabaja con archivos podemos reemplazar open con mock_open, inyectar la función de apertura o usar tmp_path para archivos temporales reales. La elección depende de si buscamos aislamiento total o comprobar comportamiento real de lectura y escritura.
En el próximo tema veremos cómo controlar fechas, horas, aleatoriedad e identificadores generados.