Как правильно написать тест для метода? [Go]
В рамках изучения тестирования в целом и testify в частности, я хочу написать тест для метода Worker в моем учебном проектике. Подскажите, как мне правильно это сделать? Собственно, сам метод:
package worker
import (
"context"
"fmt"
)
type Queue interface {
TakeMessage() (\<-chan string, error)
CloseConnections()
}
type Download interface {
Download(url string) error
}
type Worker struct {
q Queue
d Download
}
func NewWorker(queue Queue, download Download) \*Worker {
newWorker := Worker{}
newWorker.q = queue
newWorker.d = download
return &newWorker
}
func (w *Worker) Worker(ctx context.Context) error {
msgs, err := w.q.TakeMessage()
if err != nil {
return fmt.Errorf("error while consume queue: %w", err)
}
for {
select {
case <-ctx.Done():
w.q.CloseConnections()
return nil
case msg := <-msgs:
fmt.Println(msg)
w.d.Download(msg)
}
}
Я хочу протестировать, что при закрытии контекста возвращается nil, и что сообщение из очереди обрабатывается и передается в метод Download.
Я пока пытаюсь сделать как-то так, но это работает не так, как мне хотелось бы. Я предполагаю такое поведение: я создаю и возвращаю канал с "some text/some.txt" в мокированном методе TakeQueue, далее сообщение из этого канала передается в мокированный метод Download. Вместо этого тест у меня подвисает. Полагаю, что это нормально поведение, т.к. код метода в бесконечном цикле ждет сообщения.
type MockQueue struct {
mock.Mock
}
type MockDownloader struct {
mock.Mock
}
func (m *MockQueue) TakeMessage() (<-chan string, error) {
//args := m.Called()
strCh := make(chan string)
strCh <- "some_url/some.txt"
return strCh, nil
}
func (d *MockDownloader) Download(url string) error {
if url == "some_url/some.txt" {
return nil
} else {
return errors.New(url)
}
// args := d.Called(url)
// return args.Error(1)
}
func TestWorker(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
newQueue := &MockQueue{}
newDownload := &MockDownloader{}
//newDownload.On("Download").Return(nil)
newWorker := Worker{newQueue, newDownload}
err := newWorker.Worker(ctx)
if err == nil {
t.Errorf("test pass")
} else {
t.Errorf("test fail, want \"some_url/some.txt\", got: ")
}
}
Отредактированный вариант:
func TestWorkerCloseContext(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
newQueue := &MockQueue{}
newDownload := &MockDownloader{}
newWorker := Worker{newQueue, newDownload}
go func() {
err := newWorker.Worker(ctx)
if err == nil {
t.Errorf("test pass")
} else {
t.Errorf("test fail")
}
}()
time.Sleep(1 * time.Second)
cancel()
}
func TestWorkerMessageReceive(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
newQueue := &MockQueue{}
newDownload := &MockDownloader{}
newWorker := Worker{newQueue, newDownload}
go func() {
err := newWorker.Worker(ctx)
if err == nil {
t.Errorf("test pass")
} else {
t.Errorf("test fail, want \"some_url/some.txt\", got: ")
}
}()
time.Sleep(1 * time.Second)
cancel()
}
Ответы (1 шт):
Асинхронный код поначалу может быть сложным для восприятия. Попробуйте проговорить то, что происходит в вашем тесте:
- вы создаёте воркера с контекстом и запускаете его,
- воркер выполняет синхронный бесконечный цикл...
В этом моменте ваш код зависнет. Вы не сможете закрыть контекст на следующей строке теста. Получается вам надо распраллелить выполнение в этом месте. Пусть бесконечный цикл крутится в отдельной горутине, а ваш тест продолжается дальше:
- вы создаёте воркера с контекстом и запускаете его в отдельной горутине,
- воркер выполняет бесконечный цикл, ожидая закрытия контекста,
- вы закрываете контекст в тесте,
- вы ждёте завершения горутины с воркером и проверяете, что же он там вернул.
Получение сообщений делается аналогично.
Сперва разделите кейсы с получением сообщений и обработкой закрытия контекста. Так вам будет проще написать рабочие тесты. Потом вы можете объединить их, если вам покажется это необходимым.
package main
import "testing"
type Queue interface {
TakeMessage() <-chan string
}
type Downloader interface {
Download(msg string)
}
func Worker(q Queue, d Downloader) {
msgs := q.TakeMessage()
for {
select {
case msg := <- msgs:
d.Download(msg + " bug")
}
}
}
type QueueMock struct {
Ch chan string
}
func (q QueueMock) AddMessage(msg string) {
q.Ch <- msg
}
func (q QueueMock) TakeMessage() <-chan string {
return q.Ch
}
type DownloaderMock struct {
OnDownload func(msg string)
}
func (d DownloaderMock) Download(msg string) {
d.OnDownload(msg)
}
func TestWoker(t *testing.T) {
q := QueueMock{
Ch: make(chan string),
}
d := DownloaderMock{}
go func() {
Worker(q, d)
}()
expected := "hello world"
dch := make(chan bool)
d.OnDownload = func(actual string) {
if actual != expected {
t.Errorf("\"%v\" not equal \"%v\"", actual, expected)
}
dch <- true
}
q.AddMessage(expected)
<- dch
}