Учимся писать модульные тесты - PullRequest
0 голосов
/ 05 июля 2018

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

У меня есть простая функция, которую я написал ниже, которая принимает текстовый шаблон и файл CSV в качестве входных данных и выполняет шаблон, используя значения CSV. Я «тестировал» код методом проб и ошибок, передавая файлы и печатая значения, но я хотел бы научиться писать для него надлежащие тесты. Я чувствую, что научиться тестировать свой собственный код поможет мне понять и учиться быстрее и лучше. Любая помощь приветствуется.

// generateCmds generates configuration commands from a text template using
// the values from a CSV file. Multiple commands in the text template must
// be delimited by a semicolon. The first row of the CSV file is assumed to
// be the header row and the header values are used for key access in the
// text template.
func generateCmds(cmdTmpl string, filename string) ([]string, error) {
    t, err := template.New("cmds").Parse(cmdTmpl)
    if err != nil {
        return nil, fmt.Errorf("parsing template: %v", err)
    }

    f, err := os.Open(filename)
    if err != nil {
        return nil, fmt.Errorf("reading file: %v", err)
    }
    defer f.Close()

    records, err := csv.NewReader(f).ReadAll()
    if err != nil {
        return nil, fmt.Errorf("reading records: %v", err)
    }
    if len(records) == 0 {
        return nil, errors.New("no records to process")
    }

    var (
        b    bytes.Buffer
        cmds []string
        keys = records[0]
        vals = make(map[string]string, len(keys))
    )

    for _, rec := range records[1:] {
        for k, v := range rec {
            vals[keys[k]] = v
        }
        if err := t.Execute(&b, vals); err != nil {
            return nil, fmt.Errorf("executing template: %v", err)
        }
        for _, s := range strings.Split(b.String(), ";") {
            if cmd := strings.TrimSpace(s); cmd != "" {
                cmds = append(cmds, cmd)
            }
        }
        b.Reset()
    }
    return cmds, nil
}

Редактировать: Спасибо за все предложения до сих пор! Мой вопрос был помечен как слишком широкий, поэтому у меня есть несколько конкретных вопросов относительно моего примера.

  1. Была бы полезна тестовая таблица в такой функции? И если да, то должна ли тестовая структура включать возвращаемый cmds фрагмент строки и значение err? Например:
type tmplTest struct {
    name     string   // test name
    tmpl     string   // the text template
    filename string   // CSV file with template values
    expected []string // expected configuration commands
    err      error    // expected error
}
  1. Как вы обрабатываете ошибки, которые предполагается возвращать для конкретных тестовых случаев? Например, os.Open() возвращает ошибку типа *PathError, если обнаружена ошибка. Как инициализировать *PathError, который эквивалентен тому, который возвращается os.Open()? Та же идея для template.Parse(), template.Execute() и т. Д.

Редактировать 2: Ниже приведена тестовая функция, с которой я столкнулся. Мои два вопроса из первого редактирования остаются в силе.

package cmd

import (
    "testing"
    "strings"
    "path/filepath"
)

type tmplTest struct {
    name     string   // test name
    tmpl     string   // text template to execute
    filename string   // CSV containing template text values
    cmds     []string // expected configuration commands
}

var tests = []tmplTest{
    {"empty_error", ``, "", nil},
    {"file_error", ``, "fake_file.csv", nil},
    {"file_empty_error", ``, "empty.csv", nil},
    {"file_fmt_error", ``, "fmt_err.csv", nil},
    {"template_fmt_error", `{{ }{{`, "test_values.csv", nil},
    {"template_key_error", `{{.InvalidKey}}`, "test_values.csv", nil},
}

func TestGenerateCmds(t *testing.T) {
    for _, tc := range tests {
        t.Run(tc.name, func(t *testing.T) {
            cmds, err := generateCmds(tc.tmpl, filepath.Join("testdata", tc.filename))
            if err != nil {
                // Unexpected error. Fail the test.
                if !strings.Contains(tc.name, "error") {
                    t.Fatal(err)
                }
                // TODO: Otherwise, check that the function failed at the expected point.
            }
            if tc.cmds == nil && cmds != nil {
                t.Errorf("expected no commands; got %d", len(cmds))
            }
            if len(cmds) != len(tc.cmds) {
                t.Errorf("expected %d commands; got %d", len(tc.cmds), len(cmds))
            }
            for i := range cmds {
                if cmds[i] != tc.cmds[i] {
                    t.Errorf("expected %q; got %q", tc.cmds[i], cmds[i])
                }
            }
        })
    }
}

1 Ответ

0 голосов
/ 05 июля 2018

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

Это не так сильно отличается от примеров, которые вы, вероятно, видели для более простых случаев.

Вы можете поместить файлы в папку testdata внутри одного пакета (testdata - это специальное имя, которое инструменты Go будут игнорировать во время сборки).

Тогда вы можете сделать что-то вроде:

func TestCSVProcessing(t *testing.T) {
    templateStr := `<your template here>`
    testFile := "testdata/yourtestfile.csv"
    result, err := generateCmds(templateStr, testFile)
    if err != nil {
        // fail the test here, unless you expected an error with this file
    }
    // compare the "result" contents with what you expected
    // failing the test if it does not match
}

EDIT

О конкретных вопросах, которые вы добавили позже:

Была бы полезна тестовая таблица в такой функции? И, если это так, должна ли тестовая структура включать возвращаемый фрагмент строки cmds и значение err?

Да, имеет смысл включить как ожидаемые строки, которые будут возвращены, так и ожидаемую ошибку (если есть).

Как вы обрабатываете ошибки, которые должны возвращаться для конкретных тестовых случаев? Например, os.Open () возвращает ошибку типа * PathError, если обнаружена ошибка. Как мне инициализировать * PathError, эквивалентную той, которую возвращает os.Open ()?

Не думаю, что вы сможете "инициализировать" эквивалентную ошибку для каждого случая. Иногда библиотеки могут использовать внутренние типы для своих ошибок, что делает это невозможным. Проще всего было бы «инициализировать» обычную ошибку с тем же значением, которое было возвращено в методе Error(), а затем просто сравнить значение Error() возвращенной ошибки с ожидаемым.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...