Unit-Tests in Go mit Schnittstellen

Anstatt vorzustellen


Dieser Artikel ist für diejenigen, die wie ich aus der Welt von Django nach Go gekommen sind. Also hat Django uns verwöhnt. Man muss nur die Tests ausführen, da er selbst unter der Haube eine Testdatenbank erstellt, die Migrationen ausführt und nach dem Ausführen nach sich selbst bereinigt. Praktisch? Bestimmt. Es ist nur so, dass es Zeit braucht, um Migrationen durchzuführen - eine Kutsche, aber dies scheint eine angemessene Bezahlung für Komfort zu sein, und das gibt es immer--reuse-db... Der Kulturschock ist umso intensiver, als erfahrene Jungler in andere Sprachen wie Go kommen. Das heißt, wie ist es keine Automigrationen vorher und nachher? Mit deinen Händen? Und die Basis? Hände auch? Und nach den Tests? Was und ein Tirdown mit deinen Händen? Nun, dann beginnt der Programmierer, der den Code mit Seufzern und Seufzern durchsetzt, Jungu in Go in einem separaten Projekt zu schreiben. Natürlich sieht alles sehr traurig aus. In Go ist es jedoch durchaus möglich, schnelle und zuverlässige Komponententests zu schreiben, ohne Dienste von Drittanbietern wie eine Testdatenbank oder einen Cache zu verwenden.



Das wird meine Geschichte sein.



Was testen wir?


Stellen wir uns vor, wir müssen eine Funktion schreiben, die die Anwesenheit eines Mitarbeiters in der Datenbank anhand der Telefonnummer überprüft.



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
}


Okay, sie haben geschrieben. Wie teste ich es? Sie können natürlich eine Testdatenbank erstellen, bevor Sie die Tests ausführen, Tabellen darin erstellen und sie nach dem Ausführen dieser Datenbank vorsichtig zum Absturz bringen.



Es gibt aber auch einen anderen Weg.



Schnittstellen


, , , 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.



, , .





Natürlich hat dieser Ansatz seine Nachteile. Wenn Ihre Logik beispielsweise an eine interne Datenbanklogik gebunden ist, können solche Tests keine durch die Datenbank verursachten Fehler identifizieren. Ich glaube jedoch, dass es beim Testen unter Beteiligung einer Datenbank und von Diensten von Drittanbietern nicht mehr um Komponententests geht, sondern um Integrations- oder sogar e2e-Tests, und sie gehen etwas über den Rahmen dieses Artikels hinaus.



Vielen Dank für das Lesen und Schreiben von Tests!




All Articles