总结
我很懒,我想用最少的行动实现目标,例如生成测试数据。我们可以:
使用随机种子保证数据的一致性。
>>> random.seed(769876987698698)
>>> [random.randint(0, 10) for _ in range(10)]
[10, 9, 1, 9, 10, 6, 5, 10, 1, 9]
>>> random.seed(769876987698698)
>>> [random.randint(0, 10) for _ in range(10)]
[10, 9, 1, 9, 10, 6, 5, 10, 1, 9]
使用mimesis
生成虚假数据:
>>> from mimesis import Generic
>>> g = Generic()
>>> for _ in range(5):
... print(
... f"[{g.datetime.date()}] {g.person.full_name()}, "
... f"PIN attempt {g.code.pin()} at "
... f"{g.finance.bank()} ({g.address.city()}) "
... f"from {g.internet.ip_v4()}"
... )
[2005-08-20] Rosario Howe, PIN attempt 1155 at Bank of Marin (Solon) from 171.243.230.180
[2001-11-14] Julieann Spencer, PIN attempt 4044 at State Street Corporation (Round Rock) from 155.236.115.163
[2003-07-24] Mikel Buchanan, PIN attempt 1057 at Union Bank (Scarsdale) from 211.192.167.83
[2010-10-30] Antione Duffy, PIN attempt 6219 at Sandy Spring Bancorp (Phoenix) from 207.104.0.29
[2004-06-05] Damon Stephens, PIN attempt 6115 at First National Community Bank (Newport) from 226.230.244.209
使用freezegun
冻结时间:
>>> import datetime
... from freezegun import freeze_time
...
... def calendar_with_cheesy_quote():
... print(f"{datetime.date.today():%B %d, %Y}")
... print("Each day provide it's own gift")
...
... with freeze_time("2012-01-14"):
... calendar_with_cheesy_quote()
使用pyfakefs
模拟内存中的文件系统:
from pyfakefs.fake_filesystem_unittest import Patcher
from pathlib import Path
import oswith Patcher() as patcher:fs = patcher.fstemp_path = Path(os.path.join(os.sep, 'tmp', 'tempdir'))fs.create_dir(temp_path)(temp_path / 'subdir1').mkdir()(temp_path / 'subdir2').mkdir()(temp_path / 'file1.txt').write_text('This is file 1')(temp_path / 'file2.txt').write_text('This is file 2')print(f'Now you see me: {temp_path}')print(f'Contents: {list(temp_path.iterdir())}')fs.remove_object(temp_path)print(f"Now you don't: {temp_path.exists()}"")
通过VCR.py
记录HTTP调用:
import pytest
import requests@pytest.mark.vcr
def test_single():assert requests.get("http://httpbin.org/get").text == '{"get": true}'
使用inline-snapshot
快照测试结果:
from inline_snapshot import snapshotdef test_something():assert 1548 * 18489 == snapshot() # snapshot() is the placeholder
现在是自由活动时间,去操场玩吧!
Less is more。
你可以手动创建伪造的数据,但很麻烦。让我们升级到自动化。
关于seed
许多生成数据的工具都使用seed产生伪随机。
实际上,真正的随机无法通过软件实现,只能通过现实中的随机源实现。但很多时候我们只需要一个无法被别人预测的值。
secrets
使用随机性的操作系统源,random
使用随机种子产生伪随机。
使用相同的随机种子,会得到同一个值:
>>> random.seed(769876987698698)
>>> [random.randint(0, 10) for _ in range(10)]
[10, 9, 1, 9, 10, 6, 5, 10, 1, 9]
>>> random.seed(769876987698698)
>>> [random.randint(0, 10) for _ in range(10)]
[10, 9, 1, 9, 10, 6, 5, 10, 1, 9]
一般虚假数据
从生成 HTML 填充到填充数据库,伪造数据是一项资产,可用于概念验证、文档,当然还有测试。
memesis
是一个极好的 Python 库,您可以 pip 安装,它可以让您生成成千上万的虚假电子邮件、地址、社会安全号码、颜色、食品和 lorem ipsums。
>>> from mimesis import Generic
>>> g = Generic()
>>> for _ in range(5):
... print(
... f"[{g.datetime.date()}] {g.person.full_name()}, "
... f"PIN attempt {g.code.pin()} at "
... f"{g.finance.bank()} ({g.address.city()}) "
... f"from {g.internet.ip_v4()}"
... )
[2005-08-20] Rosario Howe, PIN attempt 1155 at Bank of Marin (Solon) from 171.243.230.180
[2001-11-14] Julieann Spencer, PIN attempt 4044 at State Street Corporation (Round Rock) from 155.236.115.163
[2003-07-24] Mikel Buchanan, PIN attempt 1057 at Union Bank (Scarsdale) from 211.192.167.83
[2010-10-30] Antione Duffy, PIN attempt 6219 at Sandy Spring Bancorp (Phoenix) from 207.104.0.29
[2004-06-05] Damon Stephens, PIN attempt 6115 at First National Community Bank (Newport) from 226.230.244.209
您甚至可以设置区域设置。例如,如果我想要一些法语数据:
>>> from mimesis.locales import Locale
... g = Generic(locale=Locale.FR)
... for _ in range(5):
... print(
... f"[{g.datetime.date()}] {g.person.full_name()}, "
... f"essai de PIN {g.code.pin()} à "
... f"{g.finance.bank()} ({g.address.city()}) "
... f"depuis {g.internet.ip_v4()}"
... )
...
[2004-02-14] Joana Menard, tentative de PIN 4616 à Neuflize OBC (Colmar) depuis 193.125.166.248
[2012-07-08] Juliet Dastous, tentative de PIN 1168 à Banque de Savoie (Agen) depuis 6.232.176.31
[2018-01-23] Chloé Maurin, tentative de PIN 4101 à LCL (Menton) depuis 142.144.98.158
[2021-03-18] Nabil Jutras, tentative de PIN 6478 à Crédit Mutuel Alliance Fédérale (Bobigny) depuis 58.188.207.52
[2013-03-29] Noam Grand, tentative de PIN 4820 à AXA Banque (Poissy) depuis 186.54.107.131
它甚至可以与日语一起使用,数据集令人印象深刻,并且它已经取代了我工具包中我心爱的faker
。
我现在在PYTHONSARTUP中使用这个:
PYTHONSARTUP: 设置shell变量的脚本
try:import mimesis
except ImportError:pass
else:from mimesis.locales import Localefrom mimesis import Genericclass FakeProvider:def __init__(self, provider):self.provider = providerself.count = 1def __dir__(self):return dir(self.provider)def __repr__(self):return f"FakeProvider({repr(self.provider)})"def __getattr__(self, name):subprovider = getattr(self.provider, name)wrapper = lambda *args, count=1, **kwargs: self.call_faker(subprovider, *args, **kwargs)wraps(subprovider)(wrapper)return wrapperdef __call__(self, count=1):self.count = countreturn selfdef call_faker(self, subprovider, *args, **kwargs):if self.count == 1:return subprovider(*args, **kwargs)else:return [subprovider(*args, **kwargs) for _ in range(self.count)]self.count = 1class Fake(object):def __init__(self, locale=Locale.EN):self.configure(locale)def get_factory(self, locale=Locale.EN, _cache={}):if isinstance(locale, str):locale = getattr(Locale, locale.upper())if locale in _cache:return _cache[locale]return Generic(locale=locale)def configure(self, locale=Locale.EN):self.factory = self.get_factory(locale)@propertydef fr(self):return self.get_factory(locale="fr")@propertydef en(self):return self.get_factory(locale="en")def __getattr__(self, name):return FakeProvider(getattr(self.factory, name))def __dir__(self):return ["fr", "en", *dir(self.factory)]fake = Fake()
在python会话中,我可以执行:
>>> fake.food(10).fruit()
['Kahikatea', 'Canistel', 'Hackberry', 'Bilberry', 'Genip', 'Cocona', 'Limeberry', 'Blueberry', 'Bilimbi', 'Redcurrant']
是时候了
如果必须在代码中提供日期,有两种方法可以使其易于测试。
第一,通过参数传递日期。例如:
def create_article(text, time_provider=datetime.datetime.now):Article(text, created_at=date_provider())
第二:使用黑魔法,在测试期间修补时间。
认识一下 freezegun
,可以让你暂停时间:
>>> import datetime
... from freezegun import freeze_time
...
... def calendar_with_cheesy_quote():
... print(f"{datetime.date.today():%B %d, %Y}")
... print("Each day provide it's own gift")
...
... with freeze_time("2012-01-14"):
... calendar_with_cheesy_quote()
...
January 14, 2012
Each day provide it's own gift
它与 pytest
集成,因此您可以装饰测试并假装您是 Timelord:
@freeze_time("Jan 14th, 2020") # you can use a nice format here too
def test_sonic_screwdriver_in_overrated_show_there_I_said_it():...
它可以从某个点开始时间,然后继续流动:
@freeze_time("2012-01-14", tick=True)
def test_turn_it_on_turn_it_on_turn_it_on_again():# the clock starts again here, and the first value is 2012-01-14 # at 00:00:00 then whatever number of seconds goes by after that
可以设置每次请求时间时增加 15 秒:
@freeze_time("2012-01-14", auto_tick_seconds=15)
def test_wheeping_angels_when_you_blink():# the clock starts again here, and the first value is 2012-01-14 # at 00:00:00 then every call to datetimes add 15 seconds
或手动控制时间:
with freeze_time("2012-01-14") as right_here_right_now:right_here_right_now.tick(3600) # add 1H next time we request timeright_here_right_now.move_to(other_datetime) # jump in time
I/O
你将不得不读取和写入一些东西,如文件和套接字。这些都是非常不可靠的副作用,所以嘲笑可能是你的第一个赌注。但是有一些专门的工具可以让你的生活更轻松。
众所周知,stdlib
附带了 tempfile
模块,该模块可以生成自动清理的临时文件和目录:
>>> import tempfile
... from pathlib import Path
...
... with tempfile.TemporaryDirectory() as temp_dir:
... temp_path = Path(temp_dir)
...
... (temp_path / 'subdir1').mkdir()
... (temp_path / 'subdir2').mkdir()
...
... (temp_path / 'file1.txt').write_text('This is file 1')
... (temp_path / 'file2.txt').write_text('This is file 2')
...
... print(f'Now you see me: {temp_dir}')
... print(f'Contents: {list(temp_path.iterdir())}')
...
... print(f"Now you don't: {Path(temp_dir).exists()}")
Now you see me: /tmp/tmp67e3hjr2
Contents: [PosixPath('/tmp/tmp67e3hjr2/file1.txt'), PosixPath('/tmp/tmp67e3hjr2/subdir2'), PosixPath('/tmp/tmp67e3hjr2/subdir1'), PosixPath('/tmp/tmp67e3hjr2/file2.txt')]
Now you don't: False
即使在测试之外也很有用,但如果由于某种原因您不想触摸硬盘驱动器,则有pyfakefs
:
from pyfakefs.fake_filesystem_unittest import Patcher
from pathlib import Path
import oswith Patcher() as patcher:fs = patcher.fstemp_path = Path(os.path.join(os.sep, 'tmp', 'tempdir'))fs.create_dir(temp_path)(temp_path / 'subdir1').mkdir()(temp_path / 'subdir2').mkdir()(temp_path / 'file1.txt').write_text('This is file 1')(temp_path / 'file2.txt').write_text('This is file 2')print(f'Now you see me: {temp_path}')print(f'Contents: {list(temp_path.iterdir())}')fs.remove_object(temp_path)print(f"Now you don't: {temp_path.exists()}"")
就像冷冻枪一样,这是一个很大的黑魔法,但很有用。它将修补所有文件系统调用,并在内存中创建一个虚构的 FS,因此您的文件操作永远不会离开 RAM。
请注意,使用真实文件进行测试将允许您处理边缘情况,例如在多个分区上具有符号链接和树,这有时会令人惊讶。为正确的工作提供正确的工具等等。
由于我们处于内存领域,因此您可以使用 sqlite
处理数据库:
import sqlite3
connection = sqlite3.connect(':memory:')
cursor = connection.cursor()cursor.execute('CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)')
cursor.execute('INSERT INTO test (name) VALUES ("Alice")')
cursor.execute('INSERT INTO test (name) VALUES ("Bob")')
connection.commit()cursor.execute('SELECT * FROM test')
rows = cursor.fetchall()
print('SQLite in-memory database contents:')
for row in rows:print(row)
这将在不接触磁盘的情况下创建一个数据库,这将是临时的,而且速度非常快。
最后,网络呢?
好吧,网络的工具和应用一样多,你会看到fakker redis
, HTTP API, 中间人拦截,甚至全面的网络控制
不过,您可能会感兴趣的一个是 VCR.py,这是一个 Python 库,可以记录您的 HTTP 请求,并可以将其响应重放给您。
编写一个执行请求的测试,用@pytest.mark.vcr
标记它:
import pytest
import requests@pytest.mark.vcr
def test_single():assert requests.get("http://httpbin.org/get").text == '{"get": true}'
然后,针对真实网络运行一次:
pytest --record-mode=once test_network.py
下次您正常运行测试(移除 --record-mode
), VCR 将为您重播网络响应,并且您不会接触网络。
快照
我已经将一半的测试写入委托给 ChatGPT,然后在其中触发一个断点,这样我就可以窃取调用结果并将它们转换为断言。这是一个很好的技巧,它在许多情况下都有效。
当然,有一种方法可以进一步自动化!
inline-snapshot
, 允许你写占位符,表示你的测试结果应该出现在哪里:
from inline_snapshot import snapshotdef test_something():assert 1548 * 18489 == snapshot() # snapshot() is the placeholder
然后运行测试以记录结果:
pytest --inline-snapshot=create
你会得到用以下值填充的测试:
神奇,你会发现文件自动变成了下面这个:
from inline_snapshot import snapshotdef test_something():assert 1548 * 18489 == snapshot(28620972)
您现在可以检查这些值是否有意义(不要相信它们,您的代码可能有问题),如果它们有效,请将它们提交到 git。下次正常运行它们(移除–inline-snapshot )时,快照中的数据将被透明地使用并据以测试。
您可以使用 --inline-snapshot=update
来更新自动更改的快照, --inline-snapshot=fix
仅更改不再通过的测试,或 --inline-snapshot=review
手动查看更改并批准更改,甚至 --inline-snapshot=disable
返回到原始值,并加快速度或检查快照是否是问题的原因。
评论是交互式的,非常方便:
你可能会问,那些 external()
是什么。如果您不希望在测试代码中使用快照,则可以使用它。数据可能非常大,或者是一些非文本格式,会弄乱您的文件。您可以将测试结果标记为outsource
:
from inline_snapshot import outsource, snapshotdef test_a_big_fat_data():assert [outsource("long text\n" * times) for times in [50, 100, 1000]] == snapshot()
然后,当你用 --inline-snapshot=create
,它会变成这样:
from inline_snapshot import outsource, snapshot, externaldef test_a_big_fat_data():assert [outsource("long text\n" * times) for times in [50, 100, 1000]] == snapshot([external("362ad8374ed6*.txt"),external("5755afea3f8d*.txt"),external("f5a956460453*.txt"),])
文件存储在默认的 pytest 配置目录中,可以使用 --inline-snapshot=trim
删除过时的文件。
我喜欢这个库,因为您可以将快照的值直接放入代码中。我认为对于许多测试来说,它比从文件加载它更有意义。
但它有一个很大的局限性:你只能测试可以使用 repr()
的输出重新创建的东西,所以主要是 Python 内置的,如字符串、整数、列表字典和一些对象,如 datetime。如果你需要更复杂的对象,你就不走运了。
在这种情况下,您可以使用更强大的 pytest-insta
,它允许 pickle
作为序列化格式,因此您可以拍摄几乎所有内容的快照。如果你做不到,你可以创建自己的格式化程序,并使用像 cloudpickle
这样的黑魔法真正将任何怪物变成平面字节。