Chapter 5. Testing in Practice

We write tests to build confidence in our tools and infrastructure code(infracode). This is helpful when collaborating on code as this can help eliminate some of the fear of making a change to the systems. The goals of testing is to help you assess risk, respond to and recover from problems quickly, and improve your delivery processes.

In this chapter, I revisit linting, unit and integration testing in practice. You’ll get real examples, and learn a bit more about infrastructure and writing tests for infrastructure.

Writing Unit Tests for Infracode

A challenge of writing unit tests for infracode is that it can be very easy to test the infrastructure platform in use rather than your code. Think about the test and whether it’s verifying the code as written, or testing that the infrastructure as code software is working. Unless it’s an in-house developed system, trust that the software does what it is supposed to do. Even if you are working with an in-house developed configuration system, test that platform in it’s git project separately from your infrastructure code project.

With infracode unit tests, there generally is a specific package that maps out to testing the platform you are using. For example, Chef has Chefspec, and puppet has rspec-puppet.

Let’s dig into infracode unit testing with Chef. Remember, the key is to focus on broad principles and concepts. While you may not use Chef in your environment, you can apply the testing concepts and practices regardless of tools. I’ll start with a brief overview of Chef terminology, look at Chefspec, and then run through an example using Chef with Chefspec.

Note

I haven’t talked about Chef or Infrastructure as Code yet. You may want to jump ahead and read the Infrastructure and Infrastructure in Practice chapters before proceeding in this chapter. Although you will find more detailed information in those chapters, they are not crucial to understanding the concepts and tools that I share here.

Examining Chef Terminology

The core Chef terminology used in this chapter includes cookbooks, recipes, and resources.

  • Chef cookbooks are a collection of policy code that includes everything about a specific function you need. In general, you want to have small cookbooks that focus on a single purpose so that it’s easy to figure out where to find the code when it needs to be modified or removed. For example, if you wanted to configure and deploy Datadog agents, you might have a Datadog cookbook.

Note

Datadog is a monitoring service that can be used to monitor servers, databases, tools, and services. There are a variety of plugins that can be installed to collect information that can be visualized on the Datadog service. I’ll dig more into Datadog in the Monitoring chapter.

The Datadog agent is software that can be installed on a compute instance to collect a variety of event information.

  • Chef recipes are text files that define the algorithm to do a specific task or set of tasks. Within the datadog cookbook, you would have Chef recipes that would install and remove the datadog agent. The algorithm for installing a datadog agent could include where to find the package to install the agent, download the package, a reference to the configuration file, and starting up the datadog agent as a service.

  • Chef resources are the components in the recipe that map out to the specific piece of infrastructure you manage. In this example of the datadog cookbook install recipe, it would be the package, configuration file, and datadog agent service. Chef provides several resources, including package, cookbook_file, and service to simplify the infrastructure code written.

  • Chef custom resources are components that can be written and included in a cookbook and then used much like a regular resource. With the datadog cookbook, if the install recipe got complex enough, you could turn the recipe into a custom resource. This would be usable by anyone who included a dependency to the cookbook that the new custom resource was included in.

Recipes can get complicated when you have specific customizations, for example, different operating systems, compute instances, or the environment that the system exists in test or production.

Valuable unit tests are going to test those inputs that change how the recipe runs so that you can have deterministic outputs.

Writing Unit Tests with Chefspec

Chefspec is the testing framework for unit tests with Chef. It will test the recipes and custom resources in the context of a simulated Chef run. It is an extension of RSpec, and once installed, is run using the rspec command.

Note

Chefspec is packaged as part of the Chef Workstation. Chef Workstation is a set of tools and utilities to facilitate developing infracode within the Chef ecosystem.

Defining RSpec Fundamentals

To use Chefspec effectively, it helps to understand RSpec fundamentals. RSpec is a testing tool for Ruby programmers. It facilitates writing acceptance and unit tests that are closer to English. When you write Chef code, you are writing Ruby, so tools that help Ruby programmers will help you with your Chef code as well.

The basic structure of RSpec uses describe and it.

Use describe to signal a collection of tests. In RSpec, this collection of tests is called an example group.

Use it to define one test. In RSpec, this test is called an example.

If you wanted to bake some cookies and wanted to test your cookie making process, you might think of this process as “Baking cookies requires me to gather all the essential cookie ingredients, preheat the oven to the specified temperature, measure the ingredients, combine the ingredients following a recipe, and bake the cookies for a specific amount of time.”

If you translated that into RSpec terminology that would look like this:

Example 5-1.
describe 'Baking cookies' do
  it 'gathers ingredients'
  it 'preheats the oven'
  it 'measures the ingredients'
  it 'combines ingredients in order'
  it 'bakes the cookies'
end

In this example, you describe the collection of tests that need to be grouped to test “Baking cookies.” Within this collection, you have a test per expectation to ensure that you’re successful in baking cookies.

If you wanted to do further grouping, you add additional describes within the block, or use the RSpec keyword context, which is an alias for describe. Context and describe have no semantic code-level difference. The intent is to provide a mechanism to heighten understanding for humans. So in this example, you could add context for when baking chocolate chip cookies versus peanut butter cookies.

Tip

Learn more about testing with RSpec from the Effective Testing with RSpec 3 book by Myron Marston and Ian Dees.

Chefspec extends RSpec providing additional matchers to common Chef resources.

Writing Unit Tests for Datadog Install Recipe

In the following examples, I’m going to demonstrate how I’d think through setting up tests for a new datadog cookbook.

The software versions used in this example:

  • RSpec 3.8

Note

It is not required to have a complete Chef working environment to write or run unit tests on Chef cookbooks.

The first thing I do when I’m working on creating a new cookbook is to check to make sure that it doesn’t already exist in some form whether it’s something that I can use directly or copy examples from. For this scenario, I’m going to pretend that there isn’t a cookbook I can snag any content from, but in practice, there are often community cookbooks for many common scenarios.

The next thing I do is review the installation instructions for the version of the software that I’m planning on installing. For this example, looking at the installation instructions for the Datadog agent helps me think about what a successful install looks like.

Installing the Datadog agent on a Debian instance requires me to:

  1. Set up apt.

  2. Configure the Datadog deb repo and import the key.

  3. Install the datadog-agent package.

  4. Copy a sample config file into place.

  5. Update the config file with my API key.

  6. Start the datadog-agent service.

Good unit tests for a recipe in this cookbook will focus on just the portions of Datadog that I’m writing. This first step of setting up apt should be handled by other Chef configuration code if I’m already managing with Chef.

Since the first Datadog specific thing that I need to do is configure the repo, that will be the first unit test that I write.

Example 5-2. Initial Datadog agent install_spec.rb Unit Test
require 'chefspec'

describe 'datadog_agent::install' do
  platform 'debian'
  describe 'adds the datadog repository with key' do
    it { is_expected.to add_apt_repository('datadog') }
  end
end

I create an initial install_spec.rb text file that will contain the unit tests.

I start with a require statement so that the chefspec gem is included. This is Ruby syntax that makes sure that the chefspec library is loaded.

Next, the describe keyword is RSpec syntax. I’m creating a block of code that says that I’m describing the datadog_agent cookbook install recipe.

Within this block, I’m defining the platform as debian. This is how I signal Chefspec about a specific platform. If I had different versions of Debian, or different operating systems entirely, I would have different blocks at this level specifying the different platforms under observation.

With the next describe keyword, I’m creating another block to say that I’m adding the datadog repository.

Within this block, I’m using the it RSpec keyword to signal an “Example.” With it, I’m specifying the specific behavior I’m expecting.

With the expect RSpec keyword, I define an “Expectation.” If I “read” this Expectation, it says, “It is expected that our code will add an apt repository datadog.”

Tip

Using the chef command provided with Chef Workstation to generate cookbooks and recipes will auto-provision the unit test file you need associated with the recipe in the right place within your project.

For example, if I run chef generate recipe install within the datadog_agent cookbook, it creates a new file spec/unit/recipes/install_spec.rb for me that I can then update with my unit tests.

To run the tests from the command line, I execute $ chef exec rspec spec/unit/recipes/install_spec.rb --color.

Example 5-3. Initial RSpec Output
Failures:

  1) datadog_agent::install adds the datadog repository with key should add apt_repository "datadog"
     Failure/Error: it { is_expected.to add_apt_repository('datadog') }

       expected "apt_repository[datadog]" with action :add to be in Chef run. Other apt_repository resources:



     # ./spec/unit/recipes/install_spec.rb:12:in `block (3 levels) in <top (required)>'

Finished in 0.43668 seconds (files took 4.94 seconds to load)
1 example, 1 failure

By specifying --color, I will see the failure in red, the red in my red, green, refactor cycle.

Example 5-4. After Adding the apt_repository Resource
$ chef exec rspec spec/unit/recipes/install_spec.rb --color
.

Finished in 0.45954 seconds (files took 5.29 seconds to load)
1 example, 0 failures

After writing the Chef infracode, I can then re-run my test and see that my test passes. I would then go through the rest of the list of requirements adding a test example for each of the expectations that I have of my code making sure that I’m covering the happy path. I would also add relevant negative tests.

Tip

In larger projects, for example a cookbook with many recipes, it is common to create a spec_helper.rb file which would include require chefspec and any other common setup tasks. Then in the spec file, we could add require ’spec_helper’.

Using the chef CLI to generate recipes will automatically set up this spec_helper.rb and add require ’spec_helper’ to the newly created file.

This helps eliminate duplication across multiple files and is a practice that can be replicated with other testing tools.

Writing Integration Tests for Infracode

Integration tests for infracode can be narrow or broad depending on how many components we are testing against. At the team level, decisions can be made about the test structure. Many times, automating testing infrastructure at all is a big step!

Let’s dig into infracode integration testing with Chef. As with unit testing, I’ll introduce a few tools and key concepts from those tools and then run through an example.

Test Kitchen is an application that integrates with different cloud providers and virtualization technologies. It can leverage whatever infracode definitions in use in an environment in combination with a configuration file that defines the set of test suites to run with those applications. With a configuration file bundled with a project, it allows for team members to quickly set up the infrastructure and project environment to start testing and developing.

I’ll be using Test Kitchen to spin up an instance so that I can do integration testing. I go into more detail in the Infrastructure in Practice chapter, for now I want to focus on the testing tools themselves.

Note

Test Kitchen is packaged as part of the Chef Workstation. It is also distributed as a Ruby gem and can be installed using the standard Ruby gem mechanisms.

Writing Integration Tests for Datadog Install Recipe

The software versions I’m using in this example:

  • Chef InSpec 3.9.3

Depending on how the team does integration testing, the scope of what falls into integration tests varies. An example of this is whether you would test the integration with the Datadog service and verify that setup actually works correctly or if you mock out connecting to the service and assume that everything will just work in different environments.

To write metrics to the Datadog service, you must use a valid API key. Since Datadog pricing is based on instance count as of this writing, it’s important to remove any test instances after verification.

With integration testing, examine the expectations and operation of the software a bit more. The main components of the Datadog agent include:

  • collector - runs checks and collects metrics,

  • forwarder - sends data to the Datadog service,

  • APM agent - collects traces,

  • process agent - collects live process information.

Note

On Windows systems Datadog system components are named differently.

These components bind on either 3 or 4 ports depending on the operating system.

Chef InSpec is a framework for testing and auditing applications and infrastructure. It can be used with any type of infrastructure platform to verify the outcomes of code on application and infrastructure and not the code itself. Stating this in a different way, the tests that are written for verifying that code works as expected during development can be reused to audit what is currently in production as well.

InSpec organizes tests suites into profiles, which are then categorized into controls.

Tip

InSpec profiles can be shared and used separately from cookbook code for example on Supermarket or GitHub directly. The DevSec project is a popular set of profiles that are built and maintained by the community to implement baselines for hardening popular infrastructure and applications.

You have to start somewhere when writing integration tests. InSpec has a number of resources to simplify writing tests. The package and service InSpec resources can be used to verify that the datadog-agent package is installed and that the service is configured to run as expected.

This configuration uses the package and service InSpec resources. The syntax is very similar to RSpec as it’s inspired by Serverspec, an extension of RSpec. A key difference between Serverspec and InSpec is the format of the resources. InSpec handles a lot of the setup with the resource definition so the test profiles are a little easier to read.

Example 5-5.
describe package('datadog-agent') do
  it { should be_installed }
end

describe service('datadog-agent') do
  it { should be_installed }
  it { should be_enabled }
  it { should be_running }
end

To verify that the repository is setup, you can use the apt resource.

Note

Serverspec is another integration and acceptance testing tool for testing infrastructure as code works as expected.

When writing Chef code, the infracode describing configuration files on the system are written to the system exactly the way you specify. If there is an error in a template, Chef won’t catch the error with the content (unless it’s failing Ruby or Chef code). One helpful command from the Datadog command line interface datadog-agent status verifies the Datadog configuration, checks the health of the agent, and version information. It can be leveraged with the InSpec command resource to validate that the configuration specified is valid syntax.

Another interesting validation might be to check that the agent running is the same as the version installed. Maybe during an upgrade it’s possible that the older version lingers and is still the active running process.

The examples I have described in this chapter have testing duplication with testing the repository, package installation and service setup. It’s important to recognize this so your teams can choose what gets tested at the unit and integration levels in your environment.

In many ways, the shape of infrastructure as code testing does not and should not follow the standard pyramid shape. Unit tests are more useful in testing complexity in the code and infrastructure code isn’t always complex.

Linting Chef Code with Rubocop and Foodcritic

There are 2 useful linters for checking Chef code. First, Rubocop is a Ruby linter that can be useful for identifying issues as Chef cookbooks are Ruby. Second, Foodcritic is a Chef linter that is specifically useful for checking Chef cookbook usage.

Note

Rubocop and Foodcritic are packaged as part of Chef Workstation. They are also distributed as Ruby gems and can be installed using the standard Ruby gem mechanisms.

Issues in Rubocop are known as cops. Cops are categorized into classes of offenses. Different classes, including Style, Layout, Naming, Lint, Metrics, Performance, and Security, can help in a variety of ways, including finding ambiguities or errors in the code, measuring properties of the code, offering replacements for slower Ruby idioms, and catching known security vulnerabilities.

Rubocop can be customized with a .rubocop.yml file to choose which concerns to comply with or ignore. For example if we want to customize the line length to 100 characters, we would have a line within .rubocop.yml to configure it:

Example 5-6.
Metrics/LineLength:
  Max: 100

Centralize Ruby style guide across an organization with a .rubocop.yml that can then be referenced within a project’s local .rubocop.yml with the inherit_from directive.

Tip

Learn more about Rubocop from the Rubocop doc website.

Foodcritic analyzes Chef code looking for issues with portability, potential run-time failures, and anti-patterns. Instead of cops, Foodcritic describes items as rules. Each rule has tags associated with it. It’s possible to restrict checking based on a particular tag or a specific rule.

Foodcritic can be extended with organization-specific rules (or adopt other’s shared rules). Foodcritic can also be customized with a .foodcritic file to ignore rules that shouldn’t be applied.

Tip

Learn more about Foodcritic from the project website.

The software versions I’m using in this section:

  • Rubocop 0.55.0

  • Foodcritic 15.1.0

Tip

Due to the nature of linters and evolution of recommended practices, linter versions can be especially sensitive. If one person has one version of lint software on their system and someone else has a different version they can have competing changes that influence how they write code causing needless conflicts when trying to work on the same project.

Cookstyle is one way to help with versioning conflicts with Rubocop when working with Chef code. It pins to a specific version of Rubocop, and has a set of specific conventions preconfigured for cookbook development. Cookstyle is included in the Chef Workstation.

In this example, various types of issues have been found in the install recipe by the ruby linter Rubocop.

Example 5-7.
$ chef exec rubocop recipes/install.rb
Inspecting 1 file
C

Offenses:

recipes/install.rb:8:1: C: Layout/IndentationWidth: Use 2 (not 4) spaces for indentation.
    keyserver keyserver
^^^^
recipes/install.rb:15:4: C: Layout/TrailingBlankLines: Final newline missing.
end


1 file Inspected, 2 offenses detected

Both of these errors are issues with layout. With the first issue, Ruby should have 2 spaces for indentation, but the code has 4. In the second reported issue, Ruby files should have a final newline. The message provided with the error message give guidance towards how to fix the issues.

Example 5-8.
$ chef exec foodcritic .
Checking 3 files
x..
FC008: Generated cookbook metadata needs updating: ./metadata.rb:2
FC008: Generated cookbook metadata needs updating: ./metadata.rb:3
FC064: Ensure issues_url is set in metadata: ./metadata.rb:1
FC065: Ensure source_url is set in metadata: ./metadata.rb:1
FC067: Ensure at least one platform supported in metadata: ./metadata.rb:1
FC078: Ensure cookbook shared under an OSI-approved open source license: ./metadata.rb:1
FC093: Generated README text needs updating: ./README.md:1

Foodcritic output is not the same as Rubocop, because Rubocop is checking against Ruby standards while Foodcritic is checking against Chef recommended practices. You can look at the project website and drill down to the specific reported issue to find examples of how to resolve the issue that has been reported.

For example, FC067 has an issue due to the fact that the cookbook’s metadata file isn’t updated to provide information about what platforms are supported by the cookbook.

Tip

Since I’m using the Chef Workstation bundled versions of software to show these examples, I’m running chef exec SOFTWARE which runs arbitrary software within the context of the Chef Workstation environment, for example setting up the PATH environment variable and the GEM_HOME and GEM_PATH Ruby environment variables.

When Foodcritic or Rubocop return a violation, this doesn’t automatically mean that the code needs to be changed. It’s important to examine the issues and identify whether they are real problems or areas where customizations to the lint configuration file need to be made.

Wrapping Up

In this chapter, I showed different ways you can use testing in practice beyond exploratory testing to automate and make work more predictable, repeatable, and collaborative. I’ll revisit testing in the Infrastructure in Practice chapter.

Often sysadmin work is viewed as “what happens after the software is created.” I’ve tackled some of the pre-production work here; in the next chapter, I’ll review architecture and the part that system administrators play in the design of applications.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.146.35.72