• 使用 Python 进行测试(7)...until you make it


    在这里插入图片描述

    总结

    我很懒,我想用最少的行动实现目标,例如生成测试数据。我们可以:

    使用随机种子保证数据的一致性。

    >>> 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 os
    
    with Patcher() as patcher:
        fs = patcher.fs
        temp_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 snapshot
    
    def 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 Locale
        from mimesis import Generic
    
        class FakeProvider:
            def __init__(self, provider):
                self.provider = provider
                self.count = 1
    
            def __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 wrapper
    
            def __call__(self, count=1):
                self.count = count
                return self
    
            def 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 = 1
    
        class 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)
    
            @property
            def fr(self):
                return self.get_factory(locale="fr")
    
            @property
            def 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 time
        right_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 os
    
    with Patcher() as patcher:
        fs = patcher.fs
        temp_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 snapshot
    
    def test_something():
        assert 1548 * 18489 == snapshot() # snapshot() is the placeholder
    

    然后运行测试以记录结果:

    pytest --inline-snapshot=create
    

    你会得到用以下值填充的测试:

    神奇,你会发现文件自动变成了下面这个:

    from inline_snapshot import snapshot
    
    def 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, snapshot
    
    def 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, external
    
    def 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 这样的黑魔法真正将任何怪物变成平面字节。

  • 相关阅读:
    devops-4:Jenkins基于k8s cloud和docker cloud动态增减节点
    数据治理-GDPR准则
    大数据套件初识
    基于JAVA,SpringBoot,Vue,UniAPP智能停车场小程序管理系统设计
    java计算机毕业设计课堂互动应答系统mp4源码+mysql数据库+系统+lw文档+部署
    gulimall基础篇回顾Day-11
    中国传统节日春节网页HTML代码 春节大学生网页设计制作成品下载 学生网页课程设计期末作业下载 DW春节节日网页作业代码下载
    x86游戏逆向之实战游戏线程发包与普通发包的逆向
    拼多多分类ID搜索商品数据分析接口(商品列表数据,商品销量数据,商品详情数据)代码对接教程
    字符串常用方法 --- 字符串对象的属性 与 字符串对象的方法(上)
  • 原文地址:https://blog.csdn.net/qq_41068877/article/details/139870391