基础设施代码自动化测试全解析
1. 测试类型选择
在为基础设施代码库选择测试类型时,没有固定的公式可循。最佳做法是从较为简单的测试开始,当有明确需求时,再引入新的测试层和类型。
2. 反模式:反射性测试
低级基础设施测试的一个常见陷阱是编写的测试仅仅是对配置定义的重复表述。例如,下面是一段 Chef 代码片段,用于创建配置文件:
file '/etc/our_app.yml'
owner ourapp
group ourapp
end
以下是之前 Chefspec 单元测试示例中的一段代码:
describe 'creating the configuration file for our_app' do
# ...
it 'gives the file the right attributes' do
expect(chef_run).to create_template('/etc/our_app.yml').with(
user: 'ourapp',
group: 'ourapp'
)
end
end
这个测试只是重复了定义内容。本质上,它是在测试 Chef 开发人员是否正确实现了文件资源,而不是测试我们自己编写的代码。如果习惯编写这类测试,最终会有大量此类测试,并且会浪费大量精力对每个配置进行两次编辑:一次用于定义,一次用于测试。
通常,当需要验证的逻辑存在一定复杂性时,才应该实现测试。对于我们的配置文件示例,如果存在一些复杂逻辑,导致文件可能创建也可能不创建,那么编写简单测试可能是有价值的。例如,
our_app
在大多数环境中可能不需要配置文件,因此我们只在少数需要覆盖默认配置值的环境中创建该文件。在这种情况下,可能需要编写两个单元测试:一个确保文件在应该创建时被创建,另一个确保文件在不应该创建时不被创建。
3. 持续审查测试有效性
最有效的自动化测试机制包括持续审查和改进测试套件。有时,可能需要清理测试、移除测试层或测试组、添加新的测试层或类型、添加、移除或替换工具、改进测试管理方式等。
当生产环境或测试环境出现重大问题时,考虑进行无指责的事后分析。始终应考虑的一种缓解措施是添加、更改或移除测试。以下是一些应考虑改进测试的迹象:
- 修复和维护某些测试所花费的时间超过了它们发现问题所节省的时间。
- 经常在生产环境中发现问题。
- 经常发现阻止重要事件(如发布)的问题。
- 在高级测试中花费过多时间调试和追踪故障。
4. 基础设施单元测试的代码覆盖率
应避免为基础设施单元测试设置代码覆盖率目标。由于配置定义通常相当简单,基础设施代码库可能不像软件代码库那样有大量的单元测试。在软件开发领域,设置单元测试覆盖率目标是一种被过度滥用的做法,这会迫使团队编写和维护无用的测试代码,从而使维护良好的自动化测试变得更加困难。
5. 测试工具
5.1 配置定义测试框架
许多流行的服务器配置自动化工具都有类似的测试框架,例如 Chef 的
chefspec
测试框架。
rspec-puppet
可用于 Puppet,Saltstack 甚至在其发行版中包含了支持单元测试的库。
这些框架使得在不将定义应用到运行中的服务器的情况下,测试部分定义成为可能。通常,配置定义的单元测试使用配置管理工具所使用的语言编写。Puppet 和 Chef 都使用 Ruby 编写,因此 Ruby 单元测试工具
rspec
常用于单元测试清单和食谱。使用相同的语言进行低级测试框架,使得编写测试替身和实现测试设置更加容易。
5.2 高级测试
对于高级测试,所使用的语言不需要与基础设施工具匹配,因为测试不应与工具的内部进行交互。通用的行为驱动开发(BDD)和 UI 测试工具可用于涉及 UI 的高级测试,特别是基于 Web 的 UI。
对于基础设施,验证运行中服务器上的文件和服务状态特别有用。
Serverspec
是另一个基于
rspec
的工具,它添加了连接到服务器并验证它们的功能和库。以下是一个验证
our_app
是否成功安装并运行的
Serverspec
示例:
describe service('our_app') do
it { should be_running }
end
还可以使用这种类型的测试来验证网络配置。以下
Serverspec
在前端 Web 服务器上运行,检查它是否能够连接到应用服务器端口:
describe host('appserver') do
it { should be_reachable.with( :port => 8080 ) }
end
6. 安全连接到服务器运行测试
需要远程登录到服务器进行验证的自动化测试可能存在安全问题。这些测试要么需要硬编码的密码,要么需要 SSH 密钥或类似的机制来授权无人值守登录。
缓解此问题的一种方法是让测试在测试服务器上执行,并将结果推送到中央服务器。这可以与监控相结合,以便服务器可以自我测试,并在失败时触发警报。
另一种方法是为测试服务器实例使用临时凭证。一些云平台在创建新实例时会随机生成凭证,并将其返回给触发创建的脚本。其他平台允许由创建实例的脚本定义凭证。因此,自动化测试可以创建临时服务器实例,并生成随机凭证或接收平台创建的凭证。测试使用这些凭证运行,完成后销毁服务器实例。凭证无需共享或存储,即使被泄露,也不会提供对其他服务器的访问权限。
7. 实施和运行测试
7.1 隔离组件进行测试
为了有效地测试一个组件,在测试期间必须将其与任何依赖项隔离开来。例如,测试 Nginx Web 服务器的配置时,Web 服务器会将请求代理到应用服务器。但我们希望在不启动应用服务器的情况下测试 Web 服务器配置,因为启动应用服务器需要部署应用,而应用又需要数据库服务器,数据库服务器又需要数据模式和数据。这不仅使设置测试变得复杂,而且除了我们要测试的配置之外,还有许多潜在的错误来源。
解决方案是使用存根服务器代替应用服务器。存根服务器是一个简单的进程,它监听与应用服务器相同的端口,并提供测试所需的响应。这个存根可以是一个简单的应用程序,例如 Ruby Sinatra Web 应用,也可以是另一个 Nginx 实例,或者是用基础设施团队喜欢的脚本语言编写的简单 HTTP 服务器。
存根服务器应易于维护和使用,只需要返回特定于我们编写的测试的响应。例如,一个测试可以检查对
/ourapp/home
的请求是否返回 HTTP 200 响应,那么存根服务器就处理这个路径。另一个测试可能检查当应用服务器返回 500 错误时,Nginx 服务器是否返回正确的错误页面,那么存根服务器可能会对
/ourapp/500-error
这样的特殊路径返回 500 错误。还有一个测试可能检查当应用服务器完全关闭时,Nginx 是否能正常处理,这个测试可以在不启动存根服务器的情况下运行。
存根服务器应该能够快速启动,对环境和基础设施的要求简单。这意味着它可以在完全隔离的环境中运行,例如在轻量级容器中,作为更大测试套件的一部分。
7.2 测试替身
模拟对象(Mocks)、伪造对象(Fakes)和存根对象(Stubs)都是“测试替身”的类型。测试替身用于替换被测试组件或服务所需的依赖项,以简化测试。不同的人对这些术语的使用方式可能不同,但 Gerard Meszaros 在他的 xUnit 模式一书中给出的定义很有用。
7.3 重构组件以实现隔离
很多时候,某个特定组件可能不容易隔离。与其他组件的依赖关系可能是硬编码的,或者过于复杂而难以拆分。在设计和构建系统时编写测试的好处之一是,它迫使我们改进设计。难以隔离测试的组件是设计不佳的表现。一个设计良好的系统应该具有清晰且松散耦合的组件。
因此,当遇到难以隔离的组件时,应该修复这个设计。这可能很困难,组件可能需要完全重写,库、工具和应用程序可能需要替换。俗话说,这是一个特性,而不是一个 bug。为了使系统可测试,需要一个干净的设计。
有许多重构系统的策略,重构是一种在重构系统内部设计的过程中,优先保证系统完全正常运行的方法。
8. 管理外部依赖
依赖于自己团队无法管理的服务是很常见的。像 DNS、身份验证服务或电子邮件服务器等基础设施元素和服务可能由其他团队或外部供应商提供。由于以下几个原因,这些服务可能会给自动化测试带来挑战:
- 它们可能无法处理持续测试(更不用说性能测试)产生的负载。
- 它们可能存在可用性问题,从而影响自己的测试。当供应商或团队提供其服务的测试实例时,这种情况尤其常见。
- 可能存在成本或请求限制,使得它们不适合用于持续测试。
在大多数测试中,可以使用测试替身来代替外部服务。只有在自己的系统和代码经过验证后,才应该与外部服务集成。这样可以确保如果测试失败,你知道是外部服务存在问题,还是你与它的集成方式有问题。
应该确保如果外部服务确实失败,能够清楚地识别出问题所在。曾经有一个团队花了一个多星期仔细检查应用程序和基础设施代码,以诊断间歇性测试失败的原因,结果发现是云供应商的 API 达到了请求限制。浪费这么多时间在本可以更快发现的问题上是很令人沮丧的。
与第三方的任何集成,甚至自己服务之间的集成,都应该实现检查和报告机制,以便在出现问题时能够立即发现。这应该通过所有环境的监控和信息展示板来实现。在很多情况下,团队会实施单独的测试和监控检查,以报告与上游服务的连接情况。
9. 测试设置
一致性、可重复性和可再现性对于自动化测试至关重要。行为不一致的测试没有价值,因此自动化测试的一个关键部分是确保环境和数据的一致设置。
对于涉及设置基础设施(例如构建和验证虚拟机)的测试,基础设施自动化本身有助于实现可重复性和一致性。挑战在于状态管理。一个给定的测试对数据有什么假设?它对环境中已经完成的配置有什么假设?
自动化测试的一个基本原则是每个测试应该是独立的,并且应该确保它所需的起始状态。应该能够以任何顺序运行测试,并且单独运行任何测试,始终得到相同的结果。
例如,下面是两个测试示例,第一个测试我们 Web 服务器上 Nginx 的安装,第二个测试主页是否加载预期内容:
describe 'install and configure web server' do
let(:chef_run) { ChefSpec::SoloRunner.converge(nginx_configuration_recipe) }
it 'installs nginx' do
expect(chef_run).to install_package('nginx')
end
end
describe 'home page is working' do
let(:chef_run) { ChefSpec::SoloRunner.converge(home_page_deployment_recipe) }
it 'loads correctly' do
response = Net::HTTP.new('localhost',80).get('/')
expect(response.body).to include('Welcome to our home page')
end
end
乍一看,这个示例似乎合理,但如果单独运行“主页正常工作”的测试,它会失败,因为没有 Web 服务器来响应请求。我们可以确保测试始终按相同顺序运行,但这会使测试套件过于脆弱。如果改变安装和配置 Web 服务器的方式,可能需要对许多其他假设之前测试已运行的测试进行更改。更好的做法是使每个测试自包含,如下例所示:
describe 'install and configure web server' do
let(:chef_run) { ChefSpec::SoloRunner.converge(nginx_configuration_recipe) }
it 'installs nginx' do
expect(chef_run).to install_package('nginx')
end
end
describe 'home page is working' do
let(:chef_run) {
ChefSpec::SoloRunner.converge(nginx_configuration_recipe,
home_page_deployment_recipe)
}
it 'loads correctly' do
response = Net::HTTP.new('localhost',80).get('/')
expect(response.body).to include('Welcome to our home page')
end
end
在这个示例中,第二个测试的依赖关系很明确,一眼就能看出它依赖于 Nginx 配置。它也是自包含的,这些测试中的任何一个都可以单独运行,或者以任何顺序运行,每次都能得到相同的结果。
10. 管理测试数据
一些测试依赖于数据,特别是那些测试应用程序或服务的测试。例如,为了测试监控服务,可能会创建一个监控服务器的测试实例。各种测试可能会向该实例添加和移除警报,并模拟触发警报的情况。这需要仔细考虑和处理,以确保测试可以以任何顺序重复运行。
例如,我们可能编写一个测试,添加一个警报,然后验证它是否在系统中。如果在同一个测试实例上运行这个测试两次,它可能会尝试第二次添加相同的警报。根据监控服务的不同,添加重复警报的尝试可能会失败,或者测试可能会因为找到两个同名的警报而失败,又或者第二次添加警报的尝试实际上可能不起作用,但验证却发现了第一次添加的警报,从而不会告知我们失败情况。
因此,测试数据应遵循以下规则:
- 每个测试应该创建它所需的数据。
- 每个测试应该在之后清理其数据,或者每次运行时创建唯一的数据。
- 测试永远不应该对开始时数据的存在与否做出假设。
不可变服务器有助于确保干净和一致的测试环境。持久的测试环境往往会随着时间推移而发生变化,从而不再与生产环境一致。
11. 监控与测试
监控和自动化测试有很多共同点。两者都对基础设施及其服务的状态进行断言,并且当断言失败时都会提醒团队存在问题。将这些关注点结合起来,或者至少进行集成,可能会非常有效。可以考虑重用自动化测试来验证系统在生产环境中是否正常工作。不过,有一些注意事项:
- 许多自动化测试有副作用,并且/或者需要特殊设置,这在生产环境中可能具有破坏性。
- 许多测试与生产环境监控无关。监控检查由于操作状态变化可能发生的问题,而测试验证代码更改是否有害。一旦代码更改并应用到生产环境,重新运行功能测试可能毫无意义。
- 只有当在测试和监控之间重用代码能使工作更轻松时,才是有用的。在很多情况下,试图强行将测试工具用于生产环境监控可能得不偿失。
12. 总结
自动化测试可能是基础设施即代码中最具挑战性的方面,但对于支持可靠且适应性强的基础设施来说也是最重要的。团队应该养成习惯并建立流程,将测试作为基础设施的核心部分常规化。通过合理选择测试类型、持续审查测试有效性、使用合适的测试工具、安全地连接服务器、隔离组件、管理外部依赖、正确设置测试和数据,以及合理结合监控与测试等方法,可以提高基础设施代码的质量和可靠性。
基础设施代码自动化测试全解析
13. 测试流程总结与优化建议
为了更清晰地展示基础设施代码自动化测试的整体流程,我们可以用 mermaid 流程图来表示:
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px;
A([开始]):::startend --> B(选择简单测试开始):::process
B --> C{是否有新需求?}:::decision
C -->|是| D(引入新测试层和类型):::process
C -->|否| E(持续审查测试有效性):::process
D --> E
E --> F{是否需要改进测试?}:::decision
F -->|是| G(清理、添加或替换测试相关元素):::process
F -->|否| H(进行测试实施):::process
G --> H
H --> I(隔离组件进行测试):::process
I --> J(管理外部依赖):::process
J --> K(设置测试环境和数据):::process
K --> L(运行测试):::process
L --> M{测试是否通过?}:::decision
M -->|是| N(结合监控验证生产环境):::process
M -->|否| O(重新审查和改进测试):::process
O --> E
N --> P([结束]):::startend
根据上述流程,我们可以总结出以下优化建议:
-
测试规划阶段
:从简单测试入手,避免一开始就陷入复杂的测试体系。根据实际需求逐步引入新的测试层和类型,确保测试的针对性和有效性。
-
测试执行阶段
:严格遵循隔离组件、管理外部依赖和设置一致测试环境的原则,提高测试的可重复性和准确性。
-
持续改进阶段
:定期审查测试有效性,根据测试结果和实际生产环境的反馈,及时调整测试策略和方法。
14. 不同测试场景下的工具选择
不同的测试场景需要选择合适的测试工具,以下是一个简单的表格总结:
| 测试场景 | 适用工具 | 工具特点 |
| — | — | — |
| 配置定义单元测试 | chefspec、rspec - puppet、Saltstack 内置库 | 可在不应用到运行中服务器的情况下测试部分定义,使用配置管理工具相同语言,便于编写测试替身和设置测试 |
| 高级测试(涉及 UI) | 通用 BDD 和 UI 测试工具 | 不依赖基础设施工具内部,适用于基于 Web 的 UI 测试 |
| 验证服务器状态和网络配置 | Serverspec | 基于 rspec,可连接服务器并验证文件和服务状态以及网络连接性 |
15. 测试替身的使用技巧
测试替身(如 Mocks、Fakes、Stubs)在隔离测试组件时非常有用,以下是一些使用技巧:
-
选择合适的替身类型
:
-
Stubs(存根)
:适用于简单地返回预定义响应,如上述 Nginx 测试中使用的存根服务器,只需要处理特定路径的请求并返回相应状态码。
-
Mocks(模拟对象)
:用于验证方法调用和参数传递,当需要验证某个组件是否正确调用了其他组件的方法时,可以使用 Mocks。
-
Fakes(伪造对象)
:提供一个简化的实现,用于替代复杂的依赖项,例如在测试数据库操作时,可以使用 Fakes 来模拟数据库的行为。
-
保持替身的简单性
:测试替身只需要满足测试的基本需求,避免过于复杂的实现。例如,存根服务器只需要处理测试所需的特定路径和请求,不需要具备完整的应用服务器功能。
-
易于维护和修改
:随着被测试组件的变化,测试替身可能需要相应调整。因此,替身的代码应该易于理解和修改。
16. 应对外部依赖问题的策略
当面临外部依赖带来的测试挑战时,可以采取以下策略:
-
使用测试替身
:如前文所述,在大多数测试中使用测试替身代替外部服务,减少对外部服务的依赖。
-
定期集成测试
:在完成自身系统和代码的验证后,定期进行与外部服务的集成测试,及时发现和解决集成问题。
-
监控外部服务状态
:通过监控工具实时了解外部服务的状态,当外部服务出现问题时,能够及时调整测试策略或通知相关团队。
-
设置备用服务或替代方案
:对于一些关键的外部服务,可以考虑设置备用服务或替代方案,以确保测试的连续性。
17. 自动化测试的未来发展趋势
随着技术的不断发展,基础设施代码自动化测试也呈现出一些未来的发展趋势:
-
人工智能和机器学习的应用
:利用人工智能和机器学习算法来分析测试数据,预测潜在的问题,优化测试用例的选择和执行顺序,提高测试效率和准确性。
-
容器化和微服务架构下的测试
:随着容器化和微服务架构的广泛应用,测试将更加注重组件之间的交互和集成,需要开发新的测试方法和工具来适应这种变化。
-
云原生测试
:云原生技术的发展使得基础设施更加灵活和可扩展,测试也将更加关注云环境下的性能、安全性和可靠性。
-
持续测试与 DevOps 的深度融合
:持续测试将成为 DevOps 流程中不可或缺的一部分,与开发、部署等环节更加紧密地结合,实现更快的反馈和更高效的交付。
18. 总结与展望
自动化测试在基础设施即代码领域扮演着至关重要的角色。通过合理的测试规划、选择合适的工具、正确使用测试替身、有效管理外部依赖以及遵循测试设置和数据管理的原则,我们可以构建出可靠、高效的自动化测试体系。
未来,随着技术的不断创新和发展,自动化测试将面临新的挑战和机遇。我们需要不断学习和探索新的测试方法和技术,以适应不断变化的基础设施和业务需求。同时,持续改进和优化测试流程,将测试融入到整个软件开发和运维的生命周期中,为构建更加稳定、可靠和高效的基础设施提供有力保障。
总之,自动化测试是基础设施建设中不可或缺的一环,只有不断提升测试能力和水平,才能在快速发展的技术浪潮中立于不败之地。
超级会员免费看
8968

被折叠的 条评论
为什么被折叠?



