Тестовые соглашения об именах в Голанге

Я пытаюсь выполнить unit test пакет Go в первый раз, и у меня есть пара ошибок в одном файле.

type FooErr int
type BarErr int

func (e *FooErr) Error () string {
    return "A Foo Error has occurred"
}

func (e *BarErr) Error () string {
    return "A Bar Error has occurred"
}

Однако все соглашения об именах выглядят как func TestXxx(*testing.T) (из документации пакета testing). Это означало бы, что мой файл тестирования будет выглядеть так:

func TestError (t *testing.T) { ... } // FooErr
func TestError (t *testing.T) { ... } // BarErr

Это, очевидно, две функции одной и той же сигнатуры. Каков рекомендуемый метод для этого?

Ответ 1

Здесь есть несколько вещей:

Ошибки

Значения экспортированных ошибок уровня пакета обычно называются Err, за которым следует что-то, например ErrTimeout здесь. Это делается для того, чтобы клиенты вашего пакета могли делать что-то вроде

if err := yourpkg.Function(); err == yourpkg.ErrTimeout {
  // timeout
} else if err != nil {
  // some other error
}

Чтобы облегчить это, они часто создаются либо с помощью errors.New:

// Error constants
var (
  ErrTimeout = errors.New("yourpkg: connect timeout")
  ErrInvalid = errors.New("yourpkg: invalid configuration")
)

или с пользовательским, невыполненным типом:

type yourpkgError int

// Error constants
var (
  ErrTimeout yourpkgError = iota
  ErrSyntax
  ErrConfig
  ErrInvalid
)

var errText = map[yourpkgError]string{
  ErrTimeout: "yourpkg: connect timed out",
  ...
}

func (e yourpkgError) Error() string { return errText[e] }

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

В случае, если вам нужны дополнительные данные внутри ошибки, имя типа заканчивается на Error:

type SyntaxError struct {
  File           string
  Line, Position int
  Description    string
}

func (e *SyntaxError) Error() string {
  return fmt.Sprintf("%s:%d:%d: %s", e.File, e.Line, e.Position, e.Description)
}

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

tree, err := yourpkg.Parse(file)
if serr, ok := err.(*SyntaxError); ok {
  // syntax error
} else if err != nil {
  // other error
}

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

Испытания

Тесты часто называются после единицы, которую они тестируют. Во многих случаях вы не будете тестировать условия ошибки отдельно, поэтому TestError не является именем, которое должно появляться очень часто. Название самого теста просто должно быть уникальным, однако оно не ограничено ничем в тестируемом коде тем же способом, что и в примерах. Когда вы тестируете несколько условий кода, часто лучше всего сформулировать тест как Table Driven Test. На этой странице wiki есть несколько хороших примеров, но чтобы продемонстрировать проверку ошибок, вы можете сделать это:

func TestParse(t *testing.T) {
  tests := []struct{
    contents string
    err      error
  }{
    {"1st", nil},
    {"2nd", nil},
    {"third", nil},
    {"blah", ErrBadOrdinal},
    {"", ErrUnexpectedEOF},
  }
  for _, test := range tests {
    file := strings.NewReader(test.contents)
    if err := Parse(file); err != test.err {
      t.Errorf("Parse(%q) error %q, want error %q", test.contents, err, test.err)
    }
    // other stuff
  }
}

Если вам нужна специальная тестовая функция для устройства, которое делает что-то странное и не соответствует основному тесту, вы обычно называете его чем-то описательным, например TestParseTimeout, который включает как устройство, так и поведение, повторное тестирование.

Ответ 2

Я бы выполнил соглашение, например, функции, описанные в разделе overview тестового пакета:

"Соглашение об именах для объявления примеров для функции F, тип T и метод M на типе T:"

func ExampleF() { ... }
func ExampleT() { ... }
func ExampleT_M() { ... }

Для godoc требуется соглашение об именах, например, для godoc, но я бы выполнил одно и то же соглашение для тестов TestT_M для согласованности.

Ответ 3

Вам не нужно, чтобы часть Xxx TestXxx соответствовала фактическому имени функции. Согласование префикса тестов с Test достаточно для команды go test, чтобы забрать их.

Как говорит Алекс Локвуд в своем комментарии, вы можете использовать TestFooError и TestBarError, если хотите.

Ответ 4

Go 1.4 (Q4 2014) добавит еще одно соглашение об именах для методов тестирования:

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

func TestMain(m *testing.M) 

эта функция будет вызываться вместо непосредственного запуска тестов.
M struct содержит методы доступа и запуска тестов.