测试驱动开发概览。
测试驱动开发(TDD)是一种编程风格:先编写测试,再用测试来指导程序设计的代码实现。
在开始写代码之前,先编写一个或多个测试(尤其是单元测试)。 这些测试旨在覆盖程序行为的某一个方面,可能只针对单个函数或方法。 编写测试,是把程序需求和整体架构转化为具体实现层面设计的一种方式。 运行测试,它们应该失败,因为代码还没有实现。 然后实现代码,再次运行测试。 如果测试通过,那么要么该行为的实现已经完成,要么可能还需要编写更多测试。 如果测试没有通过,就调试代码,然后再次运行测试。 测试与编码的这个循环会不断重复,直到所有必要的测试都通过;此时,程序该方面行为的实现就完成了……暂时如此。
重构是指为了改进设计而重写代码。 它不只是为了修 bug 而重写代码。 有时,为了让代码通过测试而修改代码,也会被称作“重构”。 虽然为了让测试通过而修改代码_可能_也包含改进设计,但单纯调试并不一定是在改进代码的_设计_,因此也不一定是在_重构_。
下面是一个只调试、不重构的例子:
# A function intended to return x added to y.
# x and y are bad parameter names, but we ignore that for now.
def add(x, y):
# used multiply operator by mistake. It fails the tests.
return x * y
# Function corrected. It passes the tests. It has been debugged, but not refactored.
def add(x, y):
return x + y
下面是一个先重构、再调试的例子:
# Function name and parameter names are modified to something more meaningful. This is refactoring.
def lot_inventory(old_cars, new_cars):
# Introduced multiply operator by mistake. It fails the tests. This is why we test.
return old_cars * new_cars
# Function corrected. It passes the tests. This is debugging.
def lot_inventory(old_cars, new_cars):
return old_cars + new_cars
Exercism 的 Python 轨道在练习中采用 TDD 方法。 单元测试已经写好。 学生可以查看这些测试,从而更详细地了解解答需要满足哪些条件才能通过。 可能会向学生提供解答的桩代码。
当 Python 解答有一个或多个测试失败时,对应的任务不会显示绿色背景。 第一个失败的任务区域会自动展开,其标题大致如下
Task 1 Extract coordinates -
点击减号可以折叠该任务,这样我们就能查看其他任务;不过现在我们先专注于这个任务。
其下方是一个已展开的测试区域,大致如下
Test 1 ⌄
FAILED TisburyTreasure > get coordinate
其中Tisbury Treasure表示练习,get_coordinate表示失败的函数或方法。
Test 1通常是一种模板,其中包含用于设置测试的代码段。
它不包含关于具体哪个测试失败的信息。
它会在接近底部的地方显示
One or more variations of this test failed. Details can be found under each [variant#].
点击⌄可以折叠该测试。
下方是一个已折叠的测试,大致如下:
Test 2 >
FAILED TisburyTreasure > get coordinate [variation #1] (item=
("Scrimshaw Whale's Tooth", '2A'), result='2A')
它的显示效果会随右侧窗格宽度的不同而变化。
点击>可以展开该测试。
输入数据和预期结果数据很可能会显示在代码段中。
这些数据可能涵盖该任务的所有测试。
在底部的Test Failure部分,会给出该测试失败的具体原因。
它可能类似这样:
AssertionError: ['2A'] != '2A'
在这个例子中,它表示返回的值['2A']与期望值'2A'不相等。
查看get_coordinate的代码,可以看到它是这样实现的
def get_coordinate(record):
return [record[1]]
如果去掉列表的方括号(例如return record[1]),再运行测试,任务 1 的测试就会通过。
如果还有一个或多个任务持续失败,就对每一个重复上述过程,直到所有测试都通过。
有时,预期数据和返回数据太大,无法全部放进Test Failure部分。
它可能类似这样:
AssertionError: '("Sc[67 chars]\')\n\n(\'Brass Spyglass\', \'Abandoned Lighth[952 chars]')\n' != '("Sc[67 chars]\')\n(\'Brass Spyglass\', \'Abandoned Lighthou[928 chars]')\n'
Diff is 970 characters long. Set self.maxDiff to None to see it.
数据可能仍然足够看出问题所在。
在上面的例子中,返回了两个换行符(例如\n\n(\'Brass Spyglass),而预期只有一个(例如\n(\'Brass Spyglass)。
恭喜! 所有测试都通过了。 接下来做什么? 可以立即发布解答。 或者,既然代码已经能正常工作,如果你想以任何理由重构它,都可以修改代码并提交另一个版本。 如果你觉得代码可以更好,却不知道从何下手,可以为该解答申请导师指导。 如果有导师可以指导,他可能会联系你,提出解答的其他思路。 发布解答时,你可以允许评论,其他学生也可以借此机会发表评论或提问。
虽然“过早优化是万恶之源”(这句话常被归到 Tony Hoare 和 Donald Knuth 名下),但总会有这样的时候:解答明明可以正常工作,却仍希望提升它的性能。
其中一种情况是:解答通过了部分测试,却在其他测试上超时。
知道一段代码究竟花了多少时间,会很有帮助。
timeit 模块可以测量代码的执行时间,精度可达极短的时长。
timeit函数最多可以接受五个实参:timeit.timeit(stmt='pass', setup='pass', timer=<default timer>, number=1000000, globals=None)。
stmt形参定义要运行并计时的实际代码。
number形参决定stmt代码要运行多少次。
setup形参定义只运行一次的代码,用于为运行stmt代码做准备。
setup代码运行的时间也计入总时间。
stmt代码运行的迭代次数越多,每次迭代中setup时间所占的比例就越小。
timer形参允许传入一个与默认Timer不同的计时器。
timer形参的默认实参是perf_counter,对大多数情况来说已经足够。
number形参的默认实参是1_000_000。
globals形参指定执行代码所用的命名空间。
globals形参的默认实参是None。
下面是一个使用timeit的例子,用来查看判断一个句子是否包含所有英语元音需要多长时间:
import timeit
# run one million times
loops = 1_000_000
# first positional argument is for stmt
# second positional argument is for setup
# third (named) argument is for number
print(timeit.timeit("""has_all_vowels('Another piggy digs up the truffles.')""",
"""
VOWELS = "AEIOU"
def has_all_vowels(sentence):
return all(letter in sentence.casefold() for letter in VOWELS)
""", number=loops) / loops)
运行一百万次,平均每次调用耗时4.965089999896008e-07秒(约合每次调用497纳秒)。
下面这个例子用来看看把casefold调用移出列表推导式是否能节省时间:
import timeit
loops = 1_000_000
print(timeit.timeit("""has_all_vowels('Another piggy digs up the truffles.')""",
"""
VOWELS = "AEIOU"
def has_all_vowels(sentence):
sentence = sentence.casefold()
return all(letter in sentence for letter in VOWELS)
""", number=loops) / loops)
运行一百万次,平均每次调用耗时4.923898000270128e-07秒(约合每次调用492纳秒)。
所以,把casefold移出列表推导式,每次调用大约节省了5纳秒,一百万次调用总共大约节省5毫秒。
cProfile 也可以用来对代码做性能分析;不过它的粒度没那么细,最小只能到毫秒级。