使用 go 语言框架实现测试驱动开发【Learn Go With Tests 简单介绍测试驱动开发的基本流程】 - Go语言中文社区

使用 go 语言框架实现测试驱动开发【Learn Go With Tests 简单介绍测试驱动开发的基本流程】


Learn Go With Tests

本文介绍在 go 语言的基本测试框架下,如何进行测试驱动开发的实现。主要介绍了测试驱动开发的基本概念和测试驱动开发的基本流程。

Table of Contents

1. 测试驱动开发的简单介绍

  • 测试驱动开发:测试驱动开发一种软件开发过程,是敏捷开发中的一项核心技术,也是一种设计方法论。

    TDD的原理是在开发功能代码之前,先编写单元测试用例代码,测试代码确定需要编写什么产品代码,将需求转换为非常具体的测试用例,然后对代码进行改进以使测试通过。

  • 重构:代码重构是重组现有过程的程序代码,而不改变其外部行为。

    重构旨在改善软件非功能属性的设计,结构或实现,同时保留其功能。重构的潜在优势可能包括提高代码可读性和降低复杂性 ; 这些可以改善源代码可维护性并创建更简单,更简洁或更富表现力的内部架构或对象模型,以提高可扩展性。

  • 测试:描述一种用来促进鉴定软件的正确性、完整性、安全性和质量的过程,即一种实际输出与预期输出之间的审核或者比较过程。

    通常测试会评估系统:

    • 能否满足开发的设计和模式
    • 能否对各种输入进行正确的相应
    • 能否在可接受的时间内执行完毕
    • 是否达到足够的可用性
    • 能否在与其的环境中安装运行
    • 能否满足用户的需求,符合用户的总体预期
  • 基准测试:运行计算机的一组程序或其他操作,以评估对象的相对性能的行为,通常是通过运行多个标准测试和针对该对象的试验来进行评估。

    基准测试旨在模拟组件或系统上的特定类型的工作负载。应用程序基准测试通常可以更好地衡量给定系统的实际性能,从而对应用程序进行相应的改进。

2. 测试驱动开发基本流程

测试驱动开发以测试样例驱动程序的开发,要求在编写某个功能的代码之前先编写测试代码,只编写使测试通过的功能代码,通过测试来推动整个开发的进行。这样的开发方式有助于编写简洁可用和高质量的代码,降低代码的耦合程度,提高程度的内聚度,从而提高整体系统的开发速度。

下面以最长非递减序列为例,介绍在 go 环境下测试驱动开发的基本流程:

2.1. 先写测试

测试驱动开发编写功能之前,要先编写对应的测试代码,这样做的目的是明确对应模块或函数的功能和需求,从而提高程序内聚度,降低耦合度。

在该例子中,要求在一段序列中找到最长非递减序列的长度。测试代码如下:

package lis

import "testing"

func TestLis(t *testing.T) {
    cases := []struct {
        num  uint
        arr  []int
        want uint
    }{
        {7, []int{1, 7, 3, 5, 9, 4, 8}, 4},
        {0, []int{}, 0},
    }
    for _, c := range cases {
        got := lis(c.num, c.arr)
        if got != c.want {
            t.Errorf("lis(%d, %v) == %d, want %d", c.num, c.arr, got, c.want)
        }
    }
}

该测试检测了基本情况的运行以及相应的边界情况。

2.2. 尝试运行测试

在没有编写源代码的时候,测试无法正常进行。但我们也可以尝试运行测试代码,检查测试框架是否被成功导入,测试代码是否存在编译错误。在没有其他问题的情况下,运行测试代码只有一个错误为对应的测试函数未定义。我们切换到对应的工作目录执行 go test 指令,得到如下的效果图:

go test before code

2.3. 使用最少的代码来让失败的测试先跑起来

使用最少的代码让失败的测试先跑起来,这一点在教程中使用斜体字体进行了重点强调。极力强调这一点的原因是,这样的做法可以验证测试工具是否正常工作,且排除了新测试存在缺陷始终通过的可能性。在预期中,该测试就是失败的结果,得到预期的结果,让开发者对测试框架增加信心。

package lis

func lis(num uint, arr []int) uint {
    return 0
}

得到的效果如下:

go test after adding code frame

2.4. 补充代码,通过测试

接下来的工作就是完成代码的编写,实现测试的需求,通过相应的测试以及原有的测试。通过测试则说明新代码符合测试要求且不会破坏原有的功能。

上述算法的代码为:

package lis

func lis(num uint, arr []int) uint {
    ret := uint(0)
    l := make([]uint, num)
    if num == 0 {
        return 0
    }
    for j := uint(0); j < num; j++ {
        l[j] = 1
        for i := uint(0); i < j; i++ {
            if arr[i] <= arr[j] && l[i]+1 > l[j] {
                l[j] = l[i] + 1
                if l[j] > ret {
                    ret = l[j]
                }
            }
        }
    }
    return ret
}

完成代码的编写后,可以看到成功完成测试。

go test after code

2.5. 重构

以上的算法的时间复杂度为 O ( n 2 ) O(n^2) O(n2),我们可以对函数进行重构进一步优化算法,得到时间复杂度为 O ( n log ⁡ n ) O(nlog n) O(nlogn) 的算法。重构以后的代码为:

package lis

// MAXINT used to init minNum
const MAXINT = 10000

func lis(num uint, arr []int) uint {
    if num == 0 {
        return 0
    }
    minNum := make([]int, num)
    l := uint(1)
    for i := uint(0); i < num; i++ {
        minNum[i] = MAXINT
    }
    minNum[0] = arr[0]
    for i := uint(1); i < num; i++ {
        loc := binSearch(minNum, 0, l, arr[i])
        minNum[loc] = arr[i]
        if loc == l {
            l++
        }
    }
    return l
}

func binSearch(arr []int, head uint, tail uint, key int) uint {
    if head+1 == tail {
        if key < arr[head] {
            return head
        }
        return tail
    }
    mid := uint((head + tail) / 2)
    if key < arr[mid] {
        return binSearch(arr, head, mid, key)
    }
    return binSearch(arr, mid, tail, key)
}

重构以后的代码仍然符合测试的要求。达到了提升代码质量,但不改变外部功能的要求。

go test after refactoring

2.6. 基准测试

使用基准测试可以测试程序的运行时间,测试代码如下:

func BenchmarkLis(b *testing.B) {
    for i := 0; i < b.N; i++ {
        lis(7, []int{1, 7, 3, 5, 9, 4, 1})
    }
}

测试的结果如下:

benchmark

可以看到上述代码运行了 14166468 次,平均每次运行所需时间为 84.9 ns。

3. 补充练习

3.1. 修改测试代码,调用可以指定字符重复的次数,修复代码

依据测试驱动开发,第一步要做的事是重新修改测试代码。修改后的测试代码如下:

func TestRepeat(t *testing.T) {
    cases := []struct {
        ch   string
        cnt  int
        want string
    }{
        {"a", 5, "aaaaa"},
        {"a", 0, ""},
    }
    for _, c := range cases {
        got := Repeat(c.ch, c.cnt)
        if got != c.want {
            t.Errorf("Repeat(%q, %d) == %q, want %q", c.ch, c.cnt, got, c.want)
        }
    }
}

未修改源代码前,执行 go test 的效果如下:

go test before modify

修改后的源代码如下:

package iteration

// Repeat for repeatCount times
func Repeat(character string, repeatCount int) string {
    var repeated string
    for i := 0; i < repeatCount; i++ {
        repeated += character
    }
    return repeated
}

随后执行 go test 指令,验证修改后的代码的正确性:

go test afer refactoring

3.2. 写一个 ExampleRepeat 来完善你的函数文档

我们在对应的 iteration 测试函数中增加以下函数:

func ExampleRepeat() {
    str := Repeat("b", 7)
    fmt.Println(str)
    //Output: bbbbbbb
}

执行 go test 可以测试多个测试函数,包括上面的 ExampleRepeat 函数。或者可以执行 go test -v 指令,得到具体的测试结果。

ExampleRepeat

4. 总结

通过这次实验,我们学习了:

  • 测试驱动开发的相应概念
  • 进行了一次简单的测试驱动开发的实践
  • 学习了基准测试等其他内容
版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/weixin_44547842/article/details/108819381
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢