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 和加速器上运行测试。我们已在带或不带 GPU 的 Linux、MacOS、Windows 系统上实现测试自动化。您只需要选择上面列出的宏之一,然后使用标签来限制它们的执行位置。

  • manual 标记将阻止您的测试在任何地方运行,包括使用 bazel test tensorflow/… 等模式手动执行的测试。

  • no_oss 将阻止您的测试在正式的 TF OSS 测试基础架构中运行。

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

  • no_gpu 标记可用于将测试从 GPU 测试套件中排除。

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

TF 有很多测试套件。有时,它们的设置可能会让人感到困惑。可能存在不同的问题,导致您的测试在连续构建中被忽略。因此,您应当验证测试是否按预期执行。为此,请执行以下操作:

  • 等待您在拉取请求 (PR) 上的预提交完成。
  • 滚动到 PR 的底部来执行状态检查。
  • 点击任意 Kokoro 检查右侧的“Details”链接。
  • 检查“Targets”列表以找到您新添加的目标。

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

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

速度和运行时间

应尽可能少地使用分片

作为分片的替代,请考虑:

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

分片有助于减少测试的总体延迟时间,但可通过将测试分解为更小的目标来实现同样的目的。拆分测试为我们提供了对每个测试的更精细控制,这最大程度地减少了不必要的预提交运行,并降低了 buildcop 因测试用例行为不当而停用整个目标所引起的覆盖率损失。此外,分片会产生不太明显的隐藏成本,例如为全部分片运行所有测试初始化​​代码。基础架构团队已经向我们提出这个问题,认为它会造成额外的负担。

测试越小越好

您的测试运行得越快,人们越可能运行您的测试。您的测试多花一秒钟的时间,就可能导致开发者和我们的基础架构额外花费数小时的时间来运行您的测试。尝试使您的测试在 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 来等待另一个线程将永远不是确定性的。这是由于系统无法保证不同线程或进程的执行顺序。因此,最好使用确定性同步结构,例如互斥体。

检查测试是否不可靠

不可靠测试会导致 buildcop 和开发者损失很多时间。它们很难检测,也很难调试。即使有自动化系统来检测不可靠性,它们也需要积累数百次的测试运行,才能准确地将测试加入拒绝列表。即使它们被检测到,也会将您的测试加入拒绝列表,并且测试覆盖率也会丢失。因此,测试作者在编写测试时应检查其测试是否不可靠。这可以通过使用以下标记运行测试来轻松完成:--runs_per_test=1000

使用 TensorFlowTestCase

TensorFlowTestCase 会采取必要的预防措施,例如播种使用的所有随机数发生器,以尽可能减少不可靠。随着我们发现并修复更多不可靠来源,所有这些都将添加到 TensorFlowTestCase 中。因此,在为 Tensorflow 编写测试时,应当使用 TensorFlowTestCase。此处定义了 TensorFlowTestCase:tensorflow/python/framework/test_util.py

编写密封测试

密封测试不需要任何外部资源。这些测试中打包了所需的一切,它们仅会启动可能需要的任何虚假服务。除了您的测试之外的任何服务都是不确定性的来源。即使其他服务的可用性达到 99%,网络也可能不可靠,RPC 响应也可能会延迟,最终您可能会收到一个令人费解的错误消息。外部服务包括但不限于 GCS、S3 或任何网站。