How do I add testing to the data access layer methods?

Viewed 68

I am having a lot of difficulty in writing tests for my graphql resolvers.

Let's assume I have a resolver, which handles the creation of a new user. The resolver would look something like:

func (r *mutationResolver) CreateUser(ctx context.Context, name string) (*model.User, error) {
    u, err := doa.AddUser(ctx, client, index, name)
    if err != nil {
        fmt.Println("Could not add User to db")
        return nil, err
    }
    fmt.Println("Insertion Successful")
    return u, nil
}

My doa layer has the AddUser method.

func AddUser(ctx context.Context, client *elastic.Client, index string, name string) (*model.User, error) {
    u := &model.User{
        UserID: uuid.New().String(),
        Name:   name,
    }
    s, err := utils.ParseToString(u)
    if err != nil {
        return nil, err
    }
    _, err = client.Index().
             Index(index).
             BodyString(s).
             Do(ctx)
    if err != nil {
        fmt.Println("Error Storing the User")
        return nil, err
    }
    return u, err
}

I know I would be using a testing library like gomock or testify, but I don't know how to structure the code at all. How should I write the interfaces that the testing library would call?

1 Answers

For starters I would pull out the creation of the model.User type since that is a high level object that defines the policy that user IDs will be uuid. You don't want that entangled with a lower layer where you are interacting with elastic search. I would just pass the user in. Also you may want to test that users are created as expected. Having this logic in AddUser will complicate the testing.

Next I would use an interface to pass in the dependency on elastic.Client. That will allow you to mock it and see that AddUser is using that dependency as expected (i.e. adding the user).

You may even want to create a new type that holds the interfaces to your low level dependencies and provides AddUser as a method. You could then create an interface that has AddUser in it to separate clients to the interface from the low level details.

When to mock

Here is an example. I think the link above offers good advice on limiting this to architectural boundaries. You can see how it can get out of hand quickly.

package main

import (
    "context"
    "fmt"
    "github.com/olivere/elastic"
)

type User struct {}

type Repo interface {
    AddUser(User) error
}

type ElasticClient interface {
    Index() ElasticIndex
}

type ElasticIndex interface {
    Index(string) ElasticIndex
    BodyString(string) ElasticIndex
    Do(context.Context) (*elastic.IndexResponse, error)
}

type MockElasticClient struct {
    Log *[]string
    index MockElasticIndex
}

type MockElasticIndex struct {
    Log *[]string
}

func NewMockElasticClient() *MockElasticClient {
    var log []string
    return &MockElasticClient{
        Log: &log,
        index: MockElasticIndex{&log},
    }
}

func (c *MockElasticClient) Index() ElasticIndex {
    *c.Log = append(*c.Log, "Client.Index()")
    return &c.index
}

func (i *MockElasticIndex) Index(s string) ElasticIndex {
    *i.Log = append(*i.Log, fmt.Sprintf("Index.Index(%s)", s))
    return i
}

func (i *MockElasticIndex) BodyString(s string) ElasticIndex {
    *i.Log = append(*i.Log, fmt.Sprintf("Index.BodyString(%s)", s))
    return i
}

func (i *MockElasticIndex) Do(ctx context.Context) (*elastic.IndexResponse, error) {
    *i.Log = append(*i.Log, "Index.Do()")
    return nil, nil
}

// Use a wrapped client to deal with the fact that elastic
// returns their own types and not interfaces.
type WrappedElasticClient struct {
    wrapped *elastic.Client
}

type WrappedElasticIndexService struct {
    wrapped *elastic.IndexService
}

func (c WrappedElasticClient) Index() ElasticIndex {
    return WrappedElasticIndexService{c.wrapped.Index()}
}

func (i WrappedElasticIndexService) Index(s string) ElasticIndex {
    i.wrapped.Index(s)
    return i
}

func (i WrappedElasticIndexService) BodyString(s string) ElasticIndex {
    i.BodyString(s)
    return i
}

func (i WrappedElasticIndexService) Do(ctx context.Context) (*elastic.IndexResponse, error) {
    return i.wrapped.Do(ctx)
}

type ElasticRepo struct {
    ctx context.Context
    client ElasticClient
}

func (r ElasticRepo) AddUser(u User) error {
    s, err := utils.ParseToString(u)
    if err != nil {
        return err
    }

    _, err = r.client.Index().
        Index("some index").
        BodyString(s).
        Do(r.ctx)
    if err != nil {
        fmt.Println("Error Storing the User")
        return err
    }

    return err
}

func main() {
    m := NewMockElasticClient()
    r := ElasticRepo{
        ctx: nil,
        client: m,
        //client: WrappedElasticClient{&elastic.Client{}},
    }
    r.AddUser(User{})

    fmt.Println(m.Log)
}
Related