Go mocking with interfaces for testing
Asked Answered
I

2

8

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",
        },
    })
}
Isopropanol answered 25/3, 2021 at 16:33 Comment(4)
Structs aren't classes, they are structs. Overall, yes, you'd use an interface and substitute a mock for the real implementation in a test; however, that is not what your test does. Mocks are for dependencies; if type A depends on type B, and you want to test type A in isolation, you provide a mock of type B. Your test just tests the mock and real implementations directly, making the mock fairly pointless.Forgave
Not an answer to your quest but some general advice: Go's testing works best if you do it "idiomatic". As this is hard to describe I'd like to encourage you to take a look at test of the stdlib, see e.g. github.com/golang/go/tree/master/src/bufio . Mocking is a common technique in "traditional OOP" but testing in Go often becomes easier if you use different test doubles than mocks: using stubs and fakes is much easier in Go than what most people think and make your tests easier and more robust. Packages like assert seem convenient, but give plain package testing a try.Baggs
@Forgave I mean yes in real implementation it would have just mock to mock values it was more for checking how all this thing would workIsopropanol
@Baggs I think it answers my question quite accurately I was looking at how to do it in a Go or what are the best practices.Isopropanol
R
6

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.

Richardricharda answered 27/3, 2021 at 10:46 Comment(0)
N
5

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 ./... 
Nickynico answered 17/4, 2023 at 10:25 Comment(2)
In this example, how would you simulate all the scenarios like Find method returns value1, value2, returns an exception?Contrary
Please clarify your request and I'll update the code.Nickynico

© 2022 - 2024 — McMap. All rights reserved.