Python测试驱动开发(TDD)

Python测试驱动开发(TDD)

前言:TDD是一种敏捷开发模式,而不是测试方法

目录

项目github地址

https://github.com/Tyrone-Zhao/Test-Driven-Development

单元测试的好处

编程就像从井里打水

编程其实很难,我们的成功往往得益于自己的聪明才智。假如我们不那么聪明,TDD就能助我们一臂之力。Kent Beck(TDD理念基本就是他发明的)打了个比方。试想你用绳子从井里提一桶水,如果井不太深,而且桶不是很满,提起来很容易。就算提满满一桶水,刚开始也很容易。但要不了多久你就累了。TDD理念好比是一个棘轮,你可以使用它保存当前的进度,休息一会儿,而且能保证进度绝不倒退。这样你就没必要一直那么聪明了。Test All The Things!

细化测试每个函数的好处

程序变复杂后问题就来了,到时你就知道测试的重要性了。你要面临的危险是,复杂性逐渐靠近,而你可能没发觉,但不久之后你就会变成温水煮青蛙。
首先,写测试很简单,写起来不会花很长时间,所以,别抱怨,只管写就是了。
其次,占位测试很重要。先为简单的函数写好测试,当函数变复杂后,这道心理障碍就容易迈过去。你可能会在函数中添加一个if语句,几周后再添加一个for循环,不知不觉间就将其变成一个基于元类(meta-class)的多态树结构解析器了。因为从一开始你就编写了测试,每次修改都会自然而然地添加新测试,最终得到的是一个测试良好的函数。相反,如果你试图判断函数什么时候才复杂到需要编写测试的话,那就太主观了,而且情况会变得更糟,因为没有占位测试,此时开始编写测试需要投入很多精力,每次改动代码都冒着风险,你开始拖延,很快青蛙就煮熟了。

单元测试与功能测试的区别

单元测试和功能测试之间的界线有时不那么清晰。不过二者之间有个基本区别:功能测试站在用户角度从外部测试应用,单元测试则站在程序员的角度从内部测试应用。
采用的工作流程大致如下:
1)先写功能测试,从用户的角度描述应用的新功能
2)功能测试失败后,想办法编写代码让它通过(或者说至少让当前失败的测试通过)。此时,使用一个或多个单元测试定义希望代码实现的效果,保证为应用中的每一行代码(至少)编写一个单元测试
3)单元测试失败后,编写最少量的应用代码,刚好让单元测试通过。有时,要在第2步和第3步之间多次往复,直到我们觉得功能测试有一点进展为止。
4)然后,再次运行功能测试,看能否通过,或者有没有进展。这一步可能促使我们编写一些新的单元测试和代码等。

由此可以看出,在整个过程中,功能测试站在高层驱动开发,而单元测试从低层驱动我们做些什么。


功能测试代码, 测试主要功能(冒烟测试),数据有效性验证。详细代码可参见上面的github地址中的functional_test/

TDD的重要思想是必要时一次只做一件事。即每次只做必要的操作,让功能测试向前迈出一小步即可。


单元测试代码,视图逻辑测试,数据模型测试,模版表单测试。
Mock,参数校验:

from unittest.mock import patch, call
[...]
    @patch("accounts.views.auth")
    def test_calls_authenticate_with_uid_from_get_request(self, mock_auth):
        self.client.get("/accounts/login?token=abcd123")
        self.assertEqual(
            mock_auth.authenticate.call_args,
            call(uid="abcd123")
        )
  • 这里的patch mock了accounts.views.auth模块,而且把mock的范围限定在下面的测试函数范围内,测试函数执行完毕,mock即被取消
  • mock掉的auth模块中的所有属性和方法也同样是mock对象,通过在参数重传递mock对象为mock_auth来实现mock对象的调用
  • self.client.get()方法为django内置的模拟客户端,可以模拟客户端发送请求,示例中请求了”/accounts/login?token=abcd123”的url
  • 对应请求url的回调函数为auth模块中的authenticate方法,用mock对象的call_args属性获取其请求的参数,返回一个call(*args, **args)对象
  • 下面用mock模块中导入的call方法构建预期结果,使用TestCase类的assertEqual()方法断言结果,至此一个单元测试函数完毕

单元测试与集成测试的区别以及数据库

追求纯粹的人会告诉你,真正的单元测试绝不能设计数据库操作。不仅测试代码,而且还依赖于外部系统,如数据库的测试叫做集成测试更确切。


“单元测试/编写代码“循环

TDD中的单元测试/编写代码循环
1)在终端里运行单元测试,看它们是如何失败的。
2)在编辑器中改动最少量的代码,让当前失败的测试通过
然后不断重复。
想保证编写的代码无误,每次改动的幅度就要尽量小。这么做才能确保每一部分代码都有对应的测试监护。
乍一看工作量很大,初期也的确如此。但熟练之后你便会发现,即使步伐迈得很小,编程的速度也很快。

良好的单元测试实践方法要求,一个测试只能测试一个功能,多个功能需要写成多个测试。因为如果一个测试中有多个断言,一旦前面的断言导致测试失败,就无法得知后面的断言情况如何。

遵守不测试常量规则

单元测试要测试的其实是逻辑、流程控制和配置。编写断言检测HTML字符串中是否有指定的字符序列,不是单元测试应该做的。

重构的首要原则是不能没有测试,严格的TDD流程中,可以遵循以下顺序:
功能测试 -> 单元测试 -> 单元测试/编写代码循环 -> 重构代码

有用的TDD概念

  • 回归
    新添加的代码破坏了应用原本可以正常使用的功能。
  • 意外失败
    测试在意料之外失败了。这意味着测试中有错误,或者测试帮我们发现了一个回归,因此要在代码中修正。
  • 遇红/变绿/重构
    描述TDD流程的另一种方式。先编写一个测试看着它失败(遇红),然后编写代码让测试通过(变绿),最后重构,改进实现方式。
  • 三角法
    添加一个测试,专门为某些现有的代码编写用例,以此推断出普适的实现方式(在此之前的实现方式可能作弊了)。
  • 事不过三,三则重构
    判断何时删除重复代码使用的经验法则。如果两段代码很相似,往往还要等到第三段相似代码出现,才能确定重构时哪一部分是真正共通、可重用的。
  • 记在便签上的待办事项清单
    在便签上记录编写代码过程中遇到的问题,等手头的工作完成后再回过头来解决。

三种功能测试调试技术:行间print语句、time.sleep以及改进的错误消息:
如assert[Equal|In|True|其他](something, “错误消息”)

不要预先做大量的设计

TDD和软件开发中的敏捷运动联系紧密。敏捷运动反对传统软件工程实践中预先做大量设计的做法。敏捷理念认为,在实践中解决问题比理论分析能学到更多,要尽早把最简可用应用放出来,根据实际使用中得到的反馈逐步向前推进设计。当然,稍微思考一下设计往往能帮我们更快地找到答案。

使用递增的步进式方法修改现有代码,而且保证代码在修改前后都能正常运行。

YAGNI
关于设计的思考一旦开始就很难停下来,我们会冒出各种想法:或许想给每个清单起个名字或加个标题,或许想使用用户名和密码识别用户,或许想给产品页面添加一个较长的备注和简短的描述,或许想存储某种顺序,等等。但是,要遵守敏捷理念的另一个信条:”YAGNI”(读作yag-knee)。它是”You ain’t gonna need it”(你不需要这个)的简称。
有时我们冒出一个想法,觉得可能需要,可问题是,不管想法有多好,大多数情况下最终你都用不到这个功能。

REST(式)
“表现层状态转化”(representational state transfer, REST)是Web设计的一种方式。设计面向用户的网站时,不必严格遵守REST规则,可是从中能得到一些启发。想看看REST API是什么样子,可以查看我的另一篇博文。

确保出现回归测试

独立用户场景下的功能测试通过后,要注意多用户场景下的功能测试回归。
重构代码,或者开发新功能时修改了旧有代码,要注意单元测试和功能测试的回归。

如何测试设计和布局

简单来说,不应该为设计和布局编写测试。因为这太像是测试常量,所以写出的测试不太牢靠
这说明设计和布局的实现过程极具技巧性、涉及CSS和静态文件。因此,可以编写一些简单的“冒烟测试“,确认静态文件和CSS起作用即可。把代码部署到生产环境时,冒烟测试能协助我们发现问题。
但是如果某部分样式需要很多客户端JavaScript代码才能使用(如动态缩放),就必须为此编写一些测试。
要试着编写最简的测试,确信设计和布局起作用即可,不必测试具体的实现。我们的目标是能自由修改设计和布局,且无须时不时地调整测试。

功能测试代码, 布局和样式测试:

from selenium.webdriver.common.keys import Keys
from .base import FunctionalTest


class LayoutAndStylingTest(FunctionalTest):

    def test_layout_and_styling(self):
        # 小明访问首页
        self.browser.get(self.live_server_url)
        self.browser.set_window_size(1024, 768)

        # 他看到输入框完美地居中显示
        inputbox = self.get_item_input_box()
        self.assertAlmostEqual(
            inputbox.location["x"] + inputbox.size["width"] / 2,
            512,
            delta=10
        )

        # 他新建了一个清单,看到输入框仍完美地居中显示
        inputbox.send_keys("测试")
        inputbox.send_keys(Keys.ENTER)
        self.wait_for_row_in_list_table("1: 测试")
        inputbox = self.get_item_input_box()
        self.assertAlmostEqual(
            inputbox.location["x"] + inputbox.size["width"] / 2,
            512,
            delta=10
        )

TDD以及部署的危险区域

部署的过程中一些危险区域如下:
1)静态文件(CSS、JavaScript、图片等)
Web服务器往往需要特殊的配置才能伺服静态文件
2)数据库
可能会遇到权限和路径问题,还要小心处理,在多次部署之间不能丢失数据
3)依赖
要保证服务器上安装了网站依赖的包,而且版本要正确

不过这些问题有相应的解决方案:
1)使用与生产环境一样的基础架构部署过渡网站(staging site),这么做可以测试部署的过程,确保部署真正的网站时操作正确。
2)可以在过渡网站中运行功能测试,确保服务器中安装了正确的代码和依赖包。而且为了测试网站的布局,我们编写了冒烟测试,这样就能知道是否正确加载了CSS。
3)与在本地设备上一样,当服务器上运行多个Python应用时,可以使用虚拟环境管理包和依赖。
4)最后,一切操作都自动化完成。使用自动化脚本部署新版本,使用同一个脚本把网站部署到过渡环境和生产环境,这么做能尽量保证过渡网站和线上网站一样。

调试技巧
查看Nginx的错误日志,存储在/var/log/nginx/error.log中。
检查Nginx的配置:nginx -t
确保浏览器没有缓存过期的响应。按下Ctrl键的同时点击刷新按钮。

别忘了重构

TDD不是万能灵药。它要求你在测试通过后花点实践重构,改进设计。否则“技术债务“将高高筑起。
不过,重构的最佳方法往往不那么容易想到,可能等到写下代码之后的几天、几周甚至几个月,处理完全无关的事情时,突然灵光一闪才能想出来。

在解决其他问题的途中,应该停下来去重构以前的代码吗?
要视情况而定。不能冒险在无法正常运行的应用中重构,可以在便签上做个记录,等测试组件能全部通过之后再重构。

关于重构的小贴士
1)把测试放在单独的文件夹中
功能测试可以按照特定功能或用户故事的方式组织。
单元测试分拆成文件,放在一个Python包中。
2)编写测试的主要目的是让你重构代码!一定要重构,尽量让代码(包括测试)变得简洁。
3)测试失败时别重构
如果测试的对象还没实现,可以先为测试方法加上@skip装饰器。
记下想重构的地方,完成手头上的活儿,等应用处于正常状态时再重构。
提交代码之前别忘了删掉所有@skip装饰器!

尽早合并代码

精益理论中的“尽早部署“有个推论,即“尽早合并代码“。编写表单可能要花很多时间,不断添加各种功能—做了各种工作,得到一个功能完善的表单类,但发布应用后才发现大多数功能实际并不需要。
因此,要尽早试用新编写的代码。

要判断何时应该编写测试确认我们没有犯错

测试时要判断何时应该编写测试确认我们没有犯错。一般而言,做决定时要谨慎。
不可能编写测试检查所有可能出错的方式。如果有一个函数计算两数之和,可以编写一些测试:

    assert adder(1, 1) == 2
    assert adder(2, 1) == 3

但不应该认为实现这个函数时故意编写了有违常理的代码:

    def adder(a, b):
        # 不可能这么写
        if a == 3:
            return 666
        else:
            return a + b

判断时你要相信自己不会故意犯错,只会不小心犯错。

单元测试需要一个断言框架和报告程序,或许还要选择一个技术模拟库

在JavaScript领域,测试工具的选择有许多种,如jsUnit、Qunit、Mocha、Chutzpah、Karma、Testacular、Jasmine等。选择其中一个工具后,还得选择一个断言框架报告程序,或许还要选择一个驭件(spy侦件、fake伪件、stub桩件)技术库。

示例项目中使用的是QUnit,简单,根Python单元测试很像,而且能很好地和jQuery配合使用。
代码可以参考上面的github地址中list/static/tests/test.html
这里写图片描述

JavaScript测试在TDD循环中的位置

JavaScript测试在双重TDD循环中处于什么位置?答案是,JavaScript测试和Python单元测试扮演的角色完全相同。
1)编写一个功能测试,看着它失败
2)判断接下来需要哪种代码,Python还是JavaScript?
3)使用选中的语言编写单元测试,看着它失败。
4)使用选中的语言编写一些代码,让测试通过。
5)重复上述步骤

一些缺憾

1)编写JavaScript时,应该尽量利用编辑器提供的协助,避免常见的问题。试一下句法/错误检查工具,如jsLinter、jshint。
2)使用Phantomjs可以让JavaScript测试在命令行中运行。
这里写图片描述
3)前端开发圈目前流行angular.js和React这样的MVC框架。这些框架的教程大都使用一个RSpec式断言库,名为Jasmine。如果你想使用MVC框架,使用Jasmine比Qunit更方便。

探索性编程、探究及去掉探究代码

学习新工具,或者研究新的可行性方案时,一般都可以适当地把严格的TDD流程放在一边,不编写测试或编写少量的测试,先把基本的原型开发出来。
这种创建原型的过程一般叫作“探究“(spike)。

最好在一个新分支中去探究,去掉探究代码时再回到主分支。

把探究所得应用到真实的代码基中。要完全摒弃探究代码,然后从头开始,用TDD流程再实现一次。去掉探究代码后实际编写的代码往往与最初有很大不同,而且通常更好。
该不该这么做请视情况而定!

在Python中使用模拟技术

模拟技术,是在单元测试中测试外部依赖的通用方式。只要与第三方API交互都适合使用驭件测试。
代码有外部副作用时也是如此,例如调用API、发推文、发短信等等。我们并不想真的通过互联网发推文或者调用API。但又必须找到一种方法,测试代码是否正确。驭件(mock)正是我们寻找的答案。

@patch("accounts.views.auth")
class LoginViewTest(TestCase):
    ''' 登录视图测试 '''

    [...]

    @patch("accounts.views.send_mail")
    def test_sends_link_to_login_using_token_uid(self, mock_send_mail,
                                                 mock_auth):
        ''' 测试含有token的登录链接被发送到指定邮件地址 '''
        self.client.post("/accounts/send_login_email", data={
            "email": "[email protected]"
        })

        token = Token.objects.first()
        expected_url = f"http://testserver/accounts/login?token={token.uid}"
        (subject, body, from_email, to_list), kwargs = mock_send_mail.call_args
        self.assertIn(expected_url, body)

使用驭件可能导致“与实现紧密耦合“。我们知道,通常最好测试行为,而不测试实现细节;测试发生了什么,而不测试是如何发生的。驭件往往在如何做这条路上走的太远,而很少关注“是什么“。
如果能有效减少测试之间的重复,就有充分的理由使用驭件。这是避免组合爆炸的一种方式。

把ORM代码放到辅助方法中

以复杂度为准则

不同测试类型以及解耦ORM代码的利弊

CI和Selenium最佳实践

在不同的测试类型之间正确权衡

使用Sinon.js测试客户端Ajax

TDD速查表

项目开始阶段

TDD基本流程

测试不止要在开发环境中运行

通用的测试最佳实践

Selenium功能测试最佳实践

由外而内,测试隔离与整合测试,模拟技术

参考资料: 《Python测试驱动开发》第2版、《Python测试之道》

猜你喜欢

转载自blog.csdn.net/weixin_41845533/article/details/81232812