Contenido del curso
Introducción
Primeras pruebas con Go
Utilizando mocks
El verdadero valor de tus pruebas
Próximos pasos
Manejo de errores y panic en Go
Contenido del curso
Manejo de errores y panic en Go
Àlex Grau Roca
studentSebastian Aranda
studentMiguel Erick Flores Aguilar
studentAlejandro Carballo
studentDavid Hernandez
studentAndres Alfonso Puello Chavez
studentPedro Andrés Chaparro Quintero
studentSteven Javier Arevalo Poveda
studentRubens A. Rangel Gomez
studentJuan Sebastián Ovalle Silva
studentLes dejo mi test completo de parser en mi Github.
Lo he utilizado un poco distinto a la clase, básicamente utilizo assert en lugar de require, los ejemplos de error utilizo unos JSON específicos para cada caso y luego he creado la función readSample para simplificar bastante más el código, además de reducir el código duplicado.
Super ese manejo nuevo, tengo es una inquietud, si es buena idea manejar el sample.go con lógica de pruebas unitarias. En mi concepto debería tener en el nombre test para hacer una referencia de ayuda a las pruebas unitarias.
go 1.19
Método para leer archivos y desmantelarlo.
func readFileAndUnmarshalT any (T, error) { body, err := ioutil.ReadFile(pathFile) if err != nil { return data, err } err = json.Unmarshal([]byte(body), &data) if err != nil { return data, err } return data, nil }
Dejo mi solucion modificando mi aporte de la clase anterior
Mi archivo parser_test.go
package util import ( "catching-pokemons/models" "encoding/json" "io/ioutil" "testing" "github.com/stretchr/testify/assert" ) func loadJsonToMap(pathFile string, data interface{}) error { body, err := ioutil.ReadFile(pathFile) if err != nil { return err } err = json.Unmarshal([]byte(body), &data) if err != nil { return err } return nil } func TestParsePokemon(t *testing.T) { var apiPokemon map[string]models.PokeApiPokemonResponse err := loadJsonToMap("samples/pokeapi_response.json", &apiPokemon) assert.NoError(t, err, "Error loading pokeapi_response file") var pokemon map[string]models.Pokemon err = loadJsonToMap("samples/api_response.json", &pokemon) assert.NoError(t, err, "Error loading api_response file") type args struct { apiPokemon models.PokeApiPokemonResponse } tests := []struct { name string args args want models.Pokemon wantErr error }{ { name: "Success pikachu", args: args{apiPokemon: apiPokemon["pikachu"]}, want: pokemon["pikachu"], wantErr: nil, }, { name: "Pokemon type not found", args: args{apiPokemon: apiPokemon["pikachuWithNotType"]}, want: models.Pokemon{}, wantErr: ErrNotFoundPokemonType, }, { name: "Pokemon reftype's type is empty", args: args{apiPokemon: apiPokemon["pikachuWithRefTypeEmpty"]}, want: models.Pokemon{}, wantErr: ErrNotFoundPokemonTypeName, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := ParsePokemon(tt.args.apiPokemon) if err == ErrNotFoundPokemonType || err == ErrNotFoundPokemonTypeName { assert.NotNil(t, err, "ParsePokemon() error = %v, wantErr %v", err, tt.wantErr) assert.EqualError(t, err, tt.wantErr.Error(), "ParsePokemon() error = %v, wantErr %v", err, tt.wantErr) } else { assert.NoError(t, err, "ParsePokemon() error = %v, wantErr %v", err, tt.wantErr) assert.Equal(t, tt.want, got, "ParsePokemon() = %v, want %v", got, tt.want) } }) } }
mi archivo api_response.json el cual no sufre cambios
{ "pikachu": { "Id": 25, "Name": "pikachu", "Type": "electric", "Abilities": { "Attack": 55, "Defense": 40, "Hp": 35, "Speed": 90 } }, "bulbasaur": { "Id": 1, "Name": "bulbasaur", "Type": "grass", "Abilities": { "Attack": 49, "Defense": 49, "Hp": 45, "Speed": 45 } }, "charmander": { "Id": 4, "Name": "charmander", "Type": "fire", "Abilities": { "Attack": 52, "Defense": 43, "Hp": 39, "Speed": 65 } }, "squirtle": { "Id": 7, "Name": "squirtle", "Type": "water", "Abilities": { "Attack": 48, "Defense": 65, "Hp": 44, "Speed": 43 } } }
Mi archivo pokeapi_response.json
{ "pikachu" : { "id": 25, "name": "pikachu", "types": [ { "slot": 1, "type": { "name": "electric" } } ], "stats": [ { "base_stat": 35, "stat": { "name": "hp" } }, { "base_stat": 55, "stat": { "name": "attack" } }, { "base_stat": 40, "stat": { "name": "defense" } }, { "base_stat": 50, "stat": { "name": "special-attack" } }, { "base_stat": 50, "stat": { "name": "special-defense" } }, { "base_stat": 90, "stat": { "name": "speed" } } ] }, "pikachuWithNotType" : { "id": 25, "name": "pikachu", "types": [], "stats": [ { "base_stat": 35, "stat": { "name": "hp" } }, { "base_stat": 55, "stat": { "name": "attack" } }, { "base_stat": 40, "stat": { "name": "defense" } }, { "base_stat": 50, "stat": { "name": "special-attack" } }, { "base_stat": 50, "stat": { "name": "special-defense" } }, { "base_stat": 90, "stat": { "name": "speed" } } ] }, "pikachuWithRefTypeEmpty" : { "id": 25, "name": "pikachu", "types": [ { "slot": 1, "type": { "name": "" } } ], "stats": [ { "base_stat": 35, "stat": { "name": "hp" } }, { "base_stat": 55, "stat": { "name": "attack" } }, { "base_stat": 40, "stat": { "name": "defense" } }, { "base_stat": 50, "stat": { "name": "special-attack" } }, { "base_stat": 50, "stat": { "name": "special-defense" } }, { "base_stat": 90, "stat": { "name": "speed" } } ] }, "bulbasaur": { "id": 1, "name": "bulbasaur", "types": [ { "slot": 1, "type": { "name": "grass" } }, { "slot": 2, "type": { "name": "poison" } } ], "stats": [ { "base_stat": 45, "stat": { "name": "hp" } }, { "base_stat": 49, "stat": { "name": "attack" } }, { "base_stat": 49, "stat": { "name": "defense" } }, { "base_stat": 65, "stat": { "name": "special-attack" } }, { "base_stat": 65, "stat": { "name": "special-defense" } }, { "base_stat": 45, "stat": { "name": "speed" } } ] }, "charmander": { "id": 4, "name": "charmander", "types": [ { "slot": 1, "type": { "name": "fire" } } ], "stats": [ { "base_stat": 39, "stat": { "name": "hp" } }, { "base_stat": 52, "stat": { "name": "attack" } }, { "base_stat": 43, "stat": { "name": "defense" } }, { "base_stat": 60, "stat": { "name": "special-attack" } }, { "base_stat": 50, "stat": { "name": "special-defense" } }, { "base_stat": 65, "stat": { "name": "speed" } } ] }, "squirtle": { "id": 7, "name": "squirtle", "types": [ { "slot": 1, "type": { "name": "water" } } ], "stats": [ { "base_stat": 44, "stat": { "name": "hp" } }, { "base_stat": 48, "stat": { "name": "attack" } }, { "base_stat": 65, "stat": { "name": "defense" } }, { "base_stat": 50, "stat": { "name": "special-attack" } }, { "base_stat": 64, "stat": { "name": "special-defense" } }, { "base_stat": 43, "stat": { "name": "speed" } } ] } }
Mi Implementacion para ambos casos
func TestParsePokemonToDTO_WhenTypeNotFound(t *testing.T) { apiPokemon := entities.PokemonApiResponse{} err := json.Unmarshal(pokemonFromApi, &apiPokemon) assert.NoError(t, err) apiPokemon.PokemonType = []entities.PokemonType{} parsePokemon, err := ParsePokemonToDTO(apiPokemon) assert.NotNil(t, err) assert.EqualError(t, fmt.Errorf("pokemon type not found"), err.Error()) assert.Equal(t, parsePokemon, dto.Pokemon{}) } func TestParsePokemonToDTO_WhenTypeNameNotFound(t *testing.T) { apiPokemon := entities.PokemonApiResponse{} err := json.Unmarshal(pokemonFromApi, &apiPokemon) assert.NoError(t, err) apiPokemon.PokemonType[0].RefType = entities.BaseName{} parsePokemon, err := ParsePokemonToDTO(apiPokemon) assert.NotNil(t, err) assert.EqualError(t, fmt.Errorf("pokemon type name not found"), err.Error()) assert.Equal(t, parsePokemon, dto.Pokemon{}) }
Mi implementación de test para el ErrNotFoundPokemonTypeName (Bastaba con cambiar el primer índice del array PokemonType, pero decidí cambiarlos todos con un for):
func TestParserTypeNameNotFound(t *testing.T) { c := require.New(t) // Ger response sended by pokeapi // (simulate the get request) body, err := ioutil.ReadFile("samples/pokeapi_response.json") c.NoError(err) // Transform into struct var response models.PokeApiPokemonResponse err = json.Unmarshal([]byte(body), &response) c.NoError(err) // *** Remove all pokemon types names *** // This will cause the test to fail for index := range response.PokemonType { response.PokemonType[index].RefType.Name = "" } // Parse struct WITH OUR METHOD // and test the expected error _, err = ParsePokemon(response) c.NotNil(err) c.EqualError(ErrNotFoundPokemonTypeName, err.Error()) }
Adjunto un test para el Type Name Error
func TestParserPokemonTypeNameNotFound(t *testing.T) { c := require.New(t) body, err := ioutil.ReadFile("samples/pokeapi_response.json") c.NoError(err) response := models.PokeApiPokemonResponse{} err = json.Unmarshal([]byte(body), &response) c.NoError(err) response.PokemonType[0].RefType.Name = "" _, err = ParsePokemon(response) c.NotNil(err) c.EqualError(ErrNotFoundPokemonTypeName, err.Error()) }
🧠 Idea principal
En Go, el manejo de errores se basa en control explícito mediante valores error, no en excepciones;
el panic es un mecanismo excepcional que debe usarse solo en casos críticos.
Además, los tests deben validar tanto casos exitosos como escenarios de error.
🧩 Fundamentos
1. Manejo de errores en Go
Los errores se retornan como valores (error).
Siempre deben ser verificados explícitamente.
Ejemplo conceptual:
result, err := function()
if err != nil {
// manejar error
}
2. panic
Se usa cuando ocurre un error crítico e irrecuperable.
Detiene la ejecución normal del programa.
No es para lógica común, sino para fallos graves.
3. Testing de errores
No solo se prueba el caso exitoso.
Se deben cubrir escenarios como:
datos inválidos
valores vacíos
estructuras incompletas
4. Simulación de errores
Se modifican datos de entrada (ej: JSON) para provocar fallos.
Permite validar que el sistema responde correctamente.
5. Uso de assertions en errores
assert.NoError → cuando todo debe salir bien
assert.Error / assert.NotNil → cuando se espera fallo
assert.EqualError → validar mensaje específico
6. Reutilización y limpieza
Funciones auxiliares (leer JSON, parsear datos).
Reducen duplicación en tests.
Mejoran legibilidad.
🔑 Puntos importantes
En Go no hay excepciones tradicionales.
Los errores son parte del flujo normal del programa.
panic debe usarse con cuidado (no para todo).
Es obligatorio testear casos de error, no solo éxito.
Los tests deben simular escenarios reales de fallo.
Modificar datos de entrada es una técnica clave para probar errores.
Validar el tipo/mensaje de error asegura precisión.
Reducir código duplicado en tests mejora mantenimiento.
Existen múltiples formas válidas de testear (no hay una única correcta).
🎯 Conclusión corta
En Go, manejar errores no es opcional: es parte del diseño del código, y testearlos correctamente es lo que convierte un sistema funcional en uno robusto.
Mi ejemplo de los tests en el archivo parser
func TestParsePokemon(t *testing.T) { t.Run("Success", func(t *testing.T) { body, err := ioutil.ReadFile("samples/pokeapi_response.json") assert.NoError(t, err) var response models.PokeApiPokemonResponse err = json.Unmarshal(body, &response) assert.NoError(t, err) result, err := ParsePokemon(response) assert.NoError(t, err) apiResponse, err := ioutil.ReadFile("samples/api_response.json") assert.NoError(t, err) var expected models.Pokemon err = json.Unmarshal(apiResponse, &expected) assert.NoError(t, err) assert.Equal(t, expected, result) }) t.Run("TypeNotFound", func(t *testing.T) { body, err := ioutil.ReadFile("samples/pokeapi_response.json") assert.NoError(t, err) var response models.PokeApiPokemonResponse err = json.Unmarshal(body, &response) assert.NoError(t, err) response.PokemonType = []models.PokemonType{} _, err = ParsePokemon(response) assert.Error(t, err) assert.Errorf(t, err, ErrNotFoundPokemonType.Error()) }) t.Run("TypeNameNotFound", func(t *testing.T) { body, err := ioutil.ReadFile("samples/pokeapi_response.json") assert.NoError(t, err) var response models.PokeApiPokemonResponse err = json.Unmarshal(body, &response) assert.NoError(t, err) response.PokemonType[0].RefType.Name = "" _, err = ParsePokemon(response) assert.Error(t, err) assert.Errorf(t, err, ErrNotFoundPokemonTypeName.Error()) }) }