Teste de unidade em Go com interfaces

Em vez de apresentar


Este artigo é para quem, como eu, veio do mundo do Django para o Go. Bem, Django nos estragou. Basta executar os testes, pois ele mesmo, por baixo do capô, criará um banco de dados de teste, executará as migrações e, após a execução, fará a limpeza. Convenientemente? Certamente. É que leva tempo para fazer migrações - uma carruagem, mas isso parece ser um pagamento razoável pelo conforto, além disso, sempre há--reuse-db... O mais choque cultural é quando Junglers experientes vêm para outras línguas como Go. Ou seja, como é que não há automigrações antes e depois? Com suas mãos? E a base? Mãos também? E depois dos testes? O quê, e uma corda bamba com as mãos? E então o programador, intercalando o código com suspiros e suspiros, começa a escrever junga em Go em um projeto separado. Claro, tudo parece muito triste. No entanto, no Go é perfeitamente possível escrever testes de unidade rápidos e confiáveis ​​sem usar serviços de terceiros, como um banco de dados de teste ou cache.



Esta será minha história.



O que estamos testando?


Vamos imaginar que precisamos escrever uma função que verifique a presença de um funcionário no banco de dados por número de telefone.



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
}


Ok, eles escreveram. Como testar? Você pode, é claro, criar um banco de dados de teste antes de executar os testes, criar tabelas nele e, após executar esse banco de dados, travá-lo suavemente.



Mas também há outra maneira.



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.



, , .





Claro, essa abordagem tem suas desvantagens. Por exemplo, se sua lógica estiver ligada a algum tipo de lógica interna de banco de dados, esse teste não será capaz de identificar erros causados ​​pelo banco de dados. Mas acredito que o teste com a participação de um banco de dados e serviços de terceiros não é mais sobre testes unitários, são mais integração ou até mesmo testes e2e, e estão um pouco além do escopo deste artigo.



Obrigado por ler e escrever testes!




All Articles