¿Cómo probar los pánicos?

90

Actualmente estoy pensando en cómo escribir pruebas que comprueben si un fragmento de código dado entró en pánico. Sé que Go recoversuele atrapar el pánico, pero a diferencia de, digamos, el código Java, realmente no se puede especificar qué código se debe omitir en caso de pánico o lo que sea. Entonces, si tengo una función:

func f(t *testing.T) {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered in f", r)
        }
    }()
    OtherFunctionThatPanics()
    t.Errorf("The code did not panic")
}

Realmente no puedo decir si OtherFunctionThatPanicsentró en pánico y nos recuperamos, o si la función no entró en pánico en absoluto. ¿Cómo especifico qué código omitir si no hay pánico y qué código ejecutar si hay pánico? ¿Cómo puedo comprobar si hubo algún pánico del que nos recuperamos?

ElPiachu
fuente

Respuestas:

106

testingrealmente no tiene el concepto de "éxito", sólo el fracaso. Entonces, su código anterior es correcto. Puede encontrar este estilo un poco más claro, pero básicamente es lo mismo.

func TestPanic(t *testing.T) {
    defer func() {
        if r := recover(); r == nil {
            t.Errorf("The code did not panic")
        }
    }()

    // The following is the code under test
    OtherFunctionThatPanics()
}

En general, me encuentro testingbastante débil. Puede que le interesen motores de prueba más potentes como Ginkgo . Incluso si no desea el sistema Ginkgo completo, puede usar solo su biblioteca de comparadores , Gomega , que se puede usar junto con testing. Gomega incluye matchers como:

Expect(OtherFunctionThatPanics).To(Panic())

También puede resumir la verificación de pánico en una función simple:

func TestPanic(t *testing.T) {
    assertPanic(t, OtherFunctionThatPanics)
}

func assertPanic(t *testing.T, f func()) {
    defer func() {
        if r := recover(); r == nil {
            t.Errorf("The code did not panic")
        }
    }()
    f()
}
Rob Napier
fuente
@IgorMikushkin en Go 1.11, utilizando la primera forma descrita por Rob Napier, realmente funciona para la cobertura.
FGM
¿Hay alguna razón por la que use r := recover(); r == nily no solo recover() == nil?
Duncan Jones
@DuncanJones No realmente en este caso. Es un patrón de Go realmente típico para hacer que el error esté disponible en el bloque, por lo que probablemente era un hábito para el OP escribirlo de esa manera (y presenté su código), pero en realidad no se usa en este caso.
Rob Napier
43

Si usa testificar / afirmar , entonces es una línea:

func TestOtherFunctionThatPanics(t *testing.T) {
  assert.Panics(t, OtherFunctionThatPanics, "The code did not panic")
}

O, si OtherFunctionThatPanicstiene una firma que no sea func():

func TestOtherFunctionThatPanics(t *testing.T) {
  assert.Panics(t, func() { OtherFunctionThatPanics(arg) }, "The code did not panic")
}

Si aún no ha intentado testificar, consulte también testificar / simular . Afirmaciones y burlas súper simples.

Jacob mármol
fuente
7

Al recorrer varios casos de prueba, optaría por algo como esto:

package main

import (
    "reflect"
    "testing"
)


func TestYourFunc(t *testing.T) {
    type args struct {
        arg1 int
        arg2 int
        arg3 int
    }
    tests := []struct {
        name      string
        args      args
        want      []int
        wantErr   bool
        wantPanic bool
    }{
        //TODO: write test cases
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            defer func() {
                r := recover()
                if (r != nil) != tt.wantPanic {
                    t.Errorf("SequenceInt() recover = %v, wantPanic = %v", r, tt.wantPanic)
                }
            }()
            got, err := YourFunc(tt.args.arg1, tt.args.arg2, tt.args.arg3)
            if (err != nil) != tt.wantErr {
                t.Errorf("YourFunc() error = %v, wantErr %v", err, tt.wantErr)
                return
            }
            if !reflect.DeepEqual(got, tt.want) {
                t.Errorf("YourFunc() = %v, want %v", got, tt.want)
            }
        })
    }
}

Ir al patio de recreo

Aleh
fuente
4

Cuando necesite verificar el contenido del pánico, puede encasillar el valor recuperado:

func TestIsAheadComparedToPanicsWithDifferingStreams(t *testing.T) {
    defer func() {
        err := recover().(error)

        if err.Error() != "Cursor: cannot compare cursors from different streams" {
            t.Fatalf("Wrong panic message: %s", err.Error())
        }
    }()

    c1 := CursorFromserializedMust("/foo:0:0")
    c2 := CursorFromserializedMust("/bar:0:0")

    // must panic
    c1.IsAheadComparedTo(c2)
}

Si el código que está probando no entra en pánico O entra en pánico con un error O entra en pánico con el mensaje de error que espera, la prueba fallará (que es lo que querría).

joonas.fi
fuente
1
Es más robusto afirmar el tipo en un tipo de error específico (por ejemplo, os.SyscallError) que comparar mensajes de error, que pueden cambiar (por ejemplo) de una versión de Go a la siguiente.
Michael
+ Michael Aug, ese es probablemente el mejor enfoque, para cuando hay un tipo específico disponible.
joonas.fi
3

En tu caso puedes hacer:

func f(t *testing.T) {
    recovered := func() (r bool) {
        defer func() {
            if r := recover(); r != nil {
                r = true
            }
        }()
        OtherFunctionThatPanics()
        // NOT BE EXECUTED IF PANICS
        // ....
    }
    if ! recovered() {
        t.Errorf("The code did not panic")

        // EXECUTED IF PANICS
        // ....
    }
}

Como función genérica de enrutador de pánico, esto también funcionará:

https://github.com/7d4b9/recover

package recover

func Recovered(IfPanic, Else func(), Then func(recover interface{})) (recoverElse interface{}) {
    defer func() {
        if r := recover(); r != nil {
            {
                // EXECUTED IF PANICS
                if Then != nil {
                    Then(r)
                }
            }
        }
    }()

    IfPanic()

    {
        // NOT BE EXECUTED IF PANICS
        if Else != nil {
            defer func() {
                recoverElse = recover()
            }()
            Else()
        }
    }
    return
}

var testError = errors.New("expected error")

func TestRecover(t *testing.T) {
    Recovered(
        func() {
            panic(testError)
        },
        func() {
            t.Errorf("The code did not panic")
        },
        func(r interface{}) {
            if err := r.(error); err != nil {
                assert.Error(t, testError, err)
                return
            }
            t.Errorf("The code did an unexpected panic")
        },
    )
}
David B
fuente
3

Manera sucinta

Para mí, la solución a continuación es fácil de leer y le muestra el flujo de código natural del código bajo prueba.

func TestPanic(t *testing.T) {
    // No need to check whether `recover()` is nil. Just turn off the panic.
    defer func() { recover() }()

    OtherFunctionThatPanics()

    // Never reaches here if `OtherFunctionThatPanics` panics.
    t.Errorf("did not panic")
}

Para una solución más general, también puede hacerlo así:

func TestPanic(t *testing.T) {
    shouldPanic(t, OtherFunctionThatPanics)
}

func shouldPanic(t *testing.T, f func()) {
    defer func() { recover() }()
    f()
    t.Errorf("should have panicked")
}
Inanc Gumus
fuente
0

Puede probar qué función sufrió pánico dando una entrada al pánico

package main

import "fmt"

func explode() {
    // Cause a panic.
    panic("WRONG")
}

func explode1() {
    // Cause a panic.
    panic("WRONG1")
}

func main() {
    // Handle errors in defer func with recover.
    defer func() {
        if r := recover(); r != nil {
            var ok bool
            err, ok := r.(error)
            if !ok {
                err = fmt.Errorf("pkg: %v", r)
                fmt.Println(err)
            }
        }

    }()
    // These causes an error. change between these
    explode()
    //explode1()

    fmt.Println("Everything fine")

}

http://play.golang.org/p/ORWBqmPSVA

Thellimist
fuente