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