On December 20th, I presented a live webinar on Writing Elegant Tests. Watch the recording below to hear me explain powerful RSpec features that you can use immediately to reduce code duplication, make your tests more expressive, and helpers that are exportable between all your cookbooks.
In the presentation, I focused on refactoring the ChefSpec test suite associated with an older version of the Ark cookbook using the following seven techniques:
- RSpec’s let
- RSpec’s shared_examples
- Ruby methods
- RSpec’s shared_context
- Ruby’s require
- RSpec’s alias_example_group_to
- Creating a Ruby gem
At the end of the presentation, I answered a few questions that I have included here in this post.
When should I use ChefSpec for my testing versus using InSpec/ServerSpec for testing?
That is always an important consideration to make when writing your tests. I cover this in more extensive detail in these two posts/webinars:
With ChefSpec tests I can be more exhaustive because I do not have to pay the extremely costly time and effort of test setup. I am able to define scenarios easier and see the results of those scenarios faster.
I write ChefSpec tests when I want:
- fast feedback when developing my recipes
- to test conditional logic (e.g. if, else, case)
- to test various search queries results
- to test myriad of system conditions
When InSpec/ServerSpec tests I can ensure that my recipes actually work on my target platforms. The cost is that these tests take longer to execute because instances have to be created, Chef needs to be installed, and the application needs to be installed and configured.
I write InSpec tests when I want:
- to ensure my platforms have the necessary pre-conditions to complete successfully (e.g. prerequisite users and packages)
- to ensure my recipes can be applied to new systems
- to ensure my recipes can be applied to a system multiple times without error
- to ensure my recipes work on one or more platforms
How much of ChefSpec techniques is applicable with InSpec/ServerSpec?
All of the techniques that I demonstrated in this webinar can be applied when writing InSpec/ServerSpec. This is because these languages are also built on top of RSpec.
However, you probably may not find yourself needing to use these techniques that often in your InSpec/ServerSpec tests and that is because those tests often do not have all the additional setup required in ChefSpect tests.
Within my ChefSpec files I am repeating chef_run in almost every one of my specifications. Could I define the chef_run in a common location and use it all my specifications?
In this webinar I demonstrate moving the chef_run helper into a shared_context that I then include into my test context that are working with recipes. This default chef_run object is fairly generic but I also demonstrated how you can set it up to allow for you to have defaults that you can override if necessary.
Ultimately I use the alias_example_group_to and create a new example group called describe_recipe that automatically includes the above context. This is very powerful but can also be a little too magical.
Should I define RSpec’s shared examples and RSpec’s shared contexts in a separate file, common location for reusability?
The important thing about shared_examples and shared_context is that they need to be defined before they are used within a particular test file. So often I move them to the top of the file. When they exist at the top of the file they can take up a lot of space it makes it harder to see the tests that are related to the recipe. I often choose to move them to a common location like the spec/spec_helper.rb file.
Would I want to also include the shared_examples in the shared_context?
I am certain that is would work. Including the examples that are defined in the shared_examples block will be indistinguishable from defining examples within a shared_context. But I encourage you to think hard about that decision.
Defining shared_examples allows you to use the examples defined within the block within any context you need. If you are going to use a particular set of examples within a number of contexts and also in the shared_context it makes sense. If you find yourself defining these shared_examples and using them only in this one shared_context I would probably say that it seems like you are trying to abstract too much and this will ultimately lead to more complicated code.
The end result of this exercise creates code that would be hard for others on my team to understand. Should I really implement all of these techniques in my cookbooks?
No. It is important that you implement a solution that works first for you and your team. A solution that is far too complex for your team to understand is not the correct solution.
Definitely. Using let allows you to more clearly express the details within your tests. It makes it easier to understand the details about the scenario under test. It makes it easier to extract information about the test that is most likely to change. It allows you to name things to give it more meaning for yourself and your team mates.
Maybe. Creating reusable examples may be useful. In this webinar it definitely made it easier to express all the packages that were being installed across all the platforms.
Ruby methods – definitely
If you cannot isolate the details with a let because you need to perform an operation with some input then I would encourage you to use Ruby methods. I think this is a great tool available to you and can definitely make your tests more elegant.
Maybe. Similar to shared_examples, I think that a shared_context has a particular time and place where it will work well to make your scenarios more elegant and save you some time.
Definitely. When you find yourself wanting to share helpers you define in one file with another file this is the best way to handle it. Use the fact that every specification file requires the spec_helper file to store all the helpers that you define.
Probably Not. I think this technique is the probably the trickiest for someone to understand. Someone would have to understand how that new keyword gets mapped to the context.
Creating a Ruby gem
Maybe. Your choice is either to copy the common helpers to all your cookbooks or to create a gem to manage that code in one location. I would definitely prefer to have the code defined in one place. However, you must understand that by creating that gem and making it a dependency to test the cookbook now means everyone on your team needs to install this gem. You have to manage it like software, releasing versions, and storing these versions in a central location like a Rubygems server.