17、基础设施自动化测试全解析

基础设施自动化测试全解析

1. 测试类型选择与反模式

在基础设施代码库的测试中,并没有固定的公式来确定应该使用哪种类型的测试。最佳的做法是从相对简单的测试开始,然后在有明确需求时引入新的测试层次和类型。

1.1 反模式:反射性测试

低级基础设施测试的一个陷阱是编写仅仅重复配置定义的测试。例如,以下是一个 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 在大多数环境中可能不需要配置文件,仅在少数需要覆盖默认配置值的环境中创建文件。在这种情况下,可能需要两个单元测试:一个确保文件在应该创建时被创建,另一个确保文件在不应该创建时不被创建。

1.2 持续审查测试有效性

最有效的自动化测试机制需要持续审查和改进测试套件。有时需要清理测试、移除测试层或组、添加新的测试层或类型、添加、移除或替换工具、改进测试管理方式等。

当生产环境甚至测试环境出现重大问题时,考虑进行无责事后分析。始终应考虑的缓解措施之一是添加、更改或移除测试。以下是一些需要考虑改进测试的迹象:
- 如果修复和维护某些测试所花费的时间超过了它们发现问题所节省的时间。
- 是否经常在生产环境中发现问题。
- 是否经常发现阻止重要事件(如发布)的问题。
- 是否在高级测试的调试和跟踪失败上花费过多时间。

1.3 基础设施单元测试的代码覆盖率

应避免为基础设施单元测试设置代码覆盖率目标。由于配置定义通常比较简单,基础设施代码库的单元测试可能不如软件代码库多。设置单元测试覆盖率目标(这在软件开发领域是一种被滥用的做法)会迫使团队编写和维护无用的测试代码,使维护良好的自动化测试变得更加困难。

2. 测试工具

2.1 配置定义测试框架

本章使用了多个 Chefspec 测试示例。大多数流行的服务器配置自动化工具都有类似的框架,如 rspec-puppet 。Saltstack 甚至在其发行版中包含了支持单元测试的库。

这些框架可以在不将定义应用到运行中的服务器的情况下测试部分定义。通常,配置定义的单元测试使用配置管理工具所使用的语言编写。Puppet 和 Chef 都使用 Ruby 编写,因此 Ruby 单元测试工具 rspec 常用于单元测试清单和食谱。使用相同的语言进行低级测试框架可以更轻松地编写测试替身和实现测试设置。

2.2 高级测试

对于高级测试,所使用的语言不需要与基础设施工具匹配,因为测试不应与工具的内部进行交互。通用的行为驱动开发(BDD)和 UI 测试工具可用于涉及 UI(尤其是基于 Web 的 UI)的高级测试。

对于基础设施,验证运行中服务器上的文件和服务状态特别有用。 Serverspec 是另一个基于 rspec 的工具,它添加了连接到服务器并验证它们的功能和库。

以下是一个验证 ourapp 是否成功安装并在服务器上运行的 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

2.3 安全连接到服务器运行测试

需要远程登录服务器进行验证的自动化测试可能存在安全问题。这些测试要么需要硬编码密码,要么需要 SSH 密钥或类似机制来授权无人值守登录。

缓解此问题的一种方法是让测试在测试服务器上执行,并将结果推送到中央服务器。这可以与监控结合使用,使服务器能够自我测试并在失败时触发警报。

另一种方法是为测试服务器实例使用临时凭证。一些云平台在创建新实例时随机生成凭证,并将其返回给触发创建的脚本。其他平台允许脚本定义实例的凭证。因此,自动化测试可以创建临时服务器实例,生成随机凭证或接收平台创建的凭证,然后使用这些凭证运行测试,完成后销毁服务器实例。凭证无需共享或存储,即使被泄露也不会访问其他服务器。

3. 实施和运行测试

3.1 隔离组件进行测试

为了有效测试组件,测试期间必须将其与任何依赖项隔离。例如,测试 nginx Web 服务器的配置时,Web 服务器将请求代理到应用服务器。但我们希望在不启动应用服务器的情况下测试 Web 服务器配置,因为启动应用服务器需要部署应用,而应用又需要数据库服务器,数据库服务器又需要数据模式和数据。这不仅使设置测试变得复杂,而且除了要测试的配置外,还有许多潜在的错误来源。

解决方案是使用存根服务器代替应用服务器。存根服务器是一个简单的进程,监听与应用服务器相同的端口,并提供测试所需的响应。存根可以是一个简单的应用程序,例如 Ruby Sinatra Web 应用,也可以是另一个 nginx 实例,或者是用基础设施团队喜欢的脚本语言编写的简单 HTTP 服务器。

存根服务器必须易于维护和使用,只需返回特定于我们编写的测试的响应。例如,一个测试可以检查对 /ourapp/home 的请求是否返回 HTTP 200 响应,因此存根服务器处理此路径。另一个测试可能检查当应用服务器返回 500 错误时, nginx 服务器是否返回正确的错误页面,因此存根可能对 /ourapp/500-error 路径返回 500 错误。第三个测试可能检查 nginx 在应用服务器完全关闭时是否能正常处理,此测试在不启动存根的情况下运行。

存根服务器应能快速启动,对环境和基础设施的要求简单,这意味着它可以在完全隔离的环境中运行,例如作为大型测试套件的一部分在轻量级容器中运行。

3.2 测试替身

模拟对象(Mocks)、伪造对象(Fakes)和存根对象(Stubs)都是“测试替身”的类型。测试替身用于替换被测试组件或服务所需的依赖项,以简化测试。不同人对这些术语的使用方式可能不同,但 Gerard Meszaros 在其 xUnit 模式一书中的定义很有用。

3.3 重构组件以实现隔离

通常,某个组件可能难以隔离。与其他组件的依赖关系可能是硬编码的,或者过于复杂难以拆分。编写测试有助于改进系统设计,难以隔离测试的组件是设计不佳的表现。设计良好的系统应该具有清晰且松散耦合的组件。

当遇到难以隔离的组件时,应修复设计。这可能很困难,组件可能需要完全重写,库、工具和应用程序可能需要替换。为了使系统可测试,需要一个清晰的设计。重构是一种在重组系统内部设计过程中优先保持系统完全正常运行的方法。

3.4 管理外部依赖项

依赖于非自己团队管理的服务是常见的情况。如 DNS、身份验证服务或电子邮件服务器等基础设施元素和服务可能由其他团队或外部供应商提供。这对自动化测试带来了挑战,原因如下:
- 它们可能无法处理持续测试甚至性能测试产生的负载。
- 可能存在可用性问题,影响自身测试,尤其是供应商或团队提供服务的测试实例时。
- 可能存在成本或请求限制,使其不适合用于持续测试。

大多数测试可以使用测试替身代替外部服务。只有在验证了自己的系统和代码后,才与外部服务集成。这样可以确保如果测试失败,知道是外部服务的问题还是集成方式的问题。

应确保在外部服务失败时能明确问题所在。例如,曾有团队花了一周多时间排查间歇性测试失败问题,最终发现是云供应商 API 的请求限制导致的。任何与第三方的集成,甚至自己服务之间的集成,都应实现检查和报告机制,使问题在所有环境中通过监控和信息展示立即可见。在许多情况下,团队会实施单独的测试和监控检查,报告与上游服务的连接情况。

3.5 测试设置

自动化测试中,一致性、可重复性和可再现性至关重要。行为不一致的测试毫无价值,因此自动化测试的关键部分是确保环境和数据的一致设置。

对于涉及设置基础设施(如构建和验证虚拟机)的测试,基础设施自动化本身有助于实现可重复性和一致性。挑战在于状态管理。给定测试对数据和环境配置有哪些假设?

自动化测试的一般原则是每个测试应独立,并确保其所需的起始状态。应能够以任何顺序运行测试,单独运行任何测试,并始终获得相同的结果。

例如,以下是两个测试示例,第一个测试 nginx 在 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(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 配置。而且它是自包含的,这些测试可以单独运行或以任何顺序运行,每次都能得到相同的结果。

3.6 管理测试数据

一些测试依赖于数据,特别是测试应用程序或服务的测试。例如,为了测试监控服务,可能会创建一个测试监控服务器实例。各种测试可能会向实例添加和删除警报,并模拟触发警报的情况。这需要仔细考虑,以确保测试可以以任何顺序重复运行。

例如,我们可能编写一个测试,添加一个警报并验证它是否在系统中。如果在同一测试实例上运行此测试两次,可能会尝试第二次添加相同的警报。根据监控服务的不同,添加重复警报的尝试可能会失败,或者测试可能会因为找到两个同名警报而失败,或者第二次添加警报实际上可能不起作用,但验证发现了第一次添加的警报,从而无法告知我们失败情况。

因此,测试数据的一些规则如下:
- 每个测试应创建其所需的数据。
- 每个测试应在之后清理其数据,或者每次运行时创建唯一的数据。
- 测试永远不应在开始时假设数据是否存在。

不可变服务器有助于确保干净和一致的测试环境。持久测试环境随着时间的推移容易出现偏差,不再与生产环境一致。

3.7 监控和测试

监控和自动化测试有很多共同点,两者都对基础设施及其服务的状态进行断言,并在断言失败时提醒团队存在问题。将这些关注点结合或至少集成起来可能非常有效。可以考虑重用自动化测试来验证生产环境中系统是否正常工作,但需要注意以下几点:
- 许多自动化测试有副作用,并且/或者需要特殊设置,这在生产环境中可能具有破坏性。
- 许多测试与生产监控无关。监控检查因操作状态变化而可能出现的问题,而测试验证代码更改是否有害。一旦代码更改并应用到生产环境,重新运行功能测试可能毫无意义。
- 只有在简化工作时,在测试和监控之间重用代码才有用。在许多情况下,为了将测试工具用于生产监控而进行改造可能得不偿失。

自动化测试是基础设施即代码中最具挑战性但对支持可靠和适应性强的基础设施也最重要的方面。团队应养成习惯并建立流程,将测试作为基础设施的核心部分。

4. 总结与建议

4.1 关键要点回顾

为了更好地理解和实施基础设施自动化测试,我们将前面提到的关键要点进行总结,如下表所示:
| 类别 | 要点 | 说明 |
| — | — | — |
| 测试类型选择 | 从简单测试开始 | 无固定公式确定测试类型,先进行简单测试,有需求时再引入新层次和类型 |
| | 避免反射性测试 | 只重复配置定义的测试无实际价值,有复杂逻辑时再编写测试 |
| 测试有效性 | 持续审查改进 | 定期清理、添加或替换测试,根据环境问题调整测试 |
| | 关注测试效率 | 若维护测试成本高于收益、生产环境问题多等情况,考虑改进测试 |
| 代码覆盖率 | 避免设置目标 | 基础设施单元测试配置简单,设置覆盖率目标会产生无用代码 |
| 测试工具 | 配置定义测试框架 | 流行工具大多有对应框架,用相同语言编写测试便于操作 |
| | 高级测试工具 | 通用 BDD 和 UI 测试工具用于高级测试,Serverspec 可验证服务器状态 |
| | 安全连接测试 | 可让测试在本地执行推结果到中央,或用临时凭证创建销毁实例 |
| 实施运行测试 | 隔离组件 | 用存根服务器代替依赖,确保测试环境简单独立 |
| | 测试替身 | 模拟、伪造和存根对象简化测试 |
| | 重构组件 | 改进设计使组件可隔离,优先保持系统运行 |
| | 管理外部依赖 | 用测试替身代替外部服务,集成时明确问题来源 |
| | 测试设置 | 每个测试独立,确保起始状态一致,避免测试耦合 |
| | 管理测试数据 | 测试创建所需数据,清理或创建唯一数据,不做数据假设 |
| | 监控与测试结合 | 可重用测试监控生产环境,但要注意副作用和相关性 |

4.2 实施流程建议

为了更清晰地展示基础设施自动化测试的实施流程,下面给出一个 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 --> F(审查测试有效性):::process
    E --> F
    F --> G{是否需要改进}:::decision
    G -->|是| H(调整测试):::process
    G -->|否| I(选择测试工具):::process
    H --> I
    I --> J(隔离组件进行测试):::process
    J --> K(管理外部依赖):::process
    K --> L(设置测试环境和数据):::process
    L --> M(运行测试):::process
    M --> N{测试是否通过}:::decision
    N -->|是| O(结合监控验证生产环境):::process
    N -->|否| P(分析问题并修复):::process
    P --> J
    O --> Q([结束]):::startend

4.3 最佳实践总结

  • 测试设计 :从简单测试入手,逐步增加复杂度,避免编写无意义的反射性测试。确保每个测试独立,明确测试的目的和依赖关系。
  • 工具选择 :根据不同的测试需求选择合适的工具,如配置定义测试使用对应框架,高级测试使用通用工具。同时,注意测试工具的安全性。
  • 组件隔离 :使用测试替身隔离组件,简化测试环境,提高测试效率和准确性。
  • 数据管理 :遵循测试数据的规则,确保测试数据的一致性和可重复性。
  • 监控集成 :合理利用自动化测试进行生产环境监控,但要注意测试的适用性和副作用。

通过遵循这些建议和最佳实践,团队可以更有效地实施基础设施自动化测试,提高基础设施的可靠性和适应性,为业务的稳定运行提供有力保障。

自动化测试虽然具有挑战性,但只要团队养成良好的习惯,建立完善的流程,将测试作为基础设施的核心部分,就能够应对各种复杂情况,实现基础设施的高效管理和持续优化。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值