Tests unitaires en Go avec interfaces

Au lieu d'introduire


Cet article s'adresse à ceux qui, comme moi, sont venus chez Go du monde de Django. Eh bien, Django nous a gâtés. Il suffit d'exécuter les tests, car lui-même, sous le capot, créera une base de données de test, exécutera les migrations et, après l'exécution, se nettoiera. Idéalement? Certainement. C'est juste qu'il faut du temps pour exécuter des migrations - un chariot, mais cela semble être un paiement raisonnable pour le confort, et il y a toujours--reuse-db... Le plus grand choc culturel, c'est lorsque les Junglers chevronnés viennent dans d'autres langues comme le go. Autrement dit, comment se fait-il pas d'automigrations avant et après? Mains? Et la base? Les mains aussi? Et après les tests? Quoi, et un tirdown avec vos mains? Et puis le programmeur, entrecoupant le code avec des halètements et des soupirs, commence à écrire junga dans Go dans un projet séparé. Bien sûr, tout semble très triste. Cependant, dans Go, il est tout à fait possible d'écrire des tests unitaires rapides et fiables sans utiliser de services tiers tels qu'une base de données de test ou un cache.



Ce sera mon histoire.



Que testons-nous?


Imaginons que nous ayons besoin d'écrire une fonction qui vérifie la présence d'un employé dans la base de données par numéro de téléphone.



func CheckEmployee(db *sqlx.DB, phone string) (error, bool) {
    err := db.Get(`SELECT * FROM employees WHERE phone = ?`, phone)
    if err != nil {
        return err, false
    }
    return nil, true
}


D'accord, ils ont écrit. Comment le tester? Vous pouvez, bien sûr, créer une base de données de test avant d'exécuter les tests, y créer des tables et, après avoir exécuté cette base de données, la planter doucement.



Mais il y a aussi un autre moyen.



Interfaces


, , , Get. , -, , , , , , .



. Go? , — -, , , , , . , ?



.



:



type ExampleInterface interface {
    Method() error
}


, , :



type ExampleStruct struct {}
func (es ExampleStruct) Method() error {
    return nil
}


, ExampleStruct ExampleInterface , , - ExampleInterface, ExampleStruct.



?



, Get, , , , , Get sqlx.Get .



Talk is cheap, let's code!


:



Get(dest interface{}, query string, args ...interface{}) error


, Get :



type BaseDBClient interface {
    Get(interface{}, string, ...interface{}) error
}


:



func CheckEmployee(db BaseDBClient, phone string) (err error, exists bool) {
    var employee interface{}
    err = db.Get(&employee, `SELECT name FROM employees WHERE phone = ?`, phone)
    if err != nil {
        return err, false
    }
    return nil, true
}


, , , , sqlx.Get, sqlx, , BaseDBClient.





, .

, , .



, BaseDBClient:



type TestDBClient struct {}

func (tc *TestDBClient) Get(interface{}, string, ...interface{}) error {
    return nil
}


, , , , , , , .



, — CheckEmployee :



func TestCheckEmployee() {
    test_client := TestDBClient{}
    err, exists := CheckEmployee(&test_client, "nevermind")
    assert.NoError(t, err)
    assert.Equal(t, exists, true)
}




, . , , :



type BaseDBClient interface {
    Get(interface{}, string, ...interface{}) error
}

type TestDBClient struct {
    success bool
}

func (t *TestDBClient) Get(interface{}, string, ...interface{}) error {
    if t.success {
        return nil
    }
    return fmt.Errorf("This is a test error")
}

func TestCheckEmployee(t *testing.T) {
    type args struct {
        db BaseDBClient
    }
    tests := []struct {
        name       string
        args       args
        wantErr    error
        wantExists bool
    }{
        {
            name: "Employee exists",
            args: args{
                db: &TestDBClient{success: true},
            },
            wantErr:    nil,
            wantExists: true,
        }, {
            name: "Employee don't exists",
            args: args{
                db: &TestDBClient{success: false},
            },
            wantErr:    fmt.Errorf("This is a test error"),
            wantExists: false,
        },
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            gotErr, gotExists := CheckEmployee(tt.args.db, "some phone")
            if !reflect.DeepEqual(gotErr, tt.wantErr) {
                t.Errorf("CheckEmployee() gotErr = %v, want %v", gotErr, tt.wantErr)
            }
            if gotExists != tt.wantExists {
                t.Errorf("CheckEmployee() gotExists = %v, want %v", gotExists, tt.wantExists)
            }
        })
    }
}


! , , , , , go.



, , .





Bien entendu, cette approche a ses inconvénients. Par exemple, si votre logique est liée à une sorte de logique de base de données interne, ces tests ne pourront pas identifier les erreurs causées par la base de données. Mais je crois que les tests avec la participation d'une base de données et de services tiers ne sont plus des tests unitaires, ce sont plus des tests d'intégration ou même e2e, et ils sortent un peu du cadre de cet article.



Merci d'avoir lu et Ă©crit des tests!




All Articles