эта статьяНачалось вмой блог, если вы найдете это полезным, ставьте лайк и собирайте.
Будь то проект с открытым исходным кодом или разработка ежедневных программ, тестирование является важным звеном. Сегодня мы начинаем знакомство с тестированием тестового модуля Go.
Я не обновлял статью о Go почти две недели, и в последнее время статус был не очень хорошим. Эта статья изначально задумывалась как очень содержательная, но она оказалась немного не впечатляет, и несколько подразделов были удалены. Подумав, я решил разделить его на несколько частей.
Кроме того, в справочных материалах есть несколько замечательных статей, и вы можете их прочитать, если вам интересно.
Простой обзор
Когда мы выбираем проекты с открытым исходным кодом, мы обычно обращаем больше внимания на то, хорошо ли написаны тест-кейсы этого проекта.Тесты отличного проекта, как правило, написаны неплохо. Чтобы в будущем самостоятельно написать хороший проект, вам все равно придется усердно учиться над тестом.
Обычно контактные тесты — это в основном модульные тесты и тесты производительности. Неудивительно, что тестирование go также поддерживает оба типа тестов. Модульные тесты используются для тестирования модулей, а производительность определяется тестами, которые являются тестами.
В дополнение к функциям, упомянутым выше, тестовый модуль Go также поддерживает написание кейсов, а в сочетании с godoc документацию библиотеки можно создать очень быстро.
Самый простой способ
Когда дело доходит до того, как проверить функцию функции, самый простой способ придумать для разработки — это напрямую вызвать функцию в main, чтобы оценить результат.
Например, чтобы протестировать функцию абсолютного значения Abs с помощью математического метода, пример кода выглядит следующим образом:
package main
import (
"fmt"
"math"
)
func main() {
v := math.Abs(-10)
if v != 10 {
fmt.Println("测试失败")
return
}
fmt.Println("测试成功")
}
Более распространенная возможность заключается в том, что нет решения if, и результат выводится непосредственно, и мы наблюдаем за результатом, чтобы подтвердить проблему. Специально для разработчиков, которые привыкли использовать языки сценариев Python и PHP, очень быстро построить скрипт-тест, потому что я давно так делаю.
Каковы недостатки этого подхода? В моем понимании, основные моменты в том, что тесты в main не так просто переиспользовать, и они часто удаляются при сборке, когда тесткейсов становится больше, гибкости не хватает, и часто возникает необходимость модификации код, автоматизированное тестирование не очень удобно и т.д.
Я мало что знаю о тестировании, это всего лишь часть моего опыта.
Если вы столкнулись с проблемами, вы должны их решить Давайте официально начнем внедрение юнит-тестирования в го-тестирование.
Кейс быстрого опыта
Модульный тест используется для проверки функционального модуля при заданных входных условиях в указанном сценарии, чтобы определить, соответствуют ли выходные результаты ожидаемым.
Давайте посмотрим непосредственно на пример, который прост и интуитивно понятен. Протестируйте функцию абсолютного значения Abs по математике. Сначала создайте тестовый файл math_test.go в каталоге со следующим кодом:
package math
import (
"math"
"testing"
)
func TestAbs(t *testing.T) {
var a, expect float64 = -10, 10
actual := math.Abs(a)
if actual != expect {
t.Fatalf("a = %f, actual = %f, expected = %f", a, actual, expect)
}
}
Программа очень проста, а — входной параметр функции Abs, ожидаемый — ожидаемый результат выполнения, фактический — фактический результат выполнения функции, а результат проверки определяется сравнением фактического и ожидаемого.
После написания тестового примера выполните тест с помощью команды go test, и мы увидим следующий вывод.
$ go test
PASS
ok study/test/math 0.004s
Вывод PASS означает, что тестовый пример был успешно выполнен. 0,004 с представляет время выполнения варианта использования.
Научитесь использовать go-тестирование
Как видно из предыдущих примеров, писать тесты на Go очень удобно. Что касается его использования, есть два основных момента: один — это правила написания тестового кода, а другой — использование API.
Правила написания тестов
Go-тесты должны быть написаны штатным образом, иначе go test не сможет правильно определить местоположение тестового кода.Есть три основных правила.
Во-первых, имя файла тестового кода должно заканчиваться на _test.go Например, имя файла math_tesh.go в предыдущем разделе выбрано не случайно.
Кроме того, функции варианта использования в коде должны соответствовать TestXxx, например TestAbs.
Насчет Xxx объясните пожалуйста кратко.В основном это передает два значения.Первое что Xxx означает, что первый символ должен быть заглавным или числом.Короче говоря, это может определять разделение слов.Второе то что символы после первого буква может быть любым допустимым символом ключевого слова Go. , например прописными и строчными буквами, символами подчеркивания, цифрами.
В-третьих, что касается определения типа функции варианта использования, определение выглядит следующим образом.
func TestXxx(*testing.T)
Тестовые функции должны быть написаны в этом фиксированном формате, иначе go test выполнится и сообщит об ошибке. В функции есть входной параметр t, тип *testing.T, он очень важен, модульному тесту нужно через него вернуть результат теста, который будет введен позже.
Использование гибкого API памяти
Только написание тест-кейсов по правилам может гарантировать правильное позиционирование и выполнение go-теста. Но для того, чтобы проанализировать результаты тестирования, нам также необходимо взаимодействовать с тестовой средой, что требует участия входного параметра t тестовой функции.
В TestAbs мы использовали t.Fatalf, его роль заключается в обратной связи с результатами теста. Предполагая, что такого кода нет, ошибка также сообщит об успешном тесте, что явно не то, что нам нужно.
Мы можем просмотреть экспортируемые методы, поддерживаемые в testing.T, в официальной документации следующим образом:
// 获取测试名称
method (*T) Name() string
// 打印日志
method (*T) Log(args ...interface{})
// 打印日志,支持 Printf 格式化打印
method (*T) Logf(format string, args ...interface{})
// 反馈测试失败,但不退出测试,继续执行
method (*T) Fail()
// 反馈测试成功,立刻退出测试
method (*T) FailNow()
// 反馈测试失败,打印错误
method (*T) Error(args ...interface{})
// 反馈测试失败,打印错误,支持 Printf 的格式化规则
method (*T) Errorf(format string, args ...interface{})
// 检测是否已经发生过错误
method (*T) Failed() bool
// 相当于 Error + FailNow,表示这是非常严重的错误,打印信息结束需立刻退出。
method (*T) Fatal(args ...interface{})
// 相当于 Errorf + FailNow,与 Fatal 类似,区别在于支持 Printf 格式化打印信息;
method (*T) Fatalf(format string, args ...interface{})
// 跳出测试,从调用 SkipNow 退出,如果之前有错误依然提示测试报错
method (*T) SkipNow()
// 相当于 Log 和 SkipNow 的组合
method (*T) Skip(args ...interface{})
// 与Skip,相当于 Logf 和 SkipNow 的组合,区别在于支持 Printf 格式化打印
method (*T) Skipf(format string, args ...interface{})
// 用于标记调用函数为 helper 函数,打印文件信息或日志,不会追溯该函数。
method (*T) Helper()
// 标记测试函数可并行执行,这个并行执行仅仅指的是与其他测试函数并行,相同测试不会并行。
method (*T) Parallel()
// 可用于执行子测试
method (*T) Run(name string, f func(t *T)) bool
Выше перечислены все общедоступные методы модульного тестирования. Т. По моему личному мнению, они примерно разделены на три категории, а именно низкоуровневые методы, тестовые отзывы и некоторые другие вспомогательные методы для управления выполнением.
Существует только один API для базовой информации, метод Name(), который используется для получения имени теста. Вспомогательные методы управления запуском в основном относятся к Helper, t.Parallel и Run, которые были кратко представлены в комментариях выше.
Здесь мы сосредоточимся на API для тестирования обратной связи, ведь он наиболее часто используется. Метод Fatalf, использованный ранее, является одним из них, и его результатом является печать журнала ошибок и немедленный выход из теста. Хотите сократить этот вид API? Мы можем помнить на нескольких уровнях.
Во-первых, мы помним некоторые связанные базовые методы, которые являются основными компонентами других методов, а именно:
- Печать журнала, Log и Logf, и разницу между Log и Logf можно сравнить с Println и Printf, то есть Logf поддерживает печать в формате Printf, а Log нет.
- Fail mark, Fail и FailNow, Fail и FailNow — все это методы, используемые для обозначения отказа теста.Разница между ними заключается в том, что отметка Fail продолжит выполнение следующего теста после отказа, а FailNow завершит работу сразу после отметки отказа.
- Тест игнорируется, метод SkipNow выходит из теста, но не отмечает провал теста, что можно сравнить с FailNow в памяти.
Давайте посмотрим на оставшиеся методы, которые в основном состоят из основных методов. Мы можем выбрать разные комбинации в соответствии со сценой. Например:
- Обычный журнал, просто распечатайте несколько журналов, вы можете напрямую использовать Log или Logf;
- Обычные ошибки, если вы не выходите из теста, просто печатайте сообщения об ошибках, используйте Error или Errorf, эти два метода представляют собой комбинацию log или logf и Fail;
- Серьезная ошибка, необходимо выйти из теста и распечатать некоторые сообщения об ошибках, используйте Fatal (log + FailNow) или Fatalf (logf + FailNow);
- Игнорировать ошибку и выйти из теста можно с помощью Skip (log + SkipNow) и Skipf (logf + SkipNow);
Если печать информации о формате Printf поддерживается, после метода будет символ f. Итак, подытоживая, мы обнаружили, что память методов в testing.T очень проста.
Внезапно я подумал, не спросит ли кто-нибудь, при каких обстоятельствах тест считается успешным. На самом деле тест считается успешным, если он не помечен как неудачный.
практиковать дело
После стольких базовых знаний у меня пересохло во рту. Теперь начните пробовать!
В качестве простого примера проверьте функцию деления. Сначала создайте файл math.go. Код функции выглядит следующим образом:
package math
import "errors"
func Division(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
Деление очень простое, входные параметры a и b — это делимое и делитель соответственно, а выходные параметры — результат вычисления и сообщение об ошибке. Если делитель равен 0, будет выдано соответствующее сообщение об ошибке.
Перед формальным тестированием функции Division нам нужно разобраться, какие входные данные и ожидаемые результаты указывают на успешность теста. Ожидаемый результат отличается в зависимости от ввода, который может быть правильным результатом или ожидаемым неправильным результатом. В чем смысл? Взяв в качестве примера Дивизион, необходимо рассмотреть два сценария:
- Обычный вызов возвращает результат, например, когда делимое равно 10, делитель равен 5, а ожидаемый результат равен 2, то есть ожидается правильный результат;
- Ожидайте, что ошибка вернет результат, когда делимое равно 10, а делитель равен 0, ожидается, что будет возвращена ошибка, что делитель не может быть равен 0, то есть ожидается, что будет возвращено сообщение об ошибке;
Если это разработка через тестирование, прежде чем мы официально напишем код реализации, нам нужно сначала определить их и написать тестовый код.
После анализа варианта использования можно приступать к написанию кода.
Первый — это обычный тест вызова, который выглядит следующим образом:
func TestDivision(t *testing.T) {
var a, b, expect float64 = 10, 5, 2
actual, err := Division(a, b)
if err != nil {
t.Errorf("a = %f, b = %f, expect = %f, err %v", a, b, expect, err)
return
}
if actual != expect {
t.Errorf("a = %f, b = %f, expect = %f, actual = %f", a, b, expect, actual)
}
}
Определены три переменные, а именно a, b и ожидаемый результат, соответствующие делимому, делителю и ожидаемому результату. Вариант использования подтверждает успешность теста путем сравнения фактического результата подразделения с ожидаемым результатом. Кроме того, ошибка, возвращаемая подразделением, также должна быть проверена, потому что здесь ожидается нормальный результат работы, пока есть ошибка, тест может быть признан неудачным.
Посмотрите на ожидаемые результаты ошибок, как показано ниже:
func TestDivisionZero(t *testing.T) {
var a, b float64 = 10, 0
var expectedErrString = "division by zero"
_, err := Division(a, b)
if err.Error() != expectedErrString {
t.Errorf("a = %f, b = %f, err %v, expect err %s", a, b, err, expectedErrString)
return
}
}
Кроме того, сначала определяются три переменные: a, b и expectErrString. Значения a и b такие же, как и раньше, а expectErrString — это ожидаемое сообщение об ошибке. Делитель b устанавливается равным 0 главным образом для того, чтобы проверить, возвращает ли функция деления ожидаемую ошибку, поэтому нас не волнует результат вычисления. Успех теста определяется путем сравнения фактической ошибки возврата с expectErrString.
Выполните тест с помощью go test следующим образом:
$ go test -v
=== RUN TestDivision
--- PASS: TestDivision (0.00s)
=== RUN TestDivisionZero
--- PASS: TestDivisionZero (0.00s)
PASS
ok study/test/math 0.005s
Результаты показывают, что испытание прошло успешно!
В демонстрации этого случая мы добавляем опцию -v для перехода к тесту, чтобы мы могли четко видеть выполнение каждого тестового примера.
Простой и компактный настольный тест
В приведенном выше примере я не знаю, обнаружена ли проблема?
Если есть много вариантов использования тестируемой функции, нам нужно будет написать много тестовых функций с очень высоким повторением кода, потому что модульное тестирование в основном вращается вокруг простого шаблона:
Укажите входные параметры -> вызвать функцию для проверки -> получить результат возврата -> Сравните фактическую доходность с ожидаемым результатом -> Подтвердите сообщение об ошибке теста
Исходя из этого, Go выступает за то, чтобы мы использовали метод тестирования под названием «Table Driven», что в китайском переводе можно назвать тестированием группы таблиц. Это позволяет нам писать тесты коротким и компактным способом. Как это сделать?
Во-первых, нам нужно определить структуру для теста группы таблиц, которая содержит требуемые входные данные и ожидаемые выходные данные теста. Взяв в качестве примера функциональный тест Division, можно определить следующую структуру:
type DivisionTable struct {
a float64 // 被除数
b float64 // 除数
expect float64 // 期待计算值
expectErr error // 期待错误字符串
}
Значение каждого поля было объяснено в разделе комментариев, что похоже на поле, используемое в тесте с одним сценарием, который мы проводили ранее. Разница в том, что expectErr больше не имеет строкового типа.
Затем варианты использования, которые нам нужно протестировать ниже, представлены литералом структуры.
var table = []DivisionTable{
{1., 1., 1., nil},
{-4., -2., 2., nil},
{2., 0., 7., errors.New("division by zero")},
}
Кратко перечислены три сценария, а именно: деление на положительные числа, деление на отрицательные числа и деление, когда делитель равен 0.
Следующая цель — реализовать общую функцию тестирования отдела. Вы только посмотрите на код!
func TestDivisionTable(t *testing.T) {
for _, v := range divisionTable {
actual, err := Division(v.a, v.b)
if err == nil {
if v.expectErr != nil {
t.Errorf(
"a = %f, b = %f, actual err not nil, expect err is nil", v.a, v.b)
}
} else if err != nil {
if v.expectErr == nil {
t.Errorf(
"a = %f, b = %f, actual err not nil, expect err is nil", v.a, v.b)
} else if !strings.Contains(err.Error(), v.expectErr.Error()) {
t.Errorf(
"a = %f, b = %f, actual err = %v, expect err = %v", v.a, v.b, err, v.expectErr)
}
} else if actual != v.expect {
t.Errorf(
"a = %f, b = %f, actual = %f, expect = %f", v.a, v.b, actual, v.expect)
}
}
}
Код выглядит беспорядочно, главным образом потому, что фактический тип внутри интерфейса ошибки — это указатель, а оператор сравнения нельзя использовать для прямого сравнения ошибок, поэтому необходимо выполнить некоторую обработку. Этот пример намного легче понять, если нет ложных сравнений.
Разобраться можно, логика на самом деле очень простая. В основном он состоит из нескольких шагов:
- Сначала пройдите по таблице DivisionTable, чтобы получить входные параметры и ожидаемые результаты;
- Вызвать функцию function с входными параметрами, полученными из DivisionTable;
- Получить результат выполнения функции function, включая результат расчета и возможные ошибки;
- Сравнивая возврат err и expectErr, нам нужно иметь дело с двумя случаями: err == nil и != nil;
- Фактический err равен нулю, expectErr также должен быть равен нулю;
- Фактическая ошибка не равна нулю, expectErr не может быть равен нулю, а сообщение об ошибке включено в err.
- Последним шагом является сравнение фактических результатов расчета с ожидаемыми результатами;
Если возникает ошибка, мы используем t.Errorf, чтобы распечатать журнал ошибок и сообщить, что тест не пройден. Причина использования Errorf заключается в том, что мы не можем просто прервать весь тест, если один вариант использования терпит неудачу. Конечно, это зависит от ситуации, жестких правил нет.
На данный момент основная часть почти такая же.
подробный вывод журнала
Для некоторых друзей, которые плохо знакомы с тестированием Go, они могли столкнуться с этой странной проблемой, которая будет обсуждаться далее.
Пример является наиболее интуитивным, а именно:
func TestDivision(t *testing.T) {
var a, b, expect float64 = 10, 5, 2
actual, err := Division(a, b)
if err != nil {
t.Errorf("a = %f, b = %f, expect = %f, err %v", a, b, expect, err)
return
}
if actual != expect {
t.Errorf("a = %f, b = %f, expect = %f, actual = %f", a, b, expect, actual)
}
t.Log("end")
}
В отличие от предыдущего примера, в конце добавляется печать журнала t.Log("end"). Эффект выполнения go test без каких-либо параметров выглядит следующим образом:
$ go test
PASS
ok study/test/math 0.004s
Я не вижу, чтобы эта строка конечного журнала добавлялась в выходной журнал.
В предыдущей демонстрации мы использовали опцию -v команды go test, которая позволяет нам просматривать очень подробную информацию о выводе. Мы добавляем параметр -v и выполняем его снова, чтобы увидеть эффект:
$ go test -v
=== RUN TestDivision
--- PASS: TestDivision (0.00s)
math_test.go:36: end
PASS
ok study/test/math 0.005s
Там гораздо больше информации, и эта строка журналов завершения печатается, указывая местоположение кода math_test.go:36: end. Кроме того, это также относится к выполнению каждого теста, например, к началу выполнения теста и результатам теста.
Гибкий контроль над тем, какие тесты выполняются
Допустим, мы поместили все тестовые функции, использовавшиеся в предыдущей демонстрации, в math_test.go. На этом этапе использование теста go test по умолчанию столкнется с проблемой, то есть тестовая функция в пакете выполняется один раз каждый раз. Есть ли способ гибко управлять им?
Сначала вы можете взглянуть на такие проблемы, и каковы общие сценарии использования! Несколько вещей, которые приходят на мой взгляд:
- Выполнение всех тестовых функций в пакете, go test по умолчанию, само собой разумеется;
- Выполнить одну из тестовых функций, например, когда мы поместили все написанные ранее тестовые функции в файл math_test.go, как выбрать одну из них для выполнения;
- Выполнение тестовых функций в соответствии с определенным типом правил сопоставления, например, выполнение тестовых функций, имена которых начинаются с Division;
- Выполнить все тестовые функции в рамках проекта, в проекте обычно более одного пакета, как выполнить все тестовые функции всех пакетов, как это сделать;
Первый не нуждается в особом представлении. Но есть еще один момент, который нужно ввести, то есть, в дополнение к выполнению по умолчанию текущего пакета пути, мы также можем указать, какую функцию проверки пакета выполнять.Метод обозначения поддерживает метод чистого пути к файлу и метод пути к пакету.
Предполагая, что импортной путь нашего пакета является примером / математикой, и наше текущее местоположение находится в примерном каталоге, есть два способа выполнить тесты в соответствии с математикой.
$ go test # 目录路径执行
$ go test example/math # GOPATH 包导入路径
Второй и третий сценарии выполняют тест или их класс, в основном параметр -run для прохождения теста, параметр -run для получения параметра является регулярным выражением.
Выполнение определенной функции, такой как TestDivision, эффект выполнения команды выглядит следующим образом:
$ go test -run "^TestDivision$" -v
=== RUN TestDivision
--- PASS: TestDivision (0.00s)
math_test.go:36: end
PASS
ok study/test/math 0.004s
Как видно из вывода, действительно выполняется только TestDivision. Не забудьте добавить здесь параметр -v, чтобы сделать вывод специфичным для теста.
Выполнение функции определенного класса, например тестового подразделения, связанного с подразделением, приводит к следующему эффекту выполнения команды:
$ go test -run "Division" -v
=== RUN TestDivision
--- PASS: TestDivision (0.00s)
math_test.go:36: end
=== RUN TestDivisionZero
--- PASS: TestDivisionZero (0.00s)
=== RUN TestDivisionTable
--- PASS: TestDivisionTable (0.00s)
PASS
ok _/Users/polo/Public/Work/go/src/study/test/math 0.005s
Выполнить все функции, которые содержат Division в имени функции, написанном ранее.
В четвертом сценарии выполняются тесты по всему проекту. В каталоге верхнего уровня проекта просто выполните go test ./... напрямую, и мы не будем это подробно демонстрировать.
Суммировать
В этой статье описаны некоторые основные методы модуля тестирования Go. Из наиболее вероятных, чтобы продумать основные методы тестирования, чтобы использовать их при написании модульных тестов, перейдите к тестированию, затем представили тестовый пример, в котором рекомендовали внедрение Go «Table Driven» методов тестирования. Наконец, в статье также описываются некоторые виды работ, которые у нас обычно имеют более значимые навыки.
Ссылаться на
Unit Testing make easy in Go
gotests
перейти к тестовому модульному тесту
тестирование - модульные тесты
Writing table driven tests in Go
How to write benchmarks in Go
Как писать тестовые случаи на Go
Приложение Описание
В дополнение к написанию технических блогов, я часто отвечаю на технические вопросы на различных процессах и платформах и агрегата, и я составляю эти содержимое в мою публичный счет WeChat.
Добро пожаловать в код сканирования для беспокойства.