Spec-Zone.ru › Go

Пакет synctest

  • import "testing/synctest"
  • Обзор
  • Указатель

Обзор

Пакет synctest предоставляет поддержку для тестирования конкурентного кода.

Функция Test запускает функцию в изолированном «пузыре». Все горутины, запущенные внутри пузыря, также являются его частью.

Каждый тест должен быть полностью самодостаточным. Для большинства тестов следует придерживаться следующих рекомендаций:

  • Не взаимодействуйте с горутинами, запущенными не из теста.
  • Не используйте сеть. При необходимости используйте фиктивную реализацию сети.
  • Не взаимодействуйте с внешними процессами.
  • Не допускайте утечки горутин в фоновых задачах.

Время

Внутри пузыря пакет time использует фиктивные часы. У каждого пузыря свои часы. Начальное время — полночь UTC 2000-01-01.

Время в пузыре продвигается только тогда, когда каждая горутина в пузыре устойчиво заблокирована. Точное определение понятия «устойчиво заблокирована» приведено ниже.

Например, этот тест выполняется сразу, а не занимает две секунды:

func TestTime(t *testing.T) {
	synctest.Test(t, func(t *testing.T) {
		start := time.Now() // always midnight UTC 2000-01-01
		go func() {
			time.Sleep(1 * time.Second)
			t.Log(time.Since(start)) // always logs "1s"
		}()
		time.Sleep(2 * time.Second) // the goroutine above will run before this Sleep returns
		t.Log(time.Since(start))    // always logs "2s"
	})
}

Время перестаёт продвигаться, когда завершается корневая горутина пузыря.

Блокировка

Горутина в пузыре «устойчиво заблокирована», если она заблокирована и разблокировать её может только другая горутина в том же пузыре. Горутина, которую может разблокировать событие за пределами её пузыря, не является устойчиво заблокированной.

Функция Wait блокируется, пока все остальные горутины в пузыре не окажутся устойчиво заблокированными.

Например:

func TestWait(t *testing.T) {
	synctest.Test(t, func(t *testing.T) {
		done := false
		go func() {
			done = true
		}()
		// Wait will block until the goroutine above has finished.
		synctest.Wait()
		t.Log(done) // always logs "true"
	})
}

Когда каждая горутина в пузыре устойчиво заблокирована:

  • Wait возвращает управление, если она была вызвана.
  • В противном случае время продвигается до следующего момента, который разблокирует хотя бы одну горутину, если такой момент существует и корневая горутина пузыря ещё не завершилась.
  • В противном случае возникает взаимная блокировка, и Test вызывает панику.

Следующие операции устойчиво блокируют горутину:

  • блокирующая отправка или получение по каналу, созданному внутри пузыря
  • блокирующий оператор select, в котором каждый вариант — канал, созданный внутри пузыря
  • sync.Cond.Wait
  • sync.WaitGroup.Wait, если sync.WaitGroup.Add был вызван внутри пузыря
  • time.Sleep

Операции, не перечисленные выше, не являются устойчиво блокирующими. В частности, следующие операции могут заблокировать горутину, но не являются устойчиво блокирующими, поскольку горутину может разблокировать событие, происходящее за пределами её пузыря:

  • блокировка на sync.Mutex или sync.RWMutex
  • блокировка при выполнении ввода-вывода, например при чтении из сетевого сокета
  • системные вызовы

Изоляция

Канал, time.Timer или time.Ticker, созданный внутри пузыря, связан с этим пузырём. Операция с каналом, таймером или тикером, созданным в пузыре, из-за его пределов вызывает панику.

sync.WaitGroup связывается с пузырём при первом вызове Add или Go. После связывания WaitGroup с пузырём вызов Add или Go за пределами этого пузыря приводит к критической ошибке. (Из-за технического ограничения WaitGroup, объявленный как переменная пакета, например «var wg sync.WaitGroup», нельзя связать с пузырём, и операции с ним могут не быть устойчиво блокирующими. Это ограничение не распространяется на *WaitGroup, хранящийся в переменной пакета, например «var wg = new(sync.WaitGroup)».)

sync.Cond.Wait является устойчиво блокирующей операцией. Пробуждение из-за пределов пузыря горутины, заблокированной внутри него на Cond.Wait, приводит к критической ошибке.

Функции очистки и финализаторы, зарегистрированные с помощью runtime.AddCleanup и runtime.SetFinalizer, выполняются вне любого пузыря.

Пример: Context.AfterFunc

В этом примере демонстрируется тестирование функции context.AfterFunc.

AfterFunc регистрирует функцию, которая будет выполнена в новой горутине после отмены контекста.

Тест проверяет, что функция не запускается до отмены контекста и запускается после неё.

func TestContextAfterFunc(t *testing.T) {
	synctest.Test(t, func(t *testing.T) {
		// Create a context.Context which can be canceled.
		ctx, cancel := context.WithCancel(t.Context())

		// context.AfterFunc registers a function to be called
		// when a context is canceled.
		afterFuncCalled := false
		context.AfterFunc(ctx, func() {
			afterFuncCalled = true
		})

		// The context has not been canceled, so the AfterFunc is not called.
		synctest.Wait()
		if afterFuncCalled {
			t.Fatalf("before context is canceled: AfterFunc called")
		}

		// Cancel the context and wait for the AfterFunc to finish executing.
		// Verify that the AfterFunc ran.
		cancel()
		synctest.Wait()
		if !afterFuncCalled {
			t.Fatalf("after context is canceled: AfterFunc not called")
		}
	})
}

Пример: Context.WithTimeout

В этом примере демонстрируется тестирование функции context.WithTimeout.

WithTimeout создаёт контекст, который отменяется по истечении тайм-аута.

Тест проверяет, что контекст не отменяется до истечения тайм-аута и отменяется после его истечения.

func TestContextWithTimeout(t *testing.T) {
	synctest.Test(t, func(t *testing.T) {
		// Create a context.Context which is canceled after a timeout.
		const timeout = 5 * time.Second
		ctx, cancel := context.WithTimeout(t.Context(), timeout)
		defer cancel()

		// Wait just less than the timeout.
		time.Sleep(timeout - time.Nanosecond)
		synctest.Wait()
		if err := ctx.Err(); err != nil {
			t.Fatalf("before timeout: ctx.Err() = %v, want nil\n", err)
		}

		// Wait the rest of the way until the timeout.
		time.Sleep(time.Nanosecond)
		synctest.Wait()
		if err := ctx.Err(); err != context.DeadlineExceeded {
			t.Fatalf("after timeout: ctx.Err() = %v, want DeadlineExceeded\n", err)
		}
	})
}

Пример: HTTP 100 Continue

В этом примере демонстрируется тестирование обработки ответа 100 Continue в [http.Transport].

HTTP-клиент, отправляющий запрос, может добавить заголовок «Expect: 100-continue», чтобы сообщить серверу о наличии дополнительных данных для отправки. Сервер может ответить информационным ответом 100 Continue, запросив данные, или указать другой статус, сообщающий клиенту, что данные не нужны. Например, клиент, загружающий большой файл, может использовать эту возможность, чтобы убедиться, что сервер готов принять файл, прежде чем отправлять его.

Этот тест проверяет, что при отправке заголовка «Expect: 100-continue» HTTP-клиент не отправляет содержимое запроса до того, как сервер запросит его, и отправляет содержимое после получения ответа 100 Continue.

func TestHTTPTransport100Continue(t *testing.T) {
	synctest.Test(t, func(*testing.T) {
		// Create an in-process fake network connection.
		// We cannot use a loopback network connection for this test,
		// because goroutines blocked on network I/O prevent a synctest
		// bubble from becoming idle.
		srvConn, cliConn := net.Pipe()
		defer cliConn.Close()
		defer srvConn.Close()

		tr := &http.Transport{
			// Use the fake network connection created above.
			DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
				return cliConn, nil
			},
			// Enable "Expect: 100-continue" handling.
			ExpectContinueTimeout: 5 * time.Second,
		}

		// Send a request with the "Expect: 100-continue" header set.
		// Send it in a new goroutine, since it won't complete until the end of the test.
		body := "request body"
		go func() {
			req, _ := http.NewRequest("PUT", "http://test.tld/", strings.NewReader(body))
			req.Header.Set("Expect", "100-continue")
			resp, err := tr.RoundTrip(req)
			if err != nil {
				t.Errorf("RoundTrip: unexpected error %v\n", err)
			} else {
				resp.Body.Close()
			}
		}()

		// Read the request headers sent by the client.
		req, err := http.ReadRequest(bufio.NewReader(srvConn))
		if err != nil {
			t.Fatalf("ReadRequest: %v\n", err)
		}

		// Start a new goroutine copying the body sent by the client into a buffer.
		// Wait for all goroutines in the bubble to block and verify that we haven't
		// read anything from the client yet.
		var gotBody bytes.Buffer
		go io.Copy(&gotBody, req.Body)
		synctest.Wait()
		if got, want := gotBody.String(), ""; got != want {
			t.Fatalf("before sending 100 Continue, read body: %q, want %q\n", got, want)
		}

		// Write a "100 Continue" response to the client and verify that
		// it sends the request body.
		srvConn.Write([]byte("HTTP/1.1 100 Continue\r\n\r\n"))
		synctest.Wait()
		if got, want := gotBody.String(), body; got != want {
			t.Fatalf("after sending 100 Continue, read body: %q, want %q\n", got, want)
		}

		// Finish up by sending the "200 OK" response to conclude the request.
		srvConn.Write([]byte("HTTP/1.1 200 OK\r\n\r\n"))

		// We started several goroutines during the test.
		// The synctest.Test call will wait for all of them to exit before returning.
	})
}

Указатель

  • func Sleep(d time.Duration)
  • func Test(t *testing.T, f func(*testing.T))
  • func Wait()

Файлы пакета

synctest.go

func Sleep 1.27

func Sleep(d time.Duration)

Sleep блокируется, пока часы текущего пузыря не продвинутся на длительность d и все горутины текущего пузыря, кроме текущей горутины, не окажутся устойчиво заблокированными.

Это полностью эквивалентно

time.Sleep(d)
synctest.Wait()

В тестах это часто предпочтительнее, чем просто вызвать time.Sleep. Если сам тест и другая горутина, выполняющая тестируемую систему, спят одинаковое время, невозможно предсказать, какая из двух горутин запустится первой. Обычно тесту нужно дождаться, пока тестируемая система «придёт в устойчивое состояние» после ожидания. Именно это и делает Sleep.

func Test 1.25

func Test(t *testing.T, f func(*testing.T))

Test выполняет f в новом пузыре.

Test ожидает завершения всех горутин в пузыре, прежде чем вернуться. Если в горутинах пузыря возникает взаимная блокировка, тест завершается ошибкой.

Test нельзя вызывать изнутри пузыря.

Объект *testing.T, передаваемый в f, обладает следующими свойствами:

  • Функции T.Cleanup выполняются внутри пузыря непосредственно перед возвратом из Test.
  • T.Context возвращает context.Context с каналом Done, связанным с пузырём.
  • Методы T.Run, T.Parallel и T.Deadline вызывать нельзя.

func Wait 1.25

func Wait()

Wait блокируется, пока все горутины текущего пузыря, кроме текущей горутины, не окажутся устойчиво заблокированными.

Wait нельзя вызывать за пределами пузыря. Wait нельзя одновременно вызывать из нескольких горутин одного пузыря.

© Google, Inc.
Licensed under the Creative Commons Attribution License 3.0.
http://golang.org/pkg/testing/synctest/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API