Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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",
        },
    })
}
like image 645
LeTadas Avatar asked Jun 17 '26 12:06

LeTadas


1 Answers

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

  • https://github.com/stretchr/testify
  • https://github.com/golang/mock
  • https://github.com/vektra/mockery
  • etc...

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 ./... 
like image 127
mikolaj semeniuk Avatar answered Jun 20 '26 10:06

mikolaj semeniuk



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!