TensorFlow 测试最佳实践

这些是在 TensorFlow 仓库 中测试代码的推荐实践。

开始之前

在您为 TensorFlow 项目贡献源代码之前,请查看项目 GitHub 仓库中的 CONTRIBUTING.md 文件。(例如,请查看 核心 TensorFlow 仓库的 CONTRIBUTING.md 文件。)所有代码贡献者都需要签署 贡献者许可协议 (CLA)。

一般原则

仅依赖于您在 BUILD 规则中使用的内容

TensorFlow 是一个大型库,在为其子模块编写单元测试时,依赖于完整包是一种常见的做法。但是,这会禁用 bazel 的基于依赖项的分析。这意味着持续集成系统无法智能地消除预提交/后提交运行中不相关的测试。如果您在 BUILD 文件中仅依赖于您正在测试的子模块,您将为所有 TensorFlow 开发人员节省时间,并节省大量宝贵的计算能力。

但是,修改您的构建依赖项以省略完整的 TF 目标会对您可以在 Python 代码中导入的内容带来一些限制。您将无法再在单元测试中使用 import tensorflow as tf 语句。但这是一种值得的权衡,因为它可以节省所有开发人员运行数千个不必要的测试的时间。

所有代码都应该有单元测试

对于您编写的任何代码,您也应该编写其单元测试。如果您编写了一个新文件 foo.py,您应该将其单元测试放在 foo_test.py 中,并在同一更改中提交。目标是为所有代码实现 >90% 的增量测试覆盖率。

避免在 TF 中使用原生 bazel 测试规则

TF 在运行测试时有很多细微之处。我们一直在努力将所有这些复杂性隐藏在我们的 bazel 宏中。为了避免处理这些问题,请使用以下方法代替原生测试规则。请注意,所有这些都在 tensorflow/tensorflow.bzl 中定义。对于 CC 测试,请使用 tf_cc_testtf_gpu_cc_testtf_gpu_only_cc_test。对于 Python 测试,请使用 tf_py_testgpu_py_test。如果您需要非常接近原生 py_test 规则的内容,请使用 tensorflow.bzl 中定义的规则。您只需要在 BUILD 文件顶部添加以下行:load(“tensorflow/tensorflow.bzl”, “py_test”)

注意测试执行的位置

当您编写测试时,我们的测试基础设施可以根据您的编写方式,在 CPU、GPU 和加速器上运行您的测试。我们有在 Linux、macos、windows 上运行的自动化测试,这些系统有或没有 GPU。您只需要选择上面列出的宏之一,然后使用标签来限制它们执行的位置。

  • manual 标签将排除您的测试在任何地方运行。这包括使用 bazel test tensorflow/… 等模式的手动测试执行。

  • no_oss 将排除您的测试在官方 TF OSS 测试基础设施中运行。

  • no_macno_windows 标签可用于从相关的操作系统测试套件中排除您的测试。

  • no_gpu 标签可用于从 GPU 测试套件中排除您的测试。

验证测试在预期的测试套件中运行

TF 有相当多的测试套件。有时,它们可能难以设置。可能存在导致您的测试从持续构建中省略的不同问题。因此,您应该验证您的测试是否按预期执行。为此

  • 等待您的拉取请求 (PR) 上的预提交运行完成。
  • 滚动到您的 PR 底部以查看状态检查。
  • 单击任何 Kokoro 检查右侧的“详细信息”链接。
  • 检查“目标”列表以找到您新添加的目标。

每个类/单元都应该有自己的单元测试文件

单独的测试类有助于我们更好地隔离故障和资源。它们会导致更短、更易于阅读的测试文件。因此,所有 Python 文件都应该至少有一个相应的测试文件(对于每个 foo.py,它应该有 foo_test.py)。对于更详细的测试,例如需要不同设置的集成测试,添加更多测试文件是可以的。

速度和运行时间

尽可能少地使用分片

请考虑以下方法代替分片

  • 使您的测试更小
  • 如果上述方法不可行,请将测试拆分

分片有助于减少测试的整体延迟,但可以通过将测试分解为更小的目标来实现相同的效果。拆分测试使我们能够更精细地控制每个测试,最大限度地减少不必要的预提交运行,并减少由于测试用例行为不当而导致构建 cop 禁用整个目标造成的覆盖率损失。此外,分片会产生一些不那么明显的隐藏成本,例如为所有分片运行所有测试初始化代码。这个问题已被基础设施团队升级为一个创建额外负载的来源。

更小的测试更好

您的测试运行得越快,人们就越有可能运行您的测试。您的测试多运行一秒钟,可能会导致开发人员和我们的基础设施花费额外数小时运行您的测试。尝试使您的测试在 30 秒内运行(在非优化模式下!),并使它们变小。只有在万不得已的情况下才将您的测试标记为中等。基础设施不会将任何大型测试作为预提交或后提交运行!因此,只有在您要安排其运行位置时才编写大型测试。以下是一些使测试运行更快的技巧

  • 在测试中减少训练迭代次数
  • 考虑使用依赖项注入将被测系统的繁重依赖项替换为简单的模拟。
  • 考虑在单元测试中使用更小的输入数据
  • 如果其他方法都不起作用,请尝试拆分您的测试文件。

测试时间应以测试大小超时的二分之一为目标,以避免出现错误

对于使用 bazel 测试目标,小型测试的超时时间为 1 分钟。中等测试的超时时间为 5 分钟。大型测试不会由 TensorFlow 测试基础设施执行。但是,许多测试在执行时间上并不确定。由于各种原因,您的测试可能会偶尔花费更多时间。而且,如果您将平均运行时间为 50 秒的测试标记为小型测试,那么如果该测试在具有旧 CPU 的机器上调度,则您的测试将出现故障。因此,小型测试的平均运行时间目标为 30 秒。中等测试的平均运行时间目标为 2 分钟 30 秒。

减少样本数量并提高训练容差

运行缓慢的测试会阻碍贡献者。在测试中运行训练可能非常缓慢。最好使用更高的容差,以便能够在测试中使用更少的样本,从而保持测试速度足够快(最大 2.5 分钟)。

消除不确定性和故障

编写确定性测试

单元测试应始终是确定性的。在 TAP 和 guitar 上运行的所有测试都应该每次以相同的方式运行,除非有代码更改影响了它们。为了确保这一点,请考虑以下几点。

始终为任何随机性来源设置种子

任何随机数生成器或任何其他随机性来源都可能导致故障。因此,必须为这些来源中的每一个设置种子。除了减少测试故障外,这还可以使所有测试可重复。在 TF 测试中可能需要设置的一些种子的不同设置方法如下

# Python RNG
import random
random.seed(42)

# Numpy RNG
import numpy as np
np.random.seed(42)

# TF RNG
from tensorflow.python.framework import random_seed
random_seed.set_seed(42)

避免在多线程测试中使用 sleep

在测试中使用 sleep 函数可能是导致故障的主要原因。尤其是在使用多个线程时,使用 sleep 来等待另一个线程永远不会是确定性的。这是因为系统无法保证不同线程或进程的执行顺序。因此,最好使用确定性的同步结构,例如互斥锁。

检查测试是否出现故障

故障会导致 buildcops 和开发人员损失大量时间。它们难以检测,也难以调试。尽管有自动系统可以检测故障,但它们需要积累数百次测试运行才能准确地将测试列入黑名单。即使它们检测到,它们也会将您的测试列入黑名单,并且测试覆盖率会丢失。因此,测试作者在编写测试时应该检查他们的测试是否出现故障。这可以通过使用以下标志运行测试来轻松完成:--runs_per_test=1000

使用 TensorFlowTestCase

TensorFlowTestCase 会采取必要的预防措施,例如为所有用于减少故障的随机数生成器设置种子。随着我们发现和修复更多故障来源,这些来源都将添加到 TensorFlowTestCase 中。因此,您应该在为 tensorflow 编写测试时使用 TensorFlowTestCase。TensorFlowTestCase 在此处定义:tensorflow/python/framework/test_util.py

编写封闭测试

封闭测试不需要任何外部资源。它们包含了它们需要的一切,并且它们只启动它们可能需要的任何伪造服务。除了您的测试之外的任何服务都是非确定性的来源。即使其他服务的可用性为 99%,网络也可能出现故障,rpc 响应也可能延迟,您最终可能会收到无法解释的错误消息。外部服务可能是,但不限于 GCS、S3 或任何网站。