以下是针对 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_test、tf_gpu_cc_test 或 tf_gpu_only_cc_test。对于 Python 测试,请使用 tf_py_test 或 gpu_py_test。如果你确实需要非常接近原生 py_test 规则的内容,请使用定义在 tensorflow.bzl 中的版本。你只需在 BUILD 文件顶部添加以下行:load(“tensorflow/tensorflow.bzl”, “py_test”)
了解测试的执行位置
当你编写测试时,如果编写得当,我们的测试基础架构可以负责在 CPU、GPU 和加速器上运行你的测试。我们拥有在 Linux、macOS 和 Windows 上运行的自动化测试,这些系统有的配备 GPU,有的则没有。你只需选择上述宏之一,然后使用标签 (tags) 来限制它们的执行位置。
manual标签将排除你的测试在任何地方运行。这包括使用bazel test tensorflow/…等模式的手动测试执行。no_oss标签将排除你的测试在官方 TF OSS 测试基础设施中运行。no_mac或no_windows标签可用于将你的测试从相关的操作系统测试套件中排除。no_gpu标签可用于将你的测试从 GPU 测试套件中排除。
验证测试是否在预期的测试套件中运行
TF 有相当多的测试套件。有时,设置它们可能会令人困惑。可能存在各种原因导致你的测试被持续构建所忽略。因此,你应该验证你的测试是否按预期执行。为此,请执行以下步骤:
- 等待你的 Pull Request (PR) 中的提交前检查 (presubmits) 完成运行。
- 滚动到 PR 底部查看状态检查。
- 点击任何 Kokoro 检查右侧的“Details”链接。
- 查看“Targets”列表以找到你新添加的目标。
每个类/单元应拥有自己的单元测试文件
独立的测试类有助于我们更好地隔离故障和资源。它们使得测试文件更短、更易于阅读。因此,你所有的 Python 文件都应该至少有一个对应的测试文件(对于每个 foo.py,都应有 foo_test.py)。对于更复杂的测试(例如需要不同设置的集成测试),添加更多的测试文件是可以的。
速度和运行时间
应尽量减少使用分片 (sharding)
除了分片,请考虑:
- 使你的测试更小
- 如果上述方法不可行,请拆分测试
分片有助于降低测试的总体延迟,但通过将测试拆分为更小的目标也可以达到同样的效果。拆分测试使我们能够更精细地控制每个测试,最大限度地减少不必要的提交前运行,并降低因测试用例表现不佳而导致构建管理员 (buildcop) 禁用整个目标带来的覆盖率损失。此外,分片会产生不那么明显的隐性成本,例如为所有分片运行所有测试初始化代码。基础设施团队已向我们反馈,此问题会造成额外的负载。
越小的测试越好
你的测试运行得越快,人们就越愿意运行你的测试。测试增加一秒钟,累积起来就是开发人员和我们的基础设施运行该测试时耗费的数小时额外时间。尝试让你的测试在 30 秒内运行完毕(在非优化模式下!),并保持其精简。仅在万不得已的情况下才将测试标记为 medium(中等)。基础设施不会运行任何 large(大型)测试作为提交前或提交后检查!因此,仅当你安排好它在何处运行时,才编写大型测试。以下是加快测试运行速度的一些技巧:
- 在测试中减少训练迭代次数。
- 考虑使用依赖注入,用简单的假对象 (fakes) 替换被测系统的繁重依赖。
- 考虑在单元测试中使用更小的输入数据。
- 如果其他方法都无效,请尝试拆分测试文件。
测试时间应设定为测试规模超时时间的一半,以避免出现偶发性失败 (flakes)
使用 bazel 测试目标时,小型测试的超时时间为 1 分钟。中型测试的超时时间为 5 分钟。大型测试则根本不会由 TensorFlow 测试基础设施执行。然而,许多测试的运行时间是不确定的。由于各种原因,你的测试有时可能会花费更多时间。如果你将一个平均运行 50 秒的测试标记为 small(小型),那么如果它被调度到一台老旧 CPU 的机器上,它就可能会出现偶发性失败。因此,小型测试的平均运行时间目标应为 30 秒。中型测试的平均运行时间目标应为 2 分 30 秒。
减少样本数量并增加训练容差
缓慢运行的测试会阻碍贡献者。在测试中进行训练可能非常缓慢。建议使用较高的容差,以便在测试中使用更少的样本,从而保持测试足够快(最多 2.5 分钟)。
消除非确定性和偶发性失败
编写确定性的测试
单元测试应始终是确定性的。所有在 TAP 和 guitar 上运行的测试,在没有影响它们的代码更改的情况下,每次都应以相同的方式运行。为确保这一点,请考虑以下几点:
始终为任何随机源设置种子 (seed)
任何随机数生成器或任何其他随机源都可能导致测试出现偶发性失败。因此,必须为这些随机源设置种子。除了减少测试的不稳定性外,这还使所有测试可复现。在 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 等待另一个线程永远不会是确定性的。这是因为系统无法保证不同线程或进程的执行顺序。因此,建议优先使用互斥锁等确定性的同步结构。
检查测试是否会出现偶发性失败
偶发性失败会导致构建管理员和开发人员浪费大量时间。它们难以检测,也难以调试。尽管有自动系统来检测不稳定性,但它们需要积累数百次测试运行才能准确地将测试列入黑名单。即使检测到了,它们也会屏蔽你的测试,导致测试覆盖率丢失。因此,测试编写者在编写测试时应检查其测试是否会偶发性失败。可以通过使用以下标志运行测试来轻松完成此操作:--runs_per_test=1000
使用 TensorFlowTestCase
TensorFlowTestCase 会采取必要的预防措施,例如为所有随机数生成器设置种子,以尽可能减少不稳定性。随着我们发现并修复更多导致不稳定的来源,这些修复都将添加到 TensorFlowTestCase 中。因此,在为 TensorFlow 编写测试时,你应该使用 TensorFlowTestCase。TensorFlowTestCase 定义在此处:tensorflow/python/framework/test_util.py
编写自包含 (hermetic) 的测试
自包含的测试不需要任何外部资源。它们打包了所需的一切,并且只会启动可能需要的任何模拟服务。除测试本身之外的任何服务都是非确定性的来源。即使其他服务的可用性达到 99%,网络也可能波动,RPC 响应可能会延迟,最终你可能会得到莫名其妙的错误消息。外部服务可能包括(但不限于)GCS、S3 或任何网站。