Use RuboCop and Foodcritic to verify code style

 60 minutes
KEY POINT: Lint testing can help you identify potential defects in your code early and also makes your code easier to read and maintain.

In the Verify with InSpec and Unit test with ChefSpec tutorials, you've learned how to use automated testing to speed up the feedback cycle. Getting feedback early and often can help you release more quickly and with higher quality.

Although your code may behave as you expect, it's also important to ensure your code is easy to read and maintain, and avoids defects that can be difficult to fix later. Lint testing, or linting, is one way to help ensure that your code adheres to standard style guidelines and avoids common problems.

Lint testing is a form of static program analysis, also called static code analysis. A static code analysis tool inspects a program's source code for potential defects without actually running the program.

Lint testing can identify a range of issues, from code that's correct but difficult to read (such as code that uses non-uniform indentation) all the way to code that surely has an error (such as using an invalid character in a variable name).

In this tutorial, you'll use two popular tools that are part of the Chef DK – RuboCop and Foodcritic – to identify potential issues in variations of the webserver_test cookbook you created previously.

Test configuration

This tutorial was last tested with this configuration:

  • A host computer running Ubuntu 14.04 on VMware Fusion
    • Software: Vagrant 1.9.1, VirtualBox 5.1, Chef Development Kit 1.1.16

Prerequisites

This tutorial builds on the webserver_test cookbook that you create in the Verify with InSpec and Unit test with ChefSpec tutorials. We recommend you work through those tutorials before you begin this one.

If you want to get started right away with this tutorial, or no longer have the code from the previous ones, run this command from the ~/learn-chef directory to create the webserver_test cookbook.

Terminal: ~/learn-chef

$                     
chef generate cookbook cookbooks/webserver_testGenerating cookbook webserver_test- Ensuring correct cookbook file content- Committing cookbook files to git- Ensuring delivery configuration- Ensuring correct delivery build cookbook content- Adding delivery configuration to feature branch- Adding build cookbook to feature branch- Merging delivery content feature branch to master Your cookbook is ready. Type `cd cookbooks/webserver_test` to enter it. There are several commands you can run to get started locally developing and testing your cookbook.Type `delivery local --help` to see a full list. Why not start by writing a test? Tests for the default recipe are stored at: test/smoke/default/default_test.rb If you'd prefer to dive right in, the default recipe can be found at: recipes/default.rb

1. Use RuboCop to make your code easier to read and maintain

Let's see how RuboCop, a Ruby static code analyzer that's based on the community Ruby style guide, can help identify inconsistent code formatting.

Start by modifying your webserver_test cookbook's default recipe like this. Be sure to copy the code exactly as it appears.

Editor: ~/learn-chef/cookbooks/webserver_test/recipes/default.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Install the httpd package.
package 'httpd'

# Start and enable the httpd service.
service 'httpd' do
    action [:enable,
       :start]
end

# Serve a custom home page.
file '/var/www/html/index.html' do
    content '<html>
  <body>
    <h1>hello world</h1>
  </body>
</html>'
  end

Note how the code is formatted.

  • It uses four spaces for indentation.
  • The action attribute is broken into multiple lines.
  • The file resource uses two spaces to indent the end keyword.

When you run the rubocop command with no arguments, RuboCop checks all Ruby source files in the current directory and its subdirectories.

Start by moving to your webserver_test cookbook's directory.

Terminal: ~

$
cd ~/learn-chef/cookbooks/webserver_test

Run rubocop from your recipes directory to run RuboCop's rules against your default recipe.

Terminal: ~/learn-chef/cookbooks/webserver_test

$                
rubocop ./recipesInspecting 1 fileW Offenses: recipes/default.rb:6:1: C: Use 2 (not 4) spaces for indentation.    action [:enable,^^^^recipes/default.rb:7:8: C: Align the elements of an array literal if they span more than one line.       :start]       ^^^^^^recipes/default.rb:17:3: W: end at 17, 2 is not aligned with file '/var/www/html/index.html' do at 11, 0.  end  ^^^ 1 file inspected, 3 offenses detected

For each file, RuboCop prints to the console a character that indicates the status of that file.

Common statuses include:

Code Name Description
. RuboCop found no potential issues.
C Convention RuboCop found code that potentially violates standard convention, for example, inconsistent use of indentation.
W Warning RuboCop found code that's valid, but potentially isn't what the programmer intended, such as comparing a variable to itself or defining unreachable code.
E Error RuboCop found a potential error, such as using an invalid character in a variable name.
F Fatal RuboCop found a potential unrecoverable error that would cause the program to crash, such as an exception that is not handled.

When RuboCop finds multiple potential issues, it reports the most severe as the file's status. In this example, RuboCop found both a convention violation and a warning, so our recipe's overall status is W (Warning).

Fix the violations

Let's fix the reported violations. To summarize them:

  • Line 6 uses 4 spaces for indentation.
  • Line 7 does not properly align the array elements.
  • Line 17 indents the end keyword.

Modify default.rb like this to fix the violations.

Editor: ~/learn-chef/cookbooks/webserver_test/recipes/default.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Install the httpd package.
package 'httpd'

# Start and enable the httpd service.
service 'httpd' do
  action [:enable, :start]
end

# Serve a custom home page.
file '/var/www/html/index.html' do
    content '<html>
  <body>
    <h1>hello world</h1>
  </body>
</html>'
end

Run rubocop a second time to check whether the violations are fixed.

Terminal: ~/learn-chef/cookbooks/webserver_test

$          
rubocop ./recipesInspecting 1 fileC Offenses: recipes/default.rb:11:1: C: Use 2 (not 4) spaces for indentation.    content '<html>^^^^ 1 file inspected, 1 offense detected

This time a new violation is discovered. This violation was not reported originally because the use of indentation was consistent with the indented end keyword.

Correct the indentation, making your default recipe look like this.

Editor: ~/learn-chef/cookbooks/webserver_test/recipes/default.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Install the httpd package.
package 'httpd'

# Start and enable the httpd service.
service 'httpd' do
  action [:enable, :start]
end

# Serve a custom home page.
file '/var/www/html/index.html' do
  content '<html>
  <body>
    <h1>hello world</h1>
  </body>
</html>'
end

Run rubocop to verify the correction.

Terminal: ~/learn-chef/cookbooks/webserver_test

$    
rubocop ./recipesInspecting 1 file. 1 file inspected, no offenses detected

Congratulations. RuboCop reported no additional violations.

Configure RuboCop to follow your preferred style

A RuboCop rule is also called a cop. You can modify the behavior of RuboCop's predefined cops or disable one or more cops altogether.

One reason you may want to customize RuboCop's predefined behavior is when your team or organization defines style guidelines that differ from the community standard.

Another reason might be when you have many RuboCop violations in your existing code, and you want to resolve them gradually.

For example, say your existing code uses 4 spaces for indentation and you want to temporarily configure RuboCop to accept this indentation so that you can focus on potentially more severe issues.

Start by modifying your default recipe to look like this. It uses 4 spaces for indentation.

Editor: ~/learn-chef/cookbooks/webserver_test/recipes/default.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Install the httpd package.
package 'httpd'

# Start and enable the httpd service.
service 'httpd' do
    action [:enable, :start]
end

# Serve a custom home page.
file '/var/www/html/index.html' do
    content '<html>
  <body>
    <h1>hello world</h1>
  </body>
</html>'
end

Run rubocop to observe the violations.

Terminal: ~/learn-chef/cookbooks/webserver_test

$             
rubocop ./recipesInspecting 1 fileC Offenses: recipes/default.rb:6:1: C: Use 2 (not 4) spaces for indentation.    action [:enable, :start]^^^^recipes/default.rb:11:1: C: Use 2 (not 4) spaces for indentation.    content '<html>^^^^ 1 file inspected, 2 offenses detected

You customize RuboCop's behavior through a file named .rubocop.yml. When RuboCop runs, it loads all predefined rules and then overrides any rules that you specify.

Create a file named .rubocop.yml in your cookbook directory, ~/learn-chef/cookbooks/webserver_test, and add this code to it.

Editor: ~/learn-chef/cookbooks/webserver_test/.rubocop.yml

1
2
3
Style/IndentationWidth:
  # Number of spaces for each indentation level.
  Width: 4

RuboCop searches for a file named .rubocop.yml by traversing up the directory tree. So in practice you might add your .rubocop.yml file to a directory that's higher in the tree if you want your modified rules to apply to multiple cookbooks.

  You can use RuboCop's default rules as a guide. Just copy the rules that you want to modify or disable to your .rubocop.yml file and modify its parameters. To disable a rule, add Enabled: false to the rule.

Now run rubocop to verify that your custom behavior is used.

Terminal: ~/learn-chef/cookbooks/webserver_test

$    
rubocop ./recipesInspecting 1 file. 1 file inspected, no offenses detected

After you address any other violations, you can remove your custom rule from .rubocop.yml and then focus on the indentation violations.

  If your code produces a large number of violations, you can run RuboCop to automatically generate a configuration file that excludes the violations from a rubocop run. As you work through the violations, you can remove each entry from the generated configuration file.
You can also disable cops directly in your source code if you have a special case that you want RuboCop to ignore.
  There's also cookstyle, which limits RuboCop tests to those that are most appropriate for cookbook development.

2. Use Foodcritic to identify better usage patterns

Foodcritic is another popular linting tool that comes with the Chef DK.

RuboCop can be run on any Ruby program, and isn't specific to Chef. Foodcritic identifies usage patterns that are specific to Chef code. Many Chef users run both tools as part of their lint testing.

In this part you'll see how Foodcritic can identify usage patterns that improve the quality of your code. You'll also extend Foodcritic by writing a custom rule.

Identify use of unnecessary string interpolation

In Ruby, string interpolation enables you to replace placeholders within a string with the values they represent.

Here's an example for a basic web server configuration.

Editor: webserver.rb

1
2
3
4
# Write the home page.
file "#{node['document_root']}/index.html" do
  content '<html>This is a placeholder</html>'
end

Here, double quotes " indicate the use of string interpolation. This file resource combines the document root, stored as a node attribute, with the name of the index file to create a full path to the home page. The #{} syntax indicates the placeholder. Its contents can be a variable or block of Ruby code.

The content property uses single quotes ' because it does not require string interpolation – there are no placeholders to replace.

Double quotes may also be used when the contents of the string contains one or more single quotes. For example:

Editor: webserver.rb

1
2
3
4
# Write the home page.
file "#{node['document_root']}/index.html" do
  content "<html>Welcome to Suzy's web site!</html>"
end

You can use double quotes even when string interpolation is not used. However, some people prefer to use double-quotes only when string interpolation is required because it makes the intention of the string clear.

Let's examine a case where lint testing can help identify the use of unnecessary string interpolation. Modify your default recipe to look like this.

Editor: ~/learn-chef/cookbooks/webserver_test/recipes/default.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package_name =
  service_name =
    case node['platform']
    when 'centos' then 'httpd'
    when 'ubuntu' then 'apache2'
    end

# Install the httpd package.
package "#{package_name}"

# Start and enable the httpd service.
service "#{service_name}" do
  action [:enable, :start]
end

# Serve a custom home page.
file '/var/www/html/index.html' do
  content '<html>
  <body>
    <h1>hello world</h1>
  </body>
</html>'
end

This code resembles the code you wrote in the previous tutorial that configures Apache for both Ubuntu and CentOS.

Notice the use of double quoted strings. Although this program is valid and behaves as you would expect, its intention might not be clear to others. Because string interpolation is not required, it might appear to others that the code intends to use string interpolation but the string is missing additional information.

From your webserver_test cookbook's directory, run foodcritic ./recipes/* to run the Foodcritic rules against the default recipe.

Terminal: ~/learn-chef/cookbooks/webserver_test

$  
foodcritic ./recipes/*FC002: Avoid string interpolation where not required: ./recipes/default.rb:9FC002: Avoid string interpolation where not required: ./recipes/default.rb:12

Foodcritic reports FC002: "Avoid string interpolation where not required".

Now remove the string interpolation to fix the violation. Your recipe should resemble the one you created in the previous tutorial.

Editor: ~/learn-chef/cookbooks/webserver_test/recipes/default.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package_name =
  service_name =
    case node['platform']
    when 'centos' then 'httpd'
    when 'ubuntu' then 'apache2'
    end

# Install the package.
package package_name

# Start and enable the service.
service service_name do
  action [:enable, :start]
end

# Serve a custom home page.
file '/var/www/html/index.html' do
  content '<html>
  <body>
    <h1>hello world</h1>
  </body>
</html>'
end

Run foodcritic a second time to verify that the violation is fixed.

Terminal: ~/learn-chef/cookbooks/webserver_test

$
foodcritic ./recipes/*

When foodcritic produces no output, it indicates that Foodcritic found no violations.

Use Foodcritic to simplify your code

Say you didn't know about the service resource, and that you instead use the execute resource to manage the Apache service in your webserver_test cookbook.

Modify your default recipe like this.

Editor: ~/learn-chef/cookbooks/webserver_test/recipes/default.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
case node['platform']
when 'centos' then
  package 'httpd'
  execute 'enable the httpd service' do
    command 'chkconfig httpd on --level 3'
    not_if 'chkconfig --list httpd | grep 3:on'
  end
  execute 'start the httpd service' do
    command 'service httpd start'
    not_if "service --status-all | grep -e 'httpd (pid\s*[0-9]*) is running'"
  end
when 'ubuntu' then
  package 'apache2'
  execute 'enable the apache2 service' do
    command 'update-rc.d apache2 defaults'
    not_if 'ls /etc/rc?.d/*apache2'
  end
  execute 'start the apache2 service' do
    command 'service apache2 start'
    not_if "service apache2 status | grep 'apache2 is running'"
  end
end

file '/var/www/html/index.html' do
  content '<html>
  <body>
    <h1>hello world</h1>
  </body>
</html>'
end

Although this recipe might work as you expect, it's unnecessarily complex. The execute resource requires additional code to ensure that the service is enabled and started only when needed. This code would require additional testing to ensure that it accounts for differences in each version of CentOS and Ubuntu that you support.

Run foodcritic to apply Foodcritic's rules to your recipe.

Terminal: ~/learn-chef/cookbooks/webserver_test

$  
foodcritic ./recipes/*FC004: Use a service resource to start and stop services: ./recipes/default.rb:8FC004: Use a service resource to start and stop services: ./recipes/default.rb:18

Foodcritic reports FC004: "Use a service resource to start and stop services".

The service resource takes care of all of the details required to enable and start the service. Modify your recipe to look like the original version, like this.

Editor: ~/learn-chef/cookbooks/webserver_test/recipes/default.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package_name =
  service_name =
    case node['platform']
    when 'centos' then 'httpd'
    when 'ubuntu' then 'apache2'
    end

# Install the package.
package package_name

# Start and enable the service.
service service_name do
  action [:enable, :start]
end

# Serve a custom home page.
file '/var/www/html/index.html' do
  content '<html>
  <body>
    <h1>hello world</h1>
  </body>
</html>'
end

Run foodcritic to verify that the violation is fixed.

Terminal: ~/learn-chef/cookbooks/webserver_test

$
foodcritic ./recipes/*

By fixing the violations, you've created a recipe that's easier to read, maintain, and verify.

Check out the Foodcritic documention to learn more, including how to:

Conclusion

In this tutorial, you saw how to use RuboCop and Foodcritic to help ensure that your code adheres to standard style guidelines and avoids common problems. Running lint testing tools such as RuboCop and Foodcritic helps ensure that the code you write adheres to your team's preferred style. They can also help detect code that surely has an error, such as using an invalid character in a variable name.

Like ChefSpec, RuboCop and Foodcritic do not involve the creation of a virtual instance, and are fast ways to validate the correctness of your work. Once you have code that passes lint and unit testing, you can apply that code to virtual instances using tools such as Test Kitchen and InSpec. After you verify your work on virtual instances, you can move on running your changes to real test infrastructure or submit your change to a continuous delivery system such as Chef Automate.

Next steps

If you haven't gone through the Verify with InSpec and Unit test with ChefSpec tutorials, now's a great time. You'll learn how to use InSpec to verify your configuration on virtual test instances and how to use ChefSpec to simulate the execution of your resources in memory.

Learn more about how to test and debug your Chef code in our Joy of Automating video series, hosted by Franklin Webber.

If you're involved in your company's compliance and audit process, you may be interested in the Chef compliance scanner, which uses InSpec as its auditing and testing framework.

You may also be interested in Chef Automate, which gives your operations and development teams a common platform for developing, building, testing, and deploying cookbooks, applications, and more. Chef Automate reinforces the Chef workflow, where you begin by developing and testing your configuration from your local workstation. Then, you submit your change to Chef Automate's pipeline, where your change goes through sets of automated tests before going out into production. If you have many different teams, each delivering software in its own way, you can use Chef Automate to bring a standard, proven approach to all of your organization's deployments.

  Ready to dig deeper? Join us in-person or online at an upcoming instructor-led training event. Learn more about our course offerings or check out our upcoming classes. Use discount code LEARN-CHEF to save 10%. Use what you've learned to gain official Chef certification.

Back to Tutorials