在Go lang中使用模拟编写单元测试

I recently started learning Golang and wrote a REST api in it. Now I want to do its unit testing it by isolating different components.

The project structure looks like below and there are two major packages

├── main.go
├── routes
│   ├── routes.go
│   └── routes_test.go
├── db
│   └── db.go

main.go: Main entry point for the project

routes/routes.go: HTTP route handler package

db/db.go: database handler package

Now when I am testing the http routing I only want to test if the requests are routed to proper handler functions are then they respond accordingly. In real application the handler function would actually insert/update the database but I don't want to do that while testing. So if I can mock a database object and make my handler functions connect/read/write against that, but I am not sure how to do that.

When it comes to testing the database handler package db.go, I think i can't mock that and I have to either set up a test db against which I should make queries.

This is how my files look.

routes/routes.go

    package routes

    import (
        "cfengine-service/db"
        "encoding/json"
        "net/http"
        "strings"

        log "github.com/sirupsen/logrus"

        "github.com/gorilla/mux"
    )

    // Route - struct modal for routes
    type Route struct {
        Name        string
        EndPoint    string
        Methods     []string
        HandlerFunc http.HandlerFunc
    }
    var dbHandler = &db.Mongo{}

    // SetupRouter - returns the mux router
    func SetupRouter() *mux.Router {
        err := dbHandler.Connect()
        if err != nil {
            log.Fatal(err)
        }
        router := mux.NewRouter()
        for _, route := range SetUpRoutes() {
            router.HandleFunc(route.EndPoint, route.HandlerFunc).Methods(route.Methods...)
        }
        return router

    }
    func SetUpRoutes() []Route {
        return []Route{
            {
                "Host config Management",
                "/configs/group",
                []string{"GET", "POST"},
                GroupConfig,
            },
    }

func GroupConfig(w http.ResponseWriter, r *http.Request) {
    reqMethod := r.Method
    if reqMethod == "GET" {
        GetGroupConfig(w, r)
    } else if reqMethod == "POST" {
        NewGroupConfig(w, r)
    }

}

func GetGroupConfig(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    // g := r.URL.Query().Get("groupname")
    g := r.FormValue("groupname")
    if g == "" {
        if data, err := dbHandler.ReadAllGroups(); StatusWriter(w, err) {
            _ = json.NewEncoder(w).Encode(data)
        }
    } else {
        if data, err := dbHandler.ReadOneGroup(g); StatusWriter(w, err) {
            _ = json.NewEncoder(w).Encode(data)
        }
    }

}

// NewGroupConfig - end point for creating new config group
func NewGroupConfig(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("content-type", "application/json")
    var group db.GroupConfig
    err := json.NewDecoder(r.Body).Decode(&group)
    StatusWriter(w, err)

    err = dbHandler.CreateGroupConfig(&group)
    StatusWriter(w, err)
}

db/db.go

    package db

    import (
        "cfengine-service/config"
        "cfengine-service/helpers"
        "context"
        "fmt"
        "time"

        log "github.com/sirupsen/logrus"
        "go.mongodb.org/mongo-driver/bson"
        "go.mongodb.org/mongo-driver/mongo"
        "go.mongodb.org/mongo-driver/mongo/options"
    )

    type HostConfig struct {
        Hostname  string            `bson:"_id" json:"hostname"`
        Type      string            `bson:"type" json:"type"`
        Groups    []string          `bson:"groups" json:"groups"`
        Overrides map[string]string `bson:"overrides" json:"overrides"`
        Excludes  []string          `bson:"excludes" json:"excludes"`
        Data      map[string]string `bson:"data" json:"data"`
    }


    // Mongo collection
    type Mongo struct {
        collection *mongo.Collection
    }

    // Storage is interface for db operations
    type Storage interface {
        Connect() error
        ReadAll() ([]HostConfig, error)
        ReadOne() (*HostConfig, error)
        CreateHostConfig(h *HostConfig) error
        CreateGroupConfig(g *GroupConfig) error
        DeleteHostConfig(h *HostConfig) error
        UpdateHostGrops(h string, g string, opr string) error
        validateGroup(ctx context.Context, val string) error
    }

    func (m *Mongo) Connect() error {
        ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
        defer cancel()
        //client, err := mongo.NewClient(config.Config.MongoURI)
        client, err := mongo.Connect(ctx, options.Client().ApplyURI(config.Config.MongoURI))
        if err != nil {
            return err
        }

        log.Info("Connected to MongoDB.....")
        m.collection = client.Database(config.Config.MongoDbName).Collection(config.Config.MongoCollectionName)
        return nil
    }
func (m *Mongo) CreateGroupConfig(g *GroupConfig) error {
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()
    data, err := bson.Marshal(g)
    if err != nil {
        log.Error(err.Error())
        return err
    }
    _, err = m.collection.InsertOne(ctx, data)
    if err != nil {
        return err
    }
    return nil
}

func (m *Mongo) ReadOneGroup(g string) (GroupConfig, error) {
    ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
    defer cancel()
    var group GroupConfig
    err := m.collection.FindOne(ctx, bson.M{"_id": g}).Decode(&group)
    if err != nil {
        return GroupConfig{}, err
    }

    return group, nil
}

func (m *Mongo) ReadAllGroups() ([]GroupConfig, error) {
    var configs []GroupConfig
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()
    cur, err := m.collection.Find(ctx, bson.M{"type": "group"})
    if err != nil {
        return nil, err
    }
    defer cur.Close(ctx)
    for cur.Next(ctx) {
        var record GroupConfig
        if err = cur.Decode(&record); err != nil {
            return nil, err
        }
        configs = append(configs, record)
    }
    return configs, nil
}

So using the mockgen package i generated the mocks for Storage interface in my db.go but I am not sure how do I start using that. Here is how mock_db.go looks.

mock_db.go

type MockStorage struct {
    ctrl     *gomock.Controller
    recorder *MockStorageMockRecorder
}

// MockStorageMockRecorder is the mock recorder for MockStorage
type MockStorageMockRecorder struct {
    mock *MockStorage
}

// NewMockStorage creates a new mock instance
func NewMockStorage(ctrl *gomock.Controller) *MockStorage {
    mock := &MockStorage{ctrl: ctrl}
    mock.recorder = &MockStorageMockRecorder{mock}
    return mock
}

// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockStorage) EXPECT() *MockStorageMockRecorder {
    return m.recorder
}

// Connect mocks base method
func (m *MockStorage) Connect() error {
    m.ctrl.T.Helper()
    ret := m.ctrl.Call(m, "Connect")
    ret0, _ := ret[0].(error)
    return ret0
}

// Connect indicates an expected call of Connect
func (mr *MockStorageMockRecorder) Connect() *gomock.Call {
    mr.mock.ctrl.T.Helper()
    return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Connect", reflect.TypeOf((*MockStorage)(nil).Connect))
}
func (m *MockStorage) CreateGroupConfig(arg0 *db.GroupConfig) error {
    m.ctrl.T.Helper()
    ret := m.ctrl.Call(m, "CreateGroupConfig", arg0)
    ret0, _ := ret[0].(error)
    return ret0
}

// CreateGroupConfig indicates an expected call of CreateGroupConfig
func (mr *MockStorageMockRecorder) CreateGroupConfig(arg0 interface{}) *gomock.Call {
    mr.mock.ctrl.T.Helper()
    return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateGroupConfig", reflect.TypeOf((*MockStorage)(nil).CreateGroupConfig), arg0)
}

So can anyone please guide me here or give me some hints to move in right direction.

The mistake in testing handlers you are making is you are creating new objects so-called "Singleton object" inside the SetupRouter function, for all dependencies required to your application should be instantiated in main.go and take them as an interface where you need them. Also define small interfaces - ideal interface size is 1 or 2 methods.

// Storage is an interface that you already have, this way you can pass your mock object while unit testing, Router should also be an interface.

func SetupRouter(db Storage, router Router) *mux.Router {

        err := db.Connect()
        if err != nil {
            log.Fatal(err)
        }
        //router := mux.NewRouter() - you do not need to do this now
        // then perform whateven actions you want to do with router
}

So the above snippet is just a sample that how you can mock the dependencies which in your case DB, but as @Adrian said in a comment, if this code is production code, this requires a lot of refactoring.