Pruebas unitarias en Go con interfaces

En lugar de presentar


Este artículo es para aquellos que, como yo, vinieron a Go desde el mundo de Django. Bueno, Django nos echó a perder. Uno solo tiene que ejecutar las pruebas, ya que él mismo, bajo el capó, creará una base de datos de prueba, ejecutará las migraciones y, después de la ejecución, se limpiará. Convenientemente? Ciertamente. Es solo que se necesita tiempo para ejecutar las migraciones: un carruaje, pero parece ser un pago razonable por comodidad, además siempre hay--reuse-db... El choque cultural es aún más intenso cuando los Junglers experimentados se acercan a otros idiomas, como Go. Es decir, ¿cómo es que no hay automigraciones antes y después? ¿Manos? ¿Y la base? ¿Manos también? ¿Y después de las pruebas? ¿Qué, y un tirón con las manos? Y luego el programador, intercalando el código con jadeos y suspiros, comienza a escribir junga en Go en un proyecto separado. Por supuesto, todo parece muy triste. Sin embargo, en Go es muy posible escribir pruebas unitarias rápidas y confiables sin utilizar servicios de terceros, como una base de datos de prueba o caché.



Esta será mi historia.



¿Qué estamos probando?


Imaginemos que necesitamos escribir una función que verifique la presencia de un empleado en la base de datos por número de teléfono.



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
}


Está bien, escribieron. ¿Cómo probarlo? Por supuesto, puede crear una base de datos de prueba antes de ejecutar las pruebas, crear tablas en ella y, después de ejecutar esta base de datos, bloquearla suavemente.



Pero hay otra manera.



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.



, , .





Por supuesto, este enfoque tiene sus inconvenientes. Por ejemplo, si su lógica está ligada a algún tipo de lógica de base de datos interna, dichas pruebas no podrán revelar errores causados ​​por la base de datos. Pero creo que probar con la participación de una base de datos y servicios de terceros ya no se trata de pruebas unitarias, son más pruebas de integración o incluso e2e, y están algo más allá del alcance de este artículo.



¡Gracias por las pruebas de lectura y escritura!




All Articles