【问题标题】:How do I speed up my GitHub PyTest executions?如何加快我的 GitHub PyTest 执行速度?
【发布时间】:2022-12-23 04:08:14
【问题描述】:

我有一个包含数百个测试的存储库,这些测试到目前为止已经足够快了,但是随着我们继续增长代码库,我担心它会变得太慢以至于我的团队会陷入等待 CI 运行完成的困境。

我可以做些什么来加快速度并使我的测试在短期和长期都更快?

我需要考虑:

  1. 可扩展性
  2. 费用
  3. 推出

【问题讨论】:

    标签: performance pytest github-actions


    【解决方案1】:

    我们可以使用horizontal and vertical scaling 加速测试运行。为此,我们需要确保并行测试安全。我们还有其他一些 PyTest 问题,我们必须解决这些问题才能完成此任务。我们还可以聪明地为难以实现并行安全的测试采用并行化。

    让我们深入挖掘。

    ⚖️ 并行安全

    过去的测试可能被编写为假定串行执行,即数据库状态在测试运行之前以某种方式存在。这意味着不同的执行顺序可能会不确定地开始失败。您将必须确保每个测试都创建专门针对您的测试的数据库状态,确保设置所有必要的对象,并(可选)在测试完成后拆除这些对象。夹具将成为您的朋友,因为它们可用于创建必要的数据库状态并在之后进行清理.

    串行执行中的一种反模式可以是基于数据库中行数的断言。 IE。:

    def test_1() -> None: 
      create_some_rows_in_db()
      assert get_rows_in_db() == 1
    
    def test_2() -> None: 
      create_some_more_rows_in_db()
      assert get_rows_in_db() == 2
    

    如果我们以不同的顺序运行这些测试,它们就会失败。相反,我们需要在数据库中创建与我们的测试会话完全对应的行,同样,我们需要从数据库中获取仅用于此测试会话的行。

    def test_1() -> None: 
      scope=uuid4()
      create_some_rows_in_db(scope=scope)
      assert get_rows_in_db(scope=scope) == 1
    
    def test_2() -> None: 
      scope=uuid4()
      create_some_more_rows_in_db(scope=scope)
      assert get_rows_in_db(scope=scope) == 1
    

    ? 一致订购

    有两种方式可以破坏测试顺序:测试名称可能会更改,并且默认情况下测试顺序不按名称排序。

    如果您在参数化测试中派生像 UUID 这样的值,这些值在测试运行之间会发生变化,这意味着测试本身的名称会发生​​变化。这意味着当并行运行测试时,它们的名称将与PyTest will fail to collect不同。幸运的是,删除在运行之间更改的参数化参数的创建很简单。

    具体来说,如果您最初的测试看起来像:

    @pytest.mark.parametrize("my_arg,...", [(uuid4(), ...), (uuid4(), ...)])
    def test_some_code(my_arg: uuid4, ...) -> None:
      assert my_arg is not None
    

    然后您将需要更改它以派生函数内部的参数。

    @pytest.mark.parametrize("...", [(...),])
    def test_some_code(...) -> None:
      my_arg = uuid4()
      assert my_arg is not None
    

    接下来,我们还需要patch the collection order 参数化测试,这意味着我们将以下内容添加到我们的conftest.py

    def pytest_collection_modifyitems(items: list[pytest.Item]) -> None:
        def param_part(item: pytest.Item) -> str:
            # find the start of the parameter part in the nodeid
            index = item.nodeid.find("[")
            if index > 0:
                # sort by parameter name
                parameter_index = item.nodeid.index("[")
                return item.name[parameter_index:]
    
            # for all other cases, sort by node id as usual
            return item.nodeid
    
        # re-order the items using the param_part function as key
        items[:] = sorted(items, key=param_part)
    

    ↕️垂直缩放

    接下来,我们可以run our tests in parallel in a single GitHub Action Runner using xdist。这个包的安装和配置很容易完成,GitHub Action Runners 默认有 2 个 CPU 可供我们利用。

    将来,它将运行这些测试的机器的will be possible to scale up the size。目前,2 核为我们提供了不错的加速。然而,我们可以走得更远。

    ↔️ 水平缩放

    垂直缩放提供了不错的加速,但我们真正想要完成的是将我们的测试工作拆分到多个运行器上。幸运的是,PyTest-split 出色地为我们完成了这项工作。

    here 所示,在您的工作流 .yml 中启用它非常简单,当与 GitHub Matrix Actions 结合使用时,我们可以告诉 PyTest 并行运行所有可用测试的一小部分。

    这意味着每个跑步者都会接受所有测试,但选择运行测试的拆分,从而将其余部分留给其他运行程序执行。现在在 matrix 参数中添加或删除跑步者的数量是微不足道的,我们可以增加或减少并行执行的数量以匹配我们的 SLA 和预算。

    我还建议使用 PyTest-split 的 test_duration 功能,这样你就可以调整每个运行器中测试的分配,使它们均匀平衡。

    说到预算...

    ? 取消上一个

    如果我们想注意成本,取消之前提交的运行是有利的,如果它们仍在执行,如here所示。这将使我们从现在每个提交的更昂贵的执行成本中收回成本。我建议您从一小部分工人开始,看看您愿意承担多少成本,然后根据需要添加以满足您的周转时间需求。

    ? 收养

    假设我们没有时间或资源进行迁移全部我们的测试是并行安全的。如果我们想为我们的开发人员提供一个逃生舱口,以防他们每次都想连续运行测试,我们可以使用 clever marking 的测试使用 pytest.mark.serial 来确保某些测试每次都以相同的顺序运行时间。这意味着我们将需要配置我们的 GitHub 工作流 .yml 以独立于我们的 Matrix 运行来执行这些测试,但这很容易实现。

    ...
    # Serial Execution 
    pytest -vv -x -n 0 -m "serial"
    
    ...
    # Parallel Execution
    pytest -vv -n auto -m "not serial" --splits PARALLELISM --group ${{ matrix.group }}
    

    ⚡️总结

    我们现在有并行安全测试,可以在工程资源允许的情况下随着时间的推移采用,具有垂直和水平扩展能力,同时注意预算。

    干杯

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-11-02
      • 2023-03-08
      相关资源
      最近更新 更多