Comment tester les paniques?

90

Je me demande actuellement comment écrire des tests qui vérifient si un morceau de code donné a paniqué? Je sais que Go utilise recoverpour attraper les paniques, mais contrairement au code Java, vous ne pouvez pas vraiment spécifier quel code doit être ignoré en cas de panique ou ce que vous avez. Donc si j'ai une fonction:

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

Je ne peux pas vraiment dire si OtherFunctionThatPanicsnous avons paniqué et nous avons récupéré, ou si la fonction n'a pas paniqué du tout. Comment spécifier le code à ignorer s'il n'y a pas de panique et quel code exécuter en cas de panique? Comment puis-je vérifier si nous nous sommes remis de la panique?

ThePiachu
la source

Réponses:

106

testingn'a pas vraiment le concept de «succès», seulement l'échec. Donc, votre code ci-dessus est à peu près correct. Vous trouverez peut-être ce style un peu plus clair, mais c'est fondamentalement la même chose.

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

Je trouve généralement testingassez faible. Vous pourriez être intéressé par des moteurs de test plus puissants comme Ginkgo . Même si vous ne voulez pas le système Ginkgo complet, vous pouvez utiliser uniquement sa bibliothèque de matrices , Gomega , qui peut être utilisée avec testing. Gomega comprend des matchers comme:

Expect(OtherFunctionThatPanics).To(Panic())

Vous pouvez également conclure la vérification de panique dans une fonction 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
la source
@IgorMikushkin dans Go 1.11, en utilisant le premier formulaire décrit par Rob Napier fonctionne en fait pour la couverture.
FGM
Y a-t-il une raison pour laquelle vous utilisez r := recover(); r == nilet pas seulement recover() == nil?
Duncan Jones
@DuncanJones Pas vraiment dans ce cas. C'est un modèle de Go très typique pour rendre l'erreur disponible dans le bloc, donc il était probablement habituel pour l'OP de l'écrire de cette façon (et j'ai avancé son code), mais ce n'est pas réellement utilisé dans ce cas.
Rob Napier
43

Si vous utilisez témoigner / affirmer , alors c'est une ligne unique:

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

Ou, si vous avez OtherFunctionThatPanicsune signature autre que func():

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

Si vous n'avez pas encore essayé de témoigner, consultez également témoigner / simuler . Assertions et moqueries super simples.

Marbre Jacob
la source
7

Lors d'une boucle sur plusieurs cas de test, j'opterais pour quelque chose comme ceci:

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

Aller au terrain de jeu

Aleh
la source
4

Lorsque vous avez besoin de vérifier le contenu de la panique, vous pouvez taper la valeur récupérée:

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 le code que vous testez ne panique pas OU ne panique pas avec une erreur OU panique avec le message d'erreur que vous attendez, le test échouera (ce que vous voudriez).

joonas.fi
la source
1
Il est plus robuste d'affirmer un type sur un type d'erreur spécifique (par exemple os.SyscallError) que de comparer les messages d'erreur, qui peuvent changer (par exemple) d'une version de Go à l'autre.
Michael
+ Michael Aug, c'est probablement la meilleure approche, quand un type spécifique est disponible.
joonas.fi
3

Dans votre cas, vous pouvez faire:

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
        // ....
    }
}

En tant que fonction de routeur panique générique , cela fonctionnera également:

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
la source
3

Voie succincte

Pour moi, la solution ci-dessous est facile à lire et vous montre le flux de code naturel du code testé.

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

Pour une solution plus générale, vous pouvez également le faire comme ceci:

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
la source
0

Vous pouvez tester quelle fonction a paniqué en donnant une entrée panique

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

Thellimiste
la source