LeTadas
LeTadas

Reputation: 3556

Go mocking with interfaces for testing

I'm pretty new with Go, and I'm coming from OOP languages. Now the concept seems quite different in go of interfaces and classes.

I was wondering how mocking would work in case of testing. The confusion I'm having is whether ok to use struct as a classes and if the approach below is how you suppose to do? Assuming that DefaultArticlesRepository would be for real data and MockArticlesRepository for mocking it.

type ArticlesRepository interface {
    GetArticleSections() []ArticleSectionResponse
}

type DefaultArticlesRepository struct{}
type MockArticlesRepository struct{}

func (repository DefaultArticlesRepository) GetArticleSections() []ArticleSectionResponse {
    return []ArticleSectionResponse{
        {
            Title: "Default response",
            Tag:   "Default Tag",
        },
    }
}

func (repository MockArticlesRepository) GetArticleSections() []ArticleSectionResponse {
    return []ArticleSectionResponse{
        {
            Title: "Mock response",
            Tag:   "Mock Tag",
        },
    }
}

func ArticleSectionsProvider(v ArticlesRepository) ArticlesRepository {
    return v
}

func TestFoo(t *testing.T) {
    realProvider := ArticleSectionsProvider(DefaultArticlesRepository{})
    mockProvider := ArticleSectionsProvider(MockArticlesRepository{})

    assert.Equal(t, realProvider.GetArticleSections(), []ArticleSectionResponse{
        {
            Title: "Default response",
            Tag:   "Default Tag",
        },
    })

    assert.Equal(t, mockProvider.GetArticleSections(), []ArticleSectionResponse{
        {
            Title: "Mock response",
            Tag:   "Mock Tag",
        },
    })
}

Upvotes: 8

Views: 27435

Answers (2)

mikolaj semeniuk
mikolaj semeniuk

Reputation: 2458

Firstly, there is no need to use any external mocking library like:

All you really is need is just an interface and some piece of code using standard library to run all your tests in parallel.

Check this real world example below instead of next "calculator testing example":

├── api
│   ├── storage.go
│   ├── server.go
│   └── server_test.go
└── main.go

api/storage.go

package api

import "database/sql"

type storage struct {
    // any database driver of your choice...
    pool *sql.DB
}

func NewStorage(p *sql.DB) *storage {
    return &storage{pool: p}
}

func (s *storage) Find() (string, error) {
    // use database driver to find from storage...
    return `{ "id": "123" }`, nil
}

func (s *storage) Add(v string) error {
    // use database driver to add to storage...
    return nil
}

api/server.go

package api

import (
    "fmt"
    "net/http"
)

type Storage interface {
    Find() (string, error)
    Add(string) error
}

type server struct {
    storage Storage
}

func NewServer(s Storage) *server {
    return &server{storage: s}
}

func (s *server) Find(w http.ResponseWriter, r *http.Request) {
    response, err := s.storage.Find()
    if err != nil {
        w.WriteHeader(http.StatusNotFound)
        fmt.Fprint(w, `{ "message": "not found" }`)
        return
    }

    fmt.Fprint(w, response)
}

func (s *server) Add(w http.ResponseWriter, r *http.Request) {
    query := r.URL.Query()
    name := query.Get("name")

    if err := s.storage.Add(name); err != nil {
        w.WriteHeader(http.StatusNotFound)
        fmt.Fprint(w, `{ "message": "not found" }`)
        return
    }

    fmt.Fprint(w, `{ "message": "success" }`)
}

api/server_test.go

package api

import (
    "errors"
    "net/http"
    "net/http/httptest"
    "testing"
)

type mock struct {
    find func() (string, error)
    add  func(string) error
}

func (m *mock) Find() (string, error) { return m.find() }
func (m *mock) Add(v string) error    { return m.add(v) }

func TestFindOk(t *testing.T) {
    t.Parallel()

    // Arrange
    expectedBody := `{ "message": "ok" }`
    expectedStatus := http.StatusOK
    m := &mock{find: func() (string, error) { return expectedBody, nil }}
    server := NewServer(m)
    recorder := httptest.NewRecorder()

    // Act
    server.Find(recorder, &http.Request{})

    // Assert
    if recorder.Code != expectedStatus {
        t.Errorf("want %d, got %d", expectedStatus, recorder.Code)
    }

    if recorder.Body.String() != expectedBody {
        t.Errorf("want %s, got %s", expectedBody, recorder.Body.String())
    }
}

func TestFindNotFound(t *testing.T) {
    t.Parallel()

    // Arrange
    expectedBody := `{ "message": "not found" }`
    expectedStatus := http.StatusNotFound
    m := &mock{find: func() (string, error) { return expectedBody, errors.New("not found") }}
    server := NewServer(m)
    recorder := httptest.NewRecorder()

    // Act
    server.Find(recorder, &http.Request{})

    // Assert
    if recorder.Code != expectedStatus {
        t.Errorf("want %d, got %d", expectedStatus, recorder.Code)
    }

    if recorder.Body.String() != expectedBody {
        t.Errorf("want %s, got %s", expectedBody, recorder.Body.String())
    }
}

func TestAddOk(t *testing.T) {
    t.Parallel()

    // Arrange
    expectedBody := `{ "message": "success" }`
    expectedStatus := http.StatusOK
    m := &mock{add: func(string) error { return nil }}
    server := NewServer(m)
    recorder := httptest.NewRecorder()

    // Act
    request, _ := http.NewRequest("GET", "/add?name=mike", nil)
    server.Add(recorder, request)

    // Assert
    if recorder.Code != expectedStatus {
        t.Errorf("want %d, got %d", expectedStatus, recorder.Code)
    }

    if recorder.Body.String() != expectedBody {
        t.Errorf("want %s, got %s", expectedBody, recorder.Body.String())
    }
}

Run your tests

go clean -testcache
go test ./... 

Upvotes: 6

Tung Nguyen
Tung Nguyen

Reputation: 845

Firstly, I suggest you to use https://github.com/vektra/mockery for generating mock structs automatically based on interfaces. Implementing a mock struct like your is ok but I think it just wastes your time and effort if you do not really need a very special behavior for that struct.

Secondly, we do not need to test mock structs like you do in your code.

assert.Equal(t, mockProvider.GetArticleSections(), []ArticleSectionResponse{
    {
        Title: "Mock response",
        Tag:   "Mock Tag",
    },
})

So when we use mock structs, suppose struct a is a dependency of struct b. For example:

type A interface {
    DoTask() bool
} 

type a struct {}

func (sa *a) DoTask() bool {
    return true
}

type b struct {
    a A
}

func (sb *b) DoSomething() bool {
    //Do some logic
    sb.a.DoTask();
    //Do some logic
    return true;
}

And you want to test function DoSomething of struct b. Of course you do not care and do not want to test function DoTask of struct a in this case. Then you just simply provide a mock of struct a to struct b in the test. This mock also helps you avoid to deal with any struggle related to struct a in testing struct b. Now your test should be like this:

func (s *TestSuiteOfStructB) TestDoSomething_NoError() {
    //Suppose that mockedOfA  is a mock of struct a
    instanceOfB := b{a: mockedOfA}
    mockedOfA.On("DoTask").Return(true)
    actualResult := instanceOfB.DoSomething()
    s.Equal(true, actualResult)
}

The last, this is just a small thing but do not see a clear responsibility of your ArticleSectionsProvider.

Upvotes: 5

Related Questions