Как проверить, была ли вызвана goroutine во время юнит-тестирования в Голанге?

Предположим, что у нас есть такой метод:

func method(intr MyInterface) {
    go intr.exec()
} 

В модульном тестировании methodМы хотим утверждать, что inter.exec был вызван один раз и только один раз; таким образом, мы можем смоделировать его с помощью другой фиктивной структуры в тестах, что даст нам функциональность, чтобы проверить, был ли он вызван или нет:

type mockInterface struct{
    CallCount int
}

func (m *mockInterface) exec() {
    m.CallCount += 1
}

И в юнит-тестах:

func TestMethod(t *testing.T) {
    var mock mockInterface{}
    method(mock)
    if mock.CallCount != 1 {
        t.Errorf("Expected exec to be called only once but it ran %d times", mock.CallCount)
    }
}

Теперь проблема в том, что с intr.exec вызывается с go ключевое слово, мы не можем быть уверены, что когда мы достигаем нашего утверждения в тестах, оно вызывается или нет.

Возможное решение 1:

Добавление канала в аргументы intr.exec может решить это: мы можем ждать получения любого объекта от него в тестах, и после получения объекта от него мы можем продолжать утверждать, что он вызывается. Этот канал будет полностью не использован в производственных (не тестовых) кодах. Это будет работать, но это добавляет ненужную сложность к не тестовым кодам и может сделать большие базы кода непонятными.

Возможное решение 2:

Добавление относительно небольшого количества сна к тестам перед утверждением может дать нам некоторую уверенность в том, что горутин будет вызван до того, как сон закончится:

func TestMethod(t *testing.T) {
    var mock mockInterface{}
    method(mock)

    time.sleep(100 * time.Millisecond)

    if mock.CallCount != 1 {
        t.Errorf("Expected exec to be called only once but it ran %d times", mock.CallCount)
    }
}

Это оставит не тестовые коды, как сейчас.
Проблема в том, что это сделает тесты медленнее и сделает их нестабильными, поскольку они могут сломаться при некоторых случайных обстоятельствах.

Возможное решение 3:

Создание полезной функции, подобной этой:

var Go = func(function func()) {
    go function()
} 

И переписать method как это:

func method(intr MyInterface) {
    Go(intr.exec())
} 

В тестах мы могли изменить Go к этому:

var Go = func(function func()) {
    function()
} 

Итак, когда мы проводим тесты, intr.exec будет вызываться синхронно, и мы можем быть уверены, что наш ложный метод вызывается до подтверждения.
Единственная проблема этого решения заключается в том, что оно переопределяет фундаментальную структуру golang, что неправильно.


Это решения, которые я мог бы найти, но, насколько я вижу, они не являются удовлетворительными. Какое решение лучше?

3 ответа

Решение

Использовать sync.WaitGroup в макете

Вы можете продлить mockInterface чтобы подождать, пока другой горутин закончит

type mockInterface struct{
    wg sync.WaitGroup // create a wait group, this will allow you to block later
    CallCount int
}

func (m *mockInterface) exec() {
    m.wg.Done() // record the fact that you've got a call to exec
    m.CallCount += 1
}

func (m *mockInterface) currentCount() int {
    m.wg.Wait() // wait for all the call to happen. This will block until wg.Done() is called.
    return m.CallCount
}

В тестах вы можете сделать:

mock := &mockInterface{}
mock.wg.Add(1) // set up the fact that you want it to block until Done is called once.

method(mock)

if mock.currentCount() != 1 {  // this line with block
    // trimmed
}

Этот тест не будет зависать вечно, как с предложенным выше решением sync.WaitGroup. Он будет зависать на секунду (в данном конкретном примере) в случае, если нет вызова mock.exec:

package main

import (
    "testing"
    "time"
)

type mockInterface struct {
    closeCh chan struct{}
}

func (m *mockInterface) exec() {
    close(closeCh)
}

func TestMethod(t *testing.T) {
    mock := mockInterface{
        closeCh: make(chan struct{}),
    }

    method(mock)

    select {
    case <-closeCh:
    case <-time.After(time.Second):
        t.Fatalf("expected call to mock.exec method")
    }
}

Это в основном то, что mc.Wait(time.Second) в моем ответе выше.

Прежде всего, я бы использовал генератор ложных выражений, т.е. github.com/gojuno/minimock, вместо того, чтобы писать сами себя:

minimock -f example.go -i MyInterface -o my_interface_mock_test.go

тогда ваш тест может выглядеть так (кстати, тестовая заглушка также генерируется с помощью github.com/hexdigest/gounit)

func Test_method(t *testing.T) {
    type args struct {
        intr MyInterface
    }
    tests := []struct {
        name string
        args func(t minimock.Tester) args
    }{
        {
            name: "check if exec is called",
            args: func(t minimock.Tester) args {
                return args{
                    intr: NewMyInterfaceMock(t).execMock.Return(),
                }
            },
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            mc := minimock.NewController(t)
            defer mc.Wait(time.Second)

            tArgs := tt.args(mc)

            method(tArgs.intr)
        })
    }
}

В этом тесте

defer mc.Wait(time.Second)

Ожидает вызова всех смоделированных методов.

Другие вопросы по тегам