Unit testing in Go with interfaces

Instead of introducing


This article is for those who, like me, came to Go from the world of Django. Well, Django spoiled us. One has only to run the tests, as he himself, under the hood, will create a test database, run the migrations, and after the run will clean up after himself. Conveniently? Certainly. It's just that it takes time to run migrations - a carriage, but this seems to be a reasonable payment for comfort, plus there is always--reuse-db... The culture shock is all the more intense when seasoned Junglers come to other languages, such as Go. That is, how is it no automigrations before and after? With your hands? And the base? Hands too? And after the tests? What, and a tirdown with your hands? Well, then the programmer, interspersing the code with sighs and sighs, begins to write jungu in Go in a separate project. Of course, it all looks very sad. However, in Go it is quite possible to write fast and reliable unit tests without using third-party services such as a test database or cache.



This will be my story.



What are we testing?


Let's imagine that we need to write a function that checks the presence of an employee in the database by phone number.



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, they wrote. How to test it? You can, of course, create a test database before running the tests, create tables in it, and after running this database, gently crash it.



But there is another way.



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.



, , .





Of course, this approach has its drawbacks. For example, if your logic is tied to some kind of internal database logic, then such testing will not be able to identify errors caused by the database. But I believe that testing with the participation of a database and third-party services is no longer about unit tests, these are rather integration or even e2e tests, and they are somewhat beyond the scope of this article.



Thanks for reading and writing tests!




All Articles