Como testar o pânico?

92

No momento, estou pensando em como escrever testes que verificam se um determinado trecho de código entrou em pânico. Eu sei que Go recovercostuma pegar pânico, mas ao contrário, digamos, do código Java, você não pode realmente especificar qual código deve ser ignorado em caso de pânico ou o que quer que seja. Então, se eu tiver uma função:

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")
}

Não sei dizer se OtherFunctionThatPanicsentramos em pânico e nos recuperamos ou se a função não entrou em pânico. Como especifico qual código ignorar se não houver pânico e qual código executar se houver pânico? Como posso verificar se houve algum pânico do qual nos recuperamos?

ThePiachu
fonte

Respostas:

107

testingnão tem realmente o conceito de "sucesso", apenas fracasso. Portanto, seu código acima está certo. Você pode achar esse estilo um pouco mais claro, mas é basicamente a mesma coisa.

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()
}

Eu geralmente acho testingque é bastante fraco. Você pode estar interessado em mecanismos de teste mais poderosos, como o Ginkgo . Mesmo se você não quiser o sistema Ginkgo completo, pode usar apenas sua biblioteca de combinações , Gomega , que pode ser usada junto com testing. Gomega inclui matchers como:

Expect(OtherFunctionThatPanics).To(Panic())

Você também pode encerrar a verificação de pânico em uma função simples:

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
fonte
@IgorMikushkin no Go 1.11, usando a primeira forma descrita por Rob Napier realmente funciona para cobertura.
FGM
Existe algum motivo para você usar r := recover(); r == nile não apenas recover() == nil?
Duncan Jones
@DuncanJones Não neste caso. É um padrão Go realmente típico tornar o erro disponível no bloco, então provavelmente era o hábito do OP escrevê-lo dessa forma (e eu apresentei seu código), mas não é realmente usado neste caso.
Rob Napier de
46

Se você usar testify / assert , é uma linha única:

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

Ou, se você OtherFunctionThatPanicstiver uma assinatura diferente de func():

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

Se você ainda não testou, verifique também testificar / simular . Asserções e simulações super simples.

Jacob Marble
fonte
7

Ao repetir vários casos de teste, eu iria para algo assim:

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 playground

Aleh
fonte
5

Forma sucinta

Para mim, a solução abaixo é fácil de ler e mostra ao mantenedor o fluxo natural do código em teste.

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 uma solução mais geral, você também pode fazer desta forma:

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
fonte
4

Quando você precisar verificar o conteúdo do pânico, pode fazer o typecast do 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)
}

Se o código que você está testando não entrar em pânico OU entrar em pânico com um erro OU entrar em pânico com a mensagem de erro que você espera, o teste irá falhar (que é o que você deseja).

joonas.fi
fonte
1
É mais robusto declarar um tipo de erro específico (por exemplo, os.SyscallError) do que comparar mensagens de erro, que podem mudar (por exemplo) de uma versão Go para a próxima.
Michael
+ Michael Aug, essa é provavelmente a melhor abordagem, para quando há um tipo específico disponível.
joonas.fi
3

No seu caso, você pode fazer:

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 uma função de roteador de pânico genérico, isso também 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
fonte
0

Você pode testar qual função apanha, dando uma entrada para o 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
fonte