Al escribir pruebas unitarias en Go, gestionar dependencias externas como sistemas de archivos, relojes del sistema o comandos externos puede resultar complicado. Este artículo explora tres enfoques para abordar este problema y muestra por qué el patrón Comando (Command Pattern) ofrece una alternativa limpia y fácil de probar frente a los mocks complejos.
Veamos un ejemplo: una función diseñada para medir el rendimiento (benchmark) de compilaciones incrementales.
func Benchmark(binary string, sourcePath string, resultPath string, numBuilds int) error {
if err := build(binary); err != nil {
return err
}
source, err := read(sourcePath)
if err != nil {
return err
}
deltas := []time.Duration{}
for i := 0; i < numBuilds; i++ {
if err := write(sourcePath, edit(source, i)); err != nil {
return err
}
start := now()
if err := build(binary); err != nil {
return err
}
delta := now().Sub(start)
deltas = append(deltas, delta)
if err := revert(sourcePath); err != nil {
return err
}
}
return write(resultPath, report(deltas))
}
Esta función Benchmark realiza varias tareas:
Probar esto directamente es difícil porque depende de cuatro elementos externos:
build()read() y write()now()revert()Muchos desarrolladores podrían limitarse a probar las funciones independientes edit y report, dejando sin probar la lógica central de orquestación. Veamos cómo podemos mejorar el diseño para que todo el proceso sea verificable mediante pruebas.
Main
🔗En ocasiones, puedes trasladar las dependencias hacia arriba, a quien llama a la función (a menudo main).
func Benchmark(binary string, sourcePath string, source string, numBuilds int) (string, error) {
// ... core loop logic remains ...
// returning the report string instead of writing it
return report(deltas), nil
}
func Main() error {
// setup and initial build moved here
source, _ := read(sourcePath)
result, _ := Benchmark(binary, sourcePath, source, 3)
return write(resultPath, result)
}
Veredicto: Aunque esto ayuda, aquí no es suficiente. El bucle de Benchmark sigue necesitando interactuar fundamentalmente con el sistema de compilación, el reloj, el sistema de archivos y el control de versiones en cada iteración.
El enfoque tradicional orientado a objetos es la inyección de dependencias. Defines una interfaz que representa todas las acciones externas.
type Delegate interface {
Build(binary string) error
Read(path string) (string, error)
Write(path string, content string) error
Now() time.Time
Revert(path string) error
}
type BenchmarkRunner struct {
Delegate Delegate
}
func (b *BenchmarkRunner) Run(binary string, sourcePath string, resultPath string, numBuilds int) error {
// ... implementation using b.Delegate.Build(), b.Delegate.Now(), etc ...
}
Veredicto: Esto funciona bien y te permite sustituir el delegado por una implementación simulada (fake) para las pruebas. Sin embargo, exige definir interfaces e implementaciones mock para cada tipo de efecto secundario, lo que puede provocar una proliferación excesiva de interfaces.
En lugar de definir interfaces específicas para cada acción, podemos tratar cada efecto secundario como un comando genérico que será ejecutado por un «Ejecutor» (Runner) central.
Primero, definimos qué significa ejecutar un comando:
type Runner interface {
Run(cmd Command) (Result, error)
}
A continuación, definimos nuestros comandos específicos como estructuras simples (o constantes):
type BuildCmd struct { Binary string }
type ReadCmd struct { Path string }
type WriteCmd struct { Path string; Content string }
type NowCmd struct {}
type RevertCmd struct { Path string }
Ahora, refactorizamos la lógica del benchmark para que devuelva los comandos que quiere ejecutar, o ceda el control a un ejecutor:
type BenchmarkProcess struct {
runner Runner
}
func (p *BenchmarkProcess) Execute(binary, sourcePath, resultPath string, numBuilds int) error {
_, err := p.runner.Run(BuildCmd{Binary: binary})
if err != nil { return err }
readRes, err := p.runner.Run(ReadCmd{Path: sourcePath})
if err != nil { return err }
source := readRes.(string) // Type assertion needed here
// ... rest of the loop using p.runner.Run() ...
}
El verdadero potencial de este patrón brilla en las pruebas unitarias. Solo necesitas escribir un FakeRunner en lugar de implementar complejas interfaces mock para cada dependencia.
La prueba se convierte en una simple lista de comandos esperados y respuestas simuladas:
func TestBenchmark(t *testing.T) {
// 1. Define the script: "When you see this command, return this result"
fakeRunner := NewFakeRunner([]Step{
{Cmd: BuildCmd{"binary"}, Err: nil},
{Cmd: ReadCmd{"source_path"}, Res: "source code", Err: nil},
{Cmd: WriteCmd{"source_path", "edited code"}, Err: nil},
{Cmd: NowCmd{}, Res: time.Unix(0, 0), Err: nil},
// ... and so on ...
})
p := BenchmarkProcess{runner: fakeRunner}
err := p.Execute("binary", "source_path", "result_path", 1)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// 2. Verify the runner consumed all expected steps
if !fakeRunner.IsDone() {
t.Fatalf("did not execute all expected commands")
}
}
Al convertir las llamadas a funciones en estructuras de datos (Commands), desacoplas la lógica de negocio de sus efectos secundarios.
Obtén la edición sin conexión de El Estilo de Vida de Kaue—una guía práctica para optimizar 🍏 Salud, 💰 Riqueza, ❤️ Amor y 🧘 Sentido.
Recibe notificaciones cuando publique nuevos ensayos, resúmenes de libros y guías prácticas sobre cómo vivir bien. Sin spam—cancela cuando quieras.
¿Tienes alguna pregunta, perspectiva o sugerencia? Comparte tus ideas abajo. Los comentarios funcionan con GitHub Discussions (sin anuncios y sin spam)—haz clic en Iniciar sesión con GitHub abajo para comentar o reaccionar (crear una cuenta gratuita en GitHub toma menos de un minuto).
Actualizado el 11 oct 2026.
AVISO LEGAL: El contenido de este sitio tiene fines exclusivamente informativos y educativos y no constituye asesoramiento médico, psicológico, financiero, de inversión, fiscal ni legal. Las ideas, la selección y las opiniones aquí presentadas son exclusivamente mías y no representan las de mi empleador. Los resúmenes de libros, conferencias y artículos son comentarios educativos independientes y siguen siendo propiedad intelectual de sus respectivos autores. Se pueden utilizar herramientas de IA para ayudar en la edición, el formato y la síntesis; aunque se hace todo lo posible por garantizar la precisión, pueden producirse errores. PRIVACIDAD: Este sitio utiliza servicios de terceros para la suscripción opcional al boletín (follow.it), los comentarios (giscus / GitHub Discussions) y el contenido multimedia incrustado (YouTube), que pueden procesar las direcciones IP y las cookies de los visitantes de acuerdo con sus respectivas políticas de privacidad.