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

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

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 ]

0 голосов
/ 27 июня 2018

прежде всего я бы использовал генератор ложных сигналов, т.е. 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)

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

0 голосов
/ 28 июня 2018

Этот тест не будет зависать вечно, как с предложенным выше решением 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) в моем ответе выше.

0 голосов
/ 27 июня 2018

Используйте 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
}
...