关于单元测试(go)

在最近开发过程中,需要每个模块都写单元测试,由于之前开发没有写单元测试的习惯,突然要求写单元测试,还不知道从何入手,于是花了点时间学习如何写单元测试,收获很多,因此本文算是近期学习单元测试的总结,主要有以下4个方面:

1 单元测试的定义

首先看看什么是单元测试(unit testing),单元测试是将开发人员编写的一个完整的类、子程序或者函数从完整的系统中隔离出来进行的测试,一般由开发人员自己编写。比如开发一个计算器,那么实现加法功能的子程序就可以从系统中隔离出来进行单元测试,当然前提是你写的代码具有可测性,我的理解是尽量模块化和函数功能单一。

2 单元测试的好处

如果开发人员在开发过程中已经做了足够的单元测试,确保了单元测试的覆盖率,那么当这些类和子程序在组合使用或者被其他模块调用时就会确保少出现bug,当然要确保没有任何bug是不可能的。还是以开发计算器为例,如果实现加法、减法、乘法和除法的模块都已经做了充分的单元测试,那么这些模块组合在一起就能确保计算器能正常工作,不会出现很严重的bug,在一定程度上保证了软件的质量。

3 单元测试应该包含哪些case

这里以一个判断有效机器名的函数为例,函数声明如下:

func IsValidHostName(hostName string) bool

有效的机器名规定如下如下:

机器名只能由小写字母组成,且机器名最短为4个字符,最长为8个字符

那么,根据以上规定,一个良好的单元测试case至少应该包含以下三种:

  • 正向case

hostaahostbb都是有效的机器名

  • 负向case

Hostaa(含有大写字母)、host123(含有数字)和Host!(包含叹号)都是无效的机器名

  • 边界case

host(满足最短机器名要求)和hostabcd(满足最长机器名要求)都是有效的机器名,但是hos(3个字符)和hostabcde(9个字符)都是无效的机器名

4 单元测试怎么写

在写单元测试时,我个人认为至少满足以下2个条件:

  • 很容易添加测试case
  • 测试失败时,能通过输出信息快速判断失败原因

基于以上2个条件,我们开始构造测试数据,先定义一个测试数据的结构体,该结构体包含2个字段,输入input和期待输出expectedOutput,这里定义成空接口interface{}方便构造任何类型的输入和输出数据。

type testData struct {
    input          interface{}
    expectedOutput interface{}
}

按照3中列出的case,测试case如下(注:可以看到每行都是是一个完整的测试case,添加测试case极其容易):

    testCaseList := []testData{
        // 正向case,每行是一个case
        {"hostaa", true},
        {"hostbb", true},
        {"host cc", true},

        //负向case,每行是一个case
        {"Hostaa", false},
        {"host123", false},
        {"host!", false},

        // 边界case,每行是一个case
        {"host", true},
        {"hostabcd", true},
        {"hos", false},
        {"hostabcde", false},
    }

测试失败时,打印的信息至少需要包含以下内容:

  • 第几个测试case
  • 输入和期待输出
  • 实际输出

基于此,可以构造一个测试失败时的打印函数,例如:

func myTestFail(
    t *testing.T,
    testCase testData,
    actualOutput interface{},
    testCaseIndex int) {

    if actualOutput != testCase.expectedOutput.(bool) {
        t.Errorf("\n\ncase %+v:", testCaseIndex)
        t.Errorf("input = %+v", testCase.input)
        t.Errorf("expected output = %+v", testCase.expectedOutput)
        t.Errorf("actual output = %+v", actualOutput)
    }
}

当某个测试case失败时,打印如下:

--- FAIL: TestIsValidHostName (0.00s)
        demo_test.go:17:

                case 2:
        demo_test.go:18: input = host cc
        demo_test.go:19: expected output = true
        demo_test.go:20: actual output = false

从输出可以知道,第2个测试case失败,输入是host cc,期待输出是true,实际输出是false,很容易就能定位出失败原因:因为多输入了一个空格。

附上完整代码:
  • demo.go(需要进行单元测试的代码)
package demo

import "unicode"

func IsValidHostName(hostName string) bool {
    const (
        MIN_HOST_NAME_LEN = 4
        MAX_HOST_NAME_LEN = 8
    )

    hostNameLen := len(hostName)
    if hostNameLen < MIN_HOST_NAME_LEN || MAX_HOST_NAME_LEN < hostNameLen {
        return false
    }

    for _, char := range hostName {
        isLower := unicode.IsLower(char)
        if !isLower {
            return false
        }
    }

    return true
}

  • demo_test.go(单元测试代码)
package demo

import "testing"

type testData struct {
    input          interface{}
    expectedOutput interface{}
}

func myTestFail(
    t *testing.T,
    testCase testData,
    actualOutput interface{},
    index int) {

    if actualOutput != testCase.expectedOutput.(bool) {
        t.Errorf("\n\ncase %+v:", index)
        t.Errorf("input = %+v", testCase.input)
        t.Errorf("expected output = %+v", testCase.expectedOutput)
        t.Errorf("actual output = %+v", actualOutput)
    }
}

func TestIsValidHostName(t *testing.T) {
    testCaseList := []testData{
        // 正向case,每行是一个case
        {"hostaa", true},
        {"hostbb", true},
        {"host cc", true},

        //负向case,每行是一个case
        {"Hostaa", false},
        {"host123", false},
        {"host!", false},

        // 边界case,每行是一个case
        {"host", true},
        {"hostabcd", true},
        {"hos", false},
        {"hostabcde", false},
    }

    for index, testCase := range testCaseList {
        actualOutput := IsValidHostName(testCase.input.(string))
        myTestFail(t, testCase, actualOutput, index)
    }
}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 177,166评论 25 709
  • 1.测试与软件模型 软件开发生命周期模型指的是软件开发全过程、活动和任务的结构性框架。软件项目的开发包括:需求、设...
    Mr希灵阅读 22,309评论 7 278
  • 1.测试与软件模型 软件开发生命周期模型指的是软件开发全过程、活动和任务的结构性框架。软件项目的开发包括:需求、设...
    宇文臭臭阅读 11,715评论 5 101
  • “曾子曰:慎终追远,民德归厚矣。 子禽问于子贡曰:夫子至于是邦也,必闻其政,求之与,抑与之与?子贡...
    钱江潮369阅读 3,777评论 0 2
  • 《我所知道的南工》 看一回宁静的桥影 数一数螺细的波纹 我倚暖了石阑的青苔 青苔凉透了我的心坎 还有几株青色的花草...
    lavender_092c阅读 1,625评论 0 0

友情链接更多精彩内容