This episode covers Test Kitchen: the kitchen.yml structure with driver, provisioner, and suites, the converge-verify-destroy flow, various drivers from Docker to cloud, writing InSpec verification inside Kitchen, and integrating cookbook testing into the CI pipeline.

In episode 17 you wrapped an application into a Habitat artifact with hab pkg build. But building an artifact is only half the story — before your cookbook touches a production node, it must be proven to work in a controlled environment. In episode 15 we briefly touched on test-driven infrastructure; episode 18 turns it into real practice with Test Kitchen: the standard Chef Workstation tool for testing cookbooks against real instances, from local containers to cloud nodes.
Test Kitchen is a testing framework that spins up real instances (not mocks), applies the cookbook to that instance (converge), then verifies the result (verify). The cycle is deliberately kept short so feedback is fast:
This pattern changes the way you work: instead of "write a cookbook then try it manually", you write the expectations first and let Kitchen prove them again and again. Kitchen is already bundled with Chef Workstation, so kitchen version is ready to use.
All of Kitchen's behavior is controlled through the kitchen.yml file at the cookbook root. This file defines three major parts: the driver (where instances run), the provisioner (how the cookbook is applied), and the suites (the scenarios being tested).
---
driver:
name: docker
provisioner:
name: chef_zero
platforms:
- name: ubuntu-22.04
driver_config:
image: dokken/ubuntu-22.04
suites:
- name: default
run_list:
- recipe[webserver::default]
verifier:
name: inspecLet's break it down part by part. The docker driver uses the dokken/ubuntu-22.04 image, which is specially designed for Kitchen — lean and without built-in systemd. The chef_zero provisioner uses the cookbook directly from the working directory without needing a remote server, and the default suite sets a run_list pointing at the recipe under test. The inspec verifier tells Kitchen which verification to use after converge.
The driver determines where instances run, and this choice affects both speed and how closely it matches production:
| Driver | Speed | Production similarity | Use case |
|---|---|---|---|
| Docker | Very fast | Low-medium | Daily testing, CI |
| Vagrant | Medium | Medium | Local VMs with VirtualBox/VMware |
| Cloud (AWS, GCP, Azure) | Slow | High | Release candidates, golden images |
Note
Docker containers do not run systemd, so the service resource cannot be managed like it is on a full node. For cookbooks that manage services, use the dokken images that ship with a supervisor, or switch to the Vagrant driver for more realistic verification.
To change drivers, simply replace the driver.name value and adjust driver_config. For cloud, Kitchen uses plugins such as kitchen-ec2, which read credentials from the environment — credentials must never be written into kitchen.yml.
The main Kitchen commands follow the basic cycle. To illustrate, let's spin up an instance and apply the cookbook:
kitchen create
kitchen converge webserver-default
kitchen listkitchen list shows the state of all instances (created, converged, or destroyed). After converge, verify with InSpec. If verification fails, fix the cookbook and run kitchen converge && kitchen verify again — this iteration is the heartbeat of test-based development. Once you are satisfied, clean up the environment:
kitchen destroyFor one full round in a single command, use kitchen test, which runs create, converge, verify, and destroy in sequence — exactly the flow CI wants.
Verification is written as a simple InSpec profile in the test/integration/<suite-name>/ directory. InSpec resources work declaratively: describe checks a property and it asserts the expectation.
describe package('nginx') do
it { should be_installed }
end
describe service('nginx') do
it { should be_enabled }
it { should be_running }
end
describe port(80) do
it { should be_listening }
end
describe file('/etc/nginx/nginx.conf') do
it { should exist }
it { should be_owned_by 'root' }
endThis verification reads facts directly from the converged instance — not from promises in the cookbook. If a recipe only installs the package without starting the service, the should be_running check will fail and force you to fix it. That is the real goal: expectations are written first, code is forced to satisfy them.
Tip
Name suites after scenarios, not just "default": nginx-basic, php-fpm-hardened, or app-multinode. The suite name determines the instance name and the verification directory, so different scenarios do not overwrite each other.
Kitchen's full power appears when it runs automatically in CI: every pull request that changes a cookbook is tested against real instances before review. With the Docker driver, no special machines are needed — the runner just executes the standard commands.
name: test-cookbook
on:
pull_request:
jobs:
kitchen:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Chef Workstation
run: |
curl -L https://omnitruck.chef.io/install.sh | sudo bash -s -- -P chef-workstation
- name: Test Kitchen
run: kitchen testFor Jenkins, the pattern is the same — a job is run by an agent with Docker and Chef Workstation installed, then executes kitchen test as the final step. The key to CI success: make sure kitchen.yml does not depend on local paths, and give it an explicit Docker image so it is deterministic.
Episode 18 closes the development cycle with Test Kitchen as the bridge between writing and proving: kitchen.yml as the three-part contract, the driver as the choice of battlefield, the converge-verify-destroy flow as the working rhythm, InSpec verification as the judge, and CI as the permanent guardian.
Key takeaways:
In episode 19 we will move from "how to test" to "how to operate": performance & troubleshooting — analyzing chef-client runs, scheduling with a systemd timer or cron, caching, and solutions to common problems in the field. See you there!