The Go Blog
Обложка
Введение
С самого начала проекта Go был спроектирован с учетом инструментов. Эти инструменты включают в себя некоторые из самых узнаваемых технологий Go, такие как инструмент отображения документации
godoc,
инструмент форматирования кода
gofmt,
и переписывающая API программа
gofix.
Возможно, самым важным из них является
go команда,
программа, которая автоматически устанавливает, собирает и тестирует Go программы,
используя в качестве спецификации сборки только исходный код.
Выпуск Go 1.2 представляет новый инструмент для покрытия тестов, который использует необычный подход к генерации статистики покрытия, подход, основанный на технологии, заложенной в godoc и аналогичных инструментах.
Поддержка инструментов
Прежде всего, немного контекста: что означает, что язык поддерживает хорошие инструменты? Это означает, что язык делает простым написание хороших инструментов, и что его экосистема поддерживает создание инструментов самых разных видов.
Существует ряд свойств Go, которые делают его подходящим для инструментов. Во-первых, у Go регулярный синтаксис, который легко парсить. Грамматика стремится быть свободной от особых случаев, требующих сложной машины для анализа.
Там, где это возможно, Go использует лексические и синтаксические конструкции, чтобы сделать семантические свойства простыми для понимания. Примерами являются использование заглавных букв для определения экспортируемых имён и радикально упрощённые правила области видимости по сравнению с другими языками из традиции C.
Наконец, стандартная библиотека поставляется с пакетами высокого уровня для лексического и синтаксического анализа исходного кода Go. Также, необычно, она включает в себя пакет высокого уровня для pretty-printing синтаксических деревьев Go.
Эти пакеты в совокупности составляют основу инструмента gofmt, но pretty-printer стоит выделить отдельно. Поскольку он может принимать произвольное синтаксическое дерево Go и выводить стандартный, удобочитаемый, корректный код, он создаёт возможность строить инструменты, которые преобразуют дерево разбора и выводят изменённый, но корректный и легко читаемый код.
Один из примеров — это инструмент gofix, который автоматически переписывает код для использования новых языковых возможностей или обновлённых библиотек. Gofix позволил нам вносить фундаментальные изменения в язык и библиотеки в подготовительный период к Go 1.0, с уверенностью, что пользователи смогут просто запустить инструмент, чтобы обновить свой исходный код до последней версии.
Внутри Google мы использовали gofix для внесения масштабных изменений в огромном репозитории кода, что было бы почти невозможно в других языках программирования, которые мы используем. Больше не нужно поддерживать несколько версий некоторых API; мы можем использовать gofix, чтобы обновить всю компанию за один раз.
Это не только большие инструменты, которые обеспечивают эти пакеты, конечно. Они также позволяют легко создавать более скромные программы, такие как плагины IDE, например. Все эти элементы взаимосвязаны, делая среду Go более продуктивной за счёт автоматизации многих задач.
Покрытие тестами
Покрытие тестами — это термин, который описывает, какая часть кода пакета выполняется при запуске тестов этого пакета. Если выполнение тестовой сборки приводит к выполнению 80% операторов исходного кода пакета, говорят, что покрытие тестами составляет 80%.
Программа, обеспечивающая покрытие тестами в Go 1.2, является самой последней в эволюции инструментария экосистемы Go.
Обычный способ вычисления покрытия тестами — это инструментирование бинарного файла. Например, программа GNU gcov устанавливает точки останова в ветвлениях, выполняемых бинарным файлом. При выполнении каждой ветви точка останова сбрасывается, и целевые операторы этой ветви помечаются как «покрытые».
Такой подход успешен и широко используется. Даже ранний инструмент покрытия тестами для Go работал похожим образом. Но у него есть проблемы. Сложно реализовать, поскольку анализ выполнения бинарных файлов представляет собой сложную задачу. Кроме того, требуется надежный способ связывания следа выполнения с исходным кодом, что также может быть сложно, как может подтвердить любой пользователь отладчика на уровне исходного кода. Проблемы включают неточную информацию отладки и такие вопросы, как встроенные функции, усложняющие анализ. Самое главное, такой подход очень непортабелен. Его необходимо реализовывать заново для каждой архитектуры и в какой-то степени для каждой операционной системы, поскольку поддержка отладки сильно различается от системы к системе.
Однако он работает, и, например, если вы являетесь пользователем gccgo, инструмент gcov может предоставить информацию о покрытии тестами. Однако, если вы используете gc — более распространённый набор компиляторов Go, до Go 1.2 вы были без помощи.
Покрытие тестами для Go
Для нового инструмента покрытия тестами в Go мы выбрали другой подход, избегающий динамической отладки.
Идея проста: Переписать исходный код пакета перед компиляцией для добавления инструментирования,
скомпилировать и запустить изменённый исходный код, а затем вывести статистику.
Переписывание легко организовать, потому что команда go контролирует поток
от исходного кода до теста до выполнения.
Вот пример. Предположим, у нас есть простой однофайловый пакет, такой как:
package size
func Size(a int) string {
switch {
case a < 0:
return "negative"
case a == 0:
return "zero"
case a < 10:
return "small"
case a < 100:
return "big"
case a < 1000:
return "huge"
}
return "enormous"
}
и этот тест:
package size
import "testing"
type Test struct {
in int
out string
}
var tests = []Test{
{-1, "negative"},
{5, "small"},
}
func TestSize(t *testing.T) {
for i, test := range tests {
size := Size(test.in)
if size != test.out {
t.Errorf("#%d: Size(%d)=%s; want %s", i, test.in, size, test.out)
}
}
}
Чтобы получить информацию о покрытии тестами для пакета, вы запускаете тест с включённым покрытием, передав флаг -cover команде go test:
<code>% go test -cover PASS coverage: 42.9% of statements ok size 0.026s % </code>
Обратите внимание, что уровень покрытия составляет 42.9%, что не очень хорошо. Прежде чем рассмотреть, как его повысить, давайте посмотрим, как это вычисляется.
Когда включено покрытие тестами, команда go test запускает инструмент “cover”, отдельную программу, включённую в дистрибутив, которая переписывает исходный код перед компиляцией. Вот как выглядит переписанная функция Size:
func Size(a int) string {
GoCover.Count[0] = 1
switch {
case a < 0:
GoCover.Count[2] = 1
return "negative"
case a == 0:
GoCover.Count[3] = 1
return "zero"
case a < 10:
GoCover.Count[4] = 1
return "small"
case a < 100:
GoCover.Count[5] = 1
return "big"
case a < 1000:
GoCover.Count[6] = 1
return "huge"
}
GoCover.Count[1] = 1
return "enormous"
}
Каждый исполняемый фрагмент программы помечается инструкцией присваивания, которая при выполнении записывает информацию о том, что этот фрагмент был выполнен. Счётчик связан с исходным положением инструкций, которые он считает, через вторую структуру данных только для чтения, которая также генерируется инструментом cover. По завершении выполнения теста счётчики собираются, и процентное соотношение вычисляется на основе того, сколько из них было установлено.
Хотя эта аннотирующая инструкция может показаться затратной, она компилируется в одну инструкцию "move". Следовательно, её влияние на производительность незначительно, добавляя лишь около 3% при запуске типичного (более реалистичного) теста. Это делает разумным включение покрытия тестами в стандартный процесс разработки.
Просмотр результатов
Покрытие тестами для нашего примера было слабым. Чтобы выяснить причину, мы просим go test создать "профиль покрытия", файл, содержащий собранные статистические данные, чтобы мы могли более подробно их изучить. Это делается просто: используйте флаг -coverprofile, чтобы указать файл для вывода:
<code>% go test -coverprofile=coverage.out PASS coverage: 42.9% of statements ok size 0.030s % </code>
(Флаг -coverprofile автоматически включает -cover для анализа покрытия.) Тест запускается так же, как и раньше, но результаты сохраняются в файл. Чтобы проанализировать их, мы запускаем инструмент покрытия тестов самостоятельно, без использования go test. В качестве начальной точки можно запросить покрытие, разбитое по функциям, хотя в данном случае это не даст большой информации, поскольку имеется только одна функция:
<code>% go tool cover -func=coverage.out size.go: Size 42.9% total: (statements) 42.9% % </code>
Более интересный способ просмотра данных — получить HTML-представление исходного кода с нанесённой информацией о покрытии. Этот вид отображается с помощью флага -html:
<code>$ go tool cover -html=coverage.out </code>
Когда эта команда запускается, в браузере открывается окно, отображающее покрытый (зелёный), непокрытый (красный) и непроинструментированный (серый) исходный код. Вот скриншот:
С таким представлением сразу видно, в чём проблема: мы забыли протестировать несколько случаев! И мы можем точно определить, какие именно это были случаи, что облегчает улучшение покрытия тестами.
Тепловые карты
Одним из больших преимуществ подхода на уровне исходного кода для покрытия тестами является возможность разного способа инструментирования кода. Например, можно не только проверить, выполнялась ли инструкция, но и сколько раз она выполнялась.
Команда go test принимает флаг -covermode, чтобы установить режим покрытия на один из трёх вариантов:
- set: выполнялась ли каждая инструкция?
- count: сколько раз выполнялась каждая инструкция?
- atomic: как count, но с точным подсчётом в параллельных программах
По умолчанию используется режим «set», который мы уже видели.
Режим atomic нужен только тогда, когда требуется точный подсчёт при работе параллельных алгоритмов. Он использует атомарные операции из пакета sync/atomic,
что может быть довольно затратно.
Однако, для большинства целей подойдёт режим count, который работает так же эффективно, как и режим set по умолчанию.
Попробуем подсчитать количество выполнений инструкций в стандартном пакете, пакете форматирования fmt.
Запустим тест и сохраним профиль покрытия, чтобы позже можно было наглядно представить информацию.
<code>% go test -covermode=count -coverprofile=count.out fmt ok fmt 0.056s coverage: 91.7% of statements % </code>
Это намного лучший уровень покрытия тестами по сравнению с предыдущим примером. (Уровень покрытия не зависит от режима покрытия.) Можно отобразить разбивку по функциям:
<code>% go tool cover -func=count.out fmt/format.go: init 100.0% fmt/format.go: clearflags 100.0% fmt/format.go: init 100.0% fmt/format.go: computePadding 84.6% fmt/format.go: writePadding 100.0% fmt/format.go: pad 100.0% ... fmt/scan.go: advance 96.2% fmt/scan.go: doScanf 96.8% total: (statements) 91.7% </code>
Наибольшая выгода проявляется в HTML-выводе:
<code>% go tool cover -html=count.out </code>
Вот как выглядит функция pad в этом представлении:
Обратите внимание на интенсивность зелёного цвета. Более яркий зелёный означает более высокий уровень выполнения; менее насыщенные оттенки зелёного соответствуют более низкому уровню выполнения. Также можно навести курсор мыши на инструкции, чтобы увидеть точные значения всплывающей подсказки. На момент написания этих строк значения выглядят так (мы переместили значения из подсказок в маркеры в начале строк, чтобы их легче было показать):
<code>2933 if !f.widPresent || f.wid == 0 {
2985 f.buf.Write(b)
2985 return
2985 }
56 padding, left, right := f.computePadding(len(b))
56 if left > 0 {
37 f.writePadding(left, padding)
37 }
56 f.buf.Write(b)
56 if right > 0 {
13 f.writePadding(right, padding)
13 }
</code>
Это много информации о выполнении функции, информация, которая может быть полезна при профилировании.
Базовые блоки
Вы могли заметить, что значения в предыдущем примере не соответствовали ожиданиям на строках с закрывающими фигурными скобками. Это происходит потому, что, как и всегда, покрытие тестами — это не точная наука.
Однако, то, что здесь происходит, стоит объяснить. Мы хотим, чтобы аннотации покрытия были ограничены ветвлениями в программе, как это происходит при инструментировании бинарного файла традиционным способом.
Однако сделать это при перезаписи исходного кода довольно трудно, поскольку ветвления не отображаются явно в исходном коде.
Аннотации покрытия инструментируют блоки, которые обычно ограничены фигурными скобками. Правильно реализовать это в общем случае очень сложно.
Одним из следствий используемого алгоритма является то, что закрывающая фигурная скобка выглядит так, будто она принадлежит блоку, который она закрывает, в то время как открывающая скобка выглядит так, будто она принадлежит вне блока.
Более интересным следствием является то, что в выражении вида
<code>f() && g() </code>
не предпринимается попытка отдельно инструментировать вызовы f и g. Независимо от фактов, всегда будет казаться, что они оба выполнялись одинаковое количество раз — количество раз, когда выполнялась f.
Честно говоря, даже gcov испытывает трудности здесь. Этот инструмент правильно выполняет инструментирование, но представление информации основано на строках и поэтому может упустить некоторые нюансы.
Общая картина
Это история о покрытии тестами в Go 1.2. Новый инструмент с интересной реализацией позволяет не только собирать статистику покрытия тестами, но и легко интерпретируемое представление этой информации, а также возможность извлекать информацию профилирования.
Тестирование — важная часть разработки программного обеспечения, а покрытие тестами — простой способ добавить дисциплину в вашу стратегию тестирования. Пусть ваша работа будет тщательно протестирована и покрыта.
Следующая статья: Внутри Go Playground
Предыдущая статья: Выпущен Go 1.2
Индекс блога