Golang测试指南与最佳实践

Golang测试指南与最佳实践 大家好,

我刚刚开始一个Go项目,对于如何更好地测试某些场景有些疑问。

代码片段:

func (e *env) processFile() {
	scanner := bufio.NewScanner(e.fd)
	for scanner.Scan() {
		line := scanner.Text()
		count := processLine(line)
		e.loaded += count
	}

	if err := scanner.Err(); err != nil {
		log.Printf("Error line processing: %s\n", err)
	}
}

我为我的包实现了测试,查看覆盖率报告时发现如下情况:

Screen Shot 2021-08-05 at 16.31.52

处理此类场景的最佳方法是什么?是否有其他策略可以达到这个覆盖率,或者我不应该为此担心?

提前感谢。


更多关于Golang测试指南与最佳实践的实战教程也可以访问 https://www.itying.com/category-94-b0.html

5 回复

感谢分享!你是对的!

更多关于Golang测试指南与最佳实践的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


你好,

虽然还不确定这是否是最佳方法,但我通过稍微调整我的方法并使用 Testify 中的模拟对象,成功在这些场景中实现了 100% 的覆盖率。

基本上,我通过使用接口创建了一个新的抽象层,这使我能够控制在所需场景下的返回值。

再次感谢 @skillian 对此问题的见解。

此致,

你好,skillian,很高兴收到你的回复!

我想我明白你的意思了。我的方法确实不是最好的。

由于我将 pattern 定义为常量,这使我无法测试输入模式不一致的情况。改变方法,使用 regexp.MustCompile,我就可以得到一个一致且可测试的场景。

归根结底,如果我没有一个清晰的方法来处理此类情况(例如,不通过修改源代码来匹配错误情况),那么很可能存在一种更清晰的方法。

如果可以的话,我还想讨论另一个场景(对我来说,这两个例子都归结为同一个方面):

处理一行:

func processLine(line string) int {
count := 0
matched := pattern.MatchString(line)

if matched {
	log.Printf("Matched: %s", line)
	// Strip eventuals " chars

	if err := setEnv(strings.ReplaceAll(line, "\"", "")); err != nil {
		log.Fatalf("Error processing line %s: %s", line, err)
	}
	count++
}

if !matched {
	log.Printf("Not matched: %s", line)
}

return count
}

处理一个文件:

func (e *env) processFile() {
scanner := bufio.NewScanner(e.fd)
for scanner.Scan() {
	line := scanner.Text()
	count := processLine(line)
	e.loaded += count
}
if err := scanner.Err(); err != nil {
	log.Printf("Error line processing: %s\n", err)
}
}

在文档中是否有地方可以查询到哪些输入数据会在 scanner.ErrSetEnv 处引发错误(我几乎可以肯定这些信息不应该在文档中,但没找到简单的方法来获取“错误”数据)?

到目前为止,我理解这是测试此类场景错误处理情况的唯一方法:

Screen Shot 2021-08-09 at 13.04.43

或者,有没有办法为函数创建存根行为并强制其返回错误?

非常感谢这次精彩的讨论。

此致,

Rafael

你好,Rafael,欢迎来到论坛!

测试不仅应该检查正常输入的行为,还应该检查错误输入。在你的测试用例中包含会产生错误的测试,以确保你的错误处理中没有bug。

pattern 是一个 const 正则表达式模式字符串吗?如果是这种情况,我建议将 pattern 从类似这样的形式:

const pattern = `my regexp pattern`

改为类似这样的形式:

var pattern = regexp.MustCompile(`my regexp pattern`)

regexp.Match 是一个便捷函数,它会对你的正则表达式调用 regexp.Compile,然后对结果调用 (*regexp.Regexp).Match。这意味着每次执行时,你的正则表达式都会被(重新)编译,所以如果你期望正则表达式被多次使用,最好通过显式调用 regexp.Compile(或者 regexp.MustCompile,它会在出错时panic)来只编译一次,然后在你编译好的 *regexp.Regexp 上调用 Match 成员函数。

如果 pattern 是一个 const 表达式字符串,我建议使用 regexp.MustCompile,因为根据你的测试,这个表达式似乎是正确的,所以 err 将始终为 nil。(*regexp.Regexp).Match 只返回一个 bool,这样可以消除错误检查,并使你达到此函数的100%覆盖率。

总结

const pattern = `myregex`

func processLine(line string) int {
    count := 0
    matched, err := regexp.Match(pattern, []byte(line))

    if err != nil {
        log.Printf("Error processing line %s: %s", line, err)
    }

    if matched {
        log.Printf("Matched: %s", line)
        err = setEnv(line)

        if err == nil {
            count++
        }
    }

    if !matched {
        log.Printf("Not matched: %s", line)
    }

    return count
}

改为:

var pattern = regexp.MustCompile(`myregex`)

func processLine(line string) int {
    count := 0
    matched := pattern.MatchString(line)

    if matched {
        log.Printf("Matched: %s", line)
        err = setEnv(line)

        if err == nil {
            count++
        }
    }

    if !matched {
        log.Printf("Not matched: %s", line)
    }

    return count
}

对于测试 processFile 方法中 scanner.Err() 的错误分支,可以通过模拟文件读取错误来覆盖。以下是具体的测试方案:

1. 使用接口解耦依赖 首先重构代码,将文件依赖抽象为接口:

type fileReader interface {
    Read(p []byte) (n int, err error)
}

type env struct {
    fd fileReader
    loaded int
}

func (e *env) processFile() {
    scanner := bufio.NewScanner(e.fd)
    for scanner.Scan() {
        line := scanner.Text()
        count := processLine(line)
        e.loaded += count
    }

    if err := scanner.Err(); err != nil {
        log.Printf("Error line processing: %s\n", err)
    }
}

2. 实现模拟错误读取器 创建可注入错误的测试实现:

type errorReader struct {
    err error
}

func (r *errorReader) Read(p []byte) (n int, err error) {
    return 0, r.err
}

3. 编写测试用例 在测试文件中覆盖错误场景:

func TestProcessFile_ScannerError(t *testing.T) {
    // 创建模拟错误
    expectedErr := errors.New("read error")
    
    // 使用错误读取器
    e := &env{
        fd: &errorReader{err: expectedErr},
    }
    
    // 捕获日志输出
    var logOutput bytes.Buffer
    log.SetOutput(&logOutput)
    defer log.SetOutput(os.Stderr)
    
    // 执行测试
    e.processFile()
    
    // 验证错误被记录
    if !strings.Contains(logOutput.String(), expectedErr.Error()) {
        t.Errorf("Expected error log containing %q, got %q", 
            expectedErr.Error(), logOutput.String())
    }
}

4. 使用测试辅助库(可选) 对于更复杂的场景,可以使用 github.com/stretchr/testify

func TestProcessFile_ScannerError_WithTestify(t *testing.T) {
    errReader := &errorReader{err: errors.New("io failure")}
    e := &env{fd: errReader}
    
    // 使用assert验证
    assert.NotPanics(t, func() {
        e.processFile()
    }, "processFile should handle scanner errors gracefully")
}

覆盖率说明:

  • 这种方法可以确保 scanner.Err() 错误分支被覆盖
  • 测试验证了错误处理逻辑而非具体实现细节
  • 通过接口注入使测试更可控

对于日志输出的验证,确保在测试完成后恢复默认日志输出,避免影响其他测试。这种模式在测试错误处理路径时很常见,特别是当函数依赖外部I/O操作时。

回到顶部