软件测试基础 (一): 单元测试

Hello!大家好,我是BugBear,一个专注于分享软件测试干货的测试开发。

对于软件测试,我们先按照开发阶段来进行划分,将软件测试分为单元测试、集成测试、系统测试、验收测试,下面我们来聊聊单元测试。

1、什么是单元测试?

在正式阐述什么是单元测试之前,我先给大家分享一个工厂组装手机的例子。


手机组装流水线按照图纸将各个电子元件组装焊接为各个模块组件(如喇叭,听筒,麦克,FPC,按键板,摄像头,LCD等),再将各个模块组件组装成一部完整的手机。

如果一起顺利,在给手机安装系统后就可以正常使用了。但是很不幸,大多数情况下的手机是无法使用的,那么就需要将已经组装好的手机重新拆机,逐个模块排查问题,在每个模块排查中需要对每个电子元件进行检测,通过花费大量的时间和精力才能定位到问题原因。

那么在后续的生产中,如何才能避免这种问题的发生呢?

你可能立即就会想到,为什么不在组装焊接前,就先测试每个要用到的电子元器件呢?这样你就可以先排除有问题的元器件,最大程度地防止组装完成后逐级排查问题的事情发生。

实践也证明,这的确是一个行之有效的好办法。

如果把手机的生产、测试和软件的开发、测试进行类比,你可以发现:

电子元器件就像是软件中的单元,通常是函数或者类,对单个元器件的测试就像是软件测试中的单元测试

组装完成的功能模块组件如喇叭,听筒,麦克,FPC,按键板,摄像头,LCD等就像是软件中的模块,对功能模块组件的测试就像是软件中的集成测试

手机全部组装并安装系统就像是软件完成了预发布版本,手机全部组装并安装系统完成后的开机测试就像是软件中的系统测试

单元测试是指,对软件中的最小可测试单元在与程序其他部分相隔离的情况下进行检查和验证的工作,这里的最小可测试单元通常是指函数或者类。

2、什么是好的单元测试?

好的单元测试应当包含四种特性:正确清晰完整健壮

  • 正确:单元测试是最基础的要求,必须要保证所写的函数或者类实现的功能是正确的,如果实现的功能都不能满足,那就是缺陷!

  • 清晰:单元测试可以帮助其他开发理解函数或者类的实现,所以要求单元测试用例简洁、清晰,需要有良好的可读性

  • 完整:单元测试需要考虑输入与输出组合的各种场景,保证单元测试的覆盖率

  • 健壮:健壮性是最容易被忽略的一项,当被测试的类或者函数被修改内部实现或者添加功能时,⼀个好的单测应该完全不需要被修改或者只有极少的修改。⽐如⼀个排序函数的单测实现是完全稳定的,它不应该跟着不同的排序算法⽽变化

3、怎么写单元测试?

可能大多数的测试人员不会接触到单元测试的编写,因为按照我个人的看法,开发人员根据自己写的代码编写单测用例是最合适不过的,也是最高效的

虽然我们不需要实际去编写单测用例,但是我们还是需要了解怎么写单元测试。

单元测试的代码结构一般包含三部分:分别是准备调用断言

  • 准备:准备部分的⽬的是准备好调⽤所需要的外部环境,如数据,Stub(桩代码),Mock,临时变量,调⽤请求,环境背景变量等等。

  • 调用:调⽤部分则是实际调⽤需要测试⽅法,即函数或者流程本身。

  • 断言:断⾔部分判断调⽤部分的返回结果是否符合预期。

每个单元测试都应该能清晰地分出这三部分,当然有时调⽤断⾔两部分合在⼀起也是⽐较常见的。

4、玩转单元测试

下面我们来聊聊单元测试编写用例的相关知识,首先我们需要了解单元测试的三个重要部分,即驱动程序桩程序Mock

驱动程序:驱动程序(Driver)也称作驱动模块,用以模拟被测模块的上级模块,能够调用被测模块。在测试过程中,驱动模块接收测试数据,调用被测模块并把相关的数据传送给被测模块。

简单说就是你负责测试的模块没有main()方法入口,所以需要写一个带main的方法来调用你的模块或方法。这个就是驱动测试

桩程序:桩程序(Stub),也称桩模块,用以模拟被测模块工作过程中所调用的下层模块,即被测模块本身调用的其他关联函数。桩模块由被测模块调用,它们一般只进行很少的数据处理。

桩是指用来代替关联代码或者未实现的代码,为了让测试对象可以正常的执行,其实一般会硬编码一些输入和输出,保证被测模块能够正常运行

Mock:Mock除了保证Stub的功能之外,还可深入的模拟对象之间的交互方式,如:调用了几次、在某种情况下是否会抛出异常以及提供数据断言

接下来我们通过一个实例来学习单元测试用例的编写


# 待测试的方法
def calculator(type):
    # 调用桩代码获取数据
    num1 = __stub1()
    num2 = __stub2()
    # 调用mock
    mock_data = __mock_check()

    # +
    if type.lower() == 'add':
        type = 'add'
        ret = num1+num2
        assert ret == mock_data[type]
        print('{} + {} = {}'.format(num1,num2,ret))
        return ret
    # -
    if type.lower() == 'minus':
        type = 'minus'
        ret = num1-num2
        assert ret == mock_data[type]
        print('{} - {} = {}'.format(num1,num2,ret))
        return ret
    # *
    if type.lower() == 'multiply':
        type = 'multiply'
        ret = num1*num2
        assert ret == mock_data[type]
        print('{} * {} = {}'.format(num1,num2,ret))
        return ret
    # /
    if type.lower() == 'divide':
        type = 'divide'
        if num2 == 0:
            print('除法分母不能为0')
            return '除法分母不能为0'
        else:
            ret = num1/num2
            assert ret == mock_data[type]
            print('{} / {} = {}'.format(num1,num2,ret))
            return ret

# 桩代码1
def __stub1():
    output = 20
    print('my stub的值是{}'.format(output))
    return output

# 桩代码2
def __stub2():
    output = 5
    print('my stub的值是{}'.format(output))
    return output

# Mock代码 => 提供断言数据
def __mock_check():
    mock_result = {}
    mock_result['add'] = 25
    mock_result['minus'] = 15
    mock_result['multiply'] = 100
    mock_result['divide'] = 4
    return mock_result

# 驱动程序
if __name__=="__main__":
    print(calculator('add'))
    print(calculator('minus'))
    print(calculator('multiply'))
    print(calculator('divide'))

上面提供的是一个简单的单元测试,包含了驱动程序、被测对象、桩程序以及Mock代码

驱动程序main作为被测对象的上级模块,运行时调用被测函数calculator

被测函数calculator被调用后,通过桩代码__stub1__stub2提供测试数据

被测函数calculator通过不同的入参匹配不同的场景,不同场景获取的结果与Mock函数__mock_check进行比对断言,校验结果是否符合预期

被测函数成功返回如下:

my stub的值是20
my stub的值是5
20 + 5 = 25
25

被测函数失败返回如下:

my stub的值是20
Traceback (most recent call last):
my stub的值是5
  File "/Users/luozelin/Desktop/demo/unittest_demo/ut_demo.py", line 73, in <module>
    print(calculator('add'))
  File "/Users/luozelin/Desktop/demo/unittest_demo/ut_demo.py", line 21, in calculator
    assert ret == mock_data[type]
AssertionError

文末结语

BugBear软件测试,专注于分享测试干货,欢迎关注,交流成长

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,284评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,115评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,614评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,671评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,699评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,562评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,309评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,223评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,668评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,859评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,981评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,705评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,310评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,904评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,023评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,146评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,933评论 2 355

推荐阅读更多精彩内容