学习编写单元测试

时间:2018-07-04 22:19:50

标签: unit-testing testing go

我正在尝试学习如何为代码编写测试以编写更好的代码,但似乎似乎最困难的是弄清楚如何实际测试我编写的某些代码。我已经阅读了很多教程,其中大多数教程似乎只涉及添加两个数字或模拟某些数据库或服务器的函数。

我在下面编写了一个简单的函数,该函数将文本模板和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 :(得分:6)

您基本上需要拥有一些示例文件,其中包含要测试的内容,然后可以在测试代码中调用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
}

编辑

关于您稍后添加的特定问题:

  

测试表在这样的函数中有用吗?而且,如果是这样,测试结构是否需要包括返回的cmds字符串切片和err的值?

是的,同时包含要返回的预期字符串和预期的错误(如果有)都是有意义的。

  

您如何处理应针对特定测试用例返回的错误?例如,如果遇到错误,则os.Open()返回* PathError类型的错误。如何初始化* PathError,它等于os.Open()返回的错误?

我认为您无法针对每种情况“初始化”等效错误。有时,库可能使用内部类型来处理错误,从而使这不可能。最简单的方法是使用其Error()方法中返回的相同值来“初始化”一个常规错误,然后将返回的错误的Error()值与期望的值进行比较。