A mock-based testing framework for Ansible playbooks.
Project description
Yako
A mock-based testing framework for Ansible playbooks and roles. Yako intercepts task execution via an Ansible callback plugin to inject mocks and run assertions, enabling unit-style testing without real infrastructure.
Inspired by Monkeyble, with key differences:
- Each test case can run in an isolated Docker container
- Hierarchical configuration (global → module → test case)
- Built-in
yako_assertmodule for inline assertions inside playbooks
Running Yako
Requires Python 3.13+.
Run Yako directly without installing it globally:
uvx yako test
If you are working inside a checkout of this repository, run the local code with:
uv sync
uv run yako test
Development
For local development, sync the project environment and include the dev dependency group
defined in pyproject.toml:
uv sync --group dev
After that, run the local checkout and development tools with uv run:
# Run all Python tests
uv run pytest
# Run Yako itself from the local checkout
uv run yako test
Quick Start
1. Create a configuration file
Create yako.yaml in your repository root:
runner_mode: "local"
2. Write a playbook to test
Create tests/yako/playbooks/hello.yaml:
- hosts: 127.0.0.1
gather_facts: false
tasks:
- name: Say hello
debug:
msg: "Hello, world!"
3. Write a test case
Create tests/yako/test_hello.yaml:
test_cases:
- name: "test_hello"
playbooks:
- "playbooks/hello.yaml"
4. Run
# Run without installing globally
uvx yako test
# Or run the local checkout
uv run yako test
Writing Test Cases
Test files are YAML files named test_*.yaml, placed under tests/yako/ by default. Each file is a test module containing one or more test cases.
# Module-level given (applies to all test cases in this file)
given:
extra_vars:
env: "testing"
test_cases:
- name: "test_something"
playbooks:
- "my_playbook.yaml"
given:
extra_vars:
feature_flag: true
mock_tasks:
- name: "Install packages"
mock: {}
Playbooks vs Inline Tasks
Each test case must specify either playbooks or tasks, not both.
Reference an existing playbook:
test_cases:
- name: "test_with_playbook"
playbooks:
- "my_playbook.yaml"
Playbook search paths (in order):
<test_file_dir>/playbooks/<base_dir>/playbooks/- Repository-level playbook paths from config
Define inline tasks directly:
test_cases:
- name: "test_with_inline_tasks"
tasks:
- name: Set a variable
set_fact:
my_var: "hello"
- name: Verify variable
yako_assert:
stmts:
- actual: "{{ my_var }}"
expected: "hello"
Directory Structure
tests/yako/
├── test_basic.yaml # Simple: tests + playbooks together
├── playbooks/
│ └── shared_playbook.yaml
├── files/
│ └── test_data.txt
└── my_role_tests/ # Nested: organized by topic
├── test_install.yaml
├── playbooks/
│ └── install.yaml
└── files/
└── config.ini
Given: Test Setup
The given block configures the test environment. It can be defined at three levels, which merge together (global → module → test case):
filesandmock_tasks: concatenated across levelsextra_vars: merged as a dict (more specific level wins)
Extra Variables
Inject Ansible variables into the playbook run:
given:
extra_vars:
target_user: "deploy"
packages:
- nginx
- curl
Copying Files
Place files into the test workspace before execution:
given:
files:
# Simple: copies file with same name
- "config.ini"
# Explicit source and destination
- src: "fixtures/config.ini"
dest: "config.ini"
# Absolute destination path
- src: "hosts.txt"
dest: "/tmp/hosts.txt"
# Jinja2 template in destination
- src: "data.txt"
dest: "{{ target_dir }}/data.txt"
# Copy a directory (trailing slash)
- "test_data/"
Files are resolved from files/ directories adjacent to the test file or in the base directory.
Mocking Tasks
Mock tasks by matching their name exactly. When Ansible reaches a mocked task, yako's callback plugin intercepts it and replaces the real execution.
Basic Mock
Prevent a task from running without specifying any result:
given:
mock_tasks:
- name: "Install packages"
mock: {}
Mock with Results
Return specific values from the mocked task:
given:
mock_tasks:
- name: "Create temp file"
mock:
changed: true
result_dict:
path: "/tmp/fake_file"
Variables from result_dict are available to subsequent tasks via register.
Mock with Custom Action
Replace a task's module with a different one entirely:
given:
mock_tasks:
- name: "Create temp file"
mock:
custom_action:
set_fact:
temp_path: "/tmp/fake"
Per-Task Extra Variables
Inject variables that are only available during a specific task:
given:
mock_tasks:
- name: "Deploy application"
extra_vars:
deploy_version: "1.2.3"
mock: {}
Assertions
Callback Assertions: assert_inputs / assert_outputs
Assert on variables before (assert_inputs) or after (assert_outputs) a mocked task runs:
given:
mock_tasks:
- name: "Deploy to server"
mock: {}
assert_inputs:
- name: "target_host"
value: "prod-01"
- name: "deploy_version"
value: "1.0"
mode: "!="
assert_outputs:
- name: "result.path"
value: "/opt/app"
Inline Assertions: yako_assert Module
Use the yako_assert Ansible module inside tasks for assertions at any point in a playbook:
tasks:
- name: Set variables
set_fact:
count: 5
items: [1, 2, 3]
- name: Verify results
yako_assert:
stmts:
- actual: "{{ count }}"
expected: 5
mode: ">"
msg: "Count should be greater than 5"
- actual: "{{ items }}"
mode: "is_not_none"
Assertion Modes
| Mode | Description |
|---|---|
== |
Equal (default) |
!= |
Not equal |
< |
Less than |
> |
Greater than |
<= |
Less than or equal |
>= |
Greater than or equal |
in |
Value is in collection |
not_in |
Value is not in collection |
is_none |
Value is None |
is_not_none |
Value is not None |
is_true |
Value is truthy |
is_false |
Value is falsy |
is_not_true |
Value is not truthy |
is_not_false |
Value is not falsy |
State Checks
Verify task behavior without checking specific values:
given:
mock_tasks:
- name: "Conditional task"
should_be_skipped: true # Assert the task was skipped
mock: {}
- name: "Modify config"
should_be_changed: true # Assert the task reported changed
mock:
changed: true
- name: "Bad input handler"
should_fail: true # Assert the task failed
mock: {}
File Output for Debugging
Write actual/expected values to files for debugging complex comparisons:
assert_outputs:
- name: "large_config"
value: "{{ expected }}"
file: "both" # Options: "no" (default), "left", "right", "both"
Parametrization
Run the same test case with different inputs:
test_cases:
- name: "test_deploy"
tasks:
- name: Deploy
debug:
msg: "Deploying to {{ target_env }}"
parametrize:
staging:
extra_vars:
target_env: "staging"
production:
extra_vars:
target_env: "production"
This creates two test cases:
test_deploy.yaml::test_deploy[staging]test_deploy.yaml::test_deploy[production]
Each variant can override extra_vars, files, and mock_tasks.
Configuration
Yako loads configuration from yako.yaml (and optionally yako_local.yaml) in the repository root.
Runner Mode
runner_mode: "local" # or "docker"
local— Runs ansible-playbook directly on the hostdocker— Runs each test case in a fresh Docker container
Ansible Settings
Use the top-level ansible block for settings shared by both runner modes. Yako resolves
roles_path before each test run and writes the resulting paths into a generated
ansible.cfg, so the playbook under test can import local roles and roles from Git
repositories.
roles_pathaccepts either local paths or{ repo, path }entriesrepo_stagingmaps a Git URL to an existing local checkout instead of using the cache- unresolved Git repos are cloned into Yako's cache under
~/.cache/yako/repos/ ansible_playbookcontrols the generatedansible-playbookinvocationrunner.local.ansibleandrunner.docker.ansibleare merged with this block for the selected runner, so you can keep shared defaults at the top level and add runner-specific overrides when needed
ansible_playbook.connection, inventory, limit, and ansible_stdout_callback map
directly to the generated command and environment. Use extra_args for additional flags
such as --diff, --check, or -vvv.
ansible:
roles_path:
# Local path
- "roles/"
# Git repository (cloned and cached automatically)
- repo: "https://github.com/org/ansible-roles.git"
path: "roles"
# Reuse a local checkout instead of cloning the repo into the cache
repo_staging:
"https://github.com/org/ansible-roles.git": "../ansible-roles"
ansible_playbook:
connection: local
inventory: "127.0.0.1,"
limit: "127.0.0.1"
ansible_stdout_callback: "debug"
extra_args:
- "--diff"
Docker Runner
Use runner.docker when you want every test case to run inside a fresh container. The
defaults match this repository's Dockerfile, which places the virtual environment in
/home/ubuntu/app and the Yako source tree in /home/ubuntu/yako.
When the Docker runner starts, Yako automatically bind-mounts:
- resolved role paths
- the base test directories and discovered playbook directories
- the generated temporary workspace for the current test case
- the directory containing the test file, so adjacent
files/content remains available
Important fields:
image_nameselects the container image to runworkspace_diris the in-container temp workspace used for generated playbooks and test case configyako_venv_dirtells Yako where to findansible-playbookinside the imageyako_src_diris used to generateansible.cfgentries for Yako's callback and module plugins inside the containerextra_argsis appended todocker container runhost_yako_repo_diroptionally mounts your local Yako checkout at/home/ubuntu/yako, which is useful when developing Yako itself and testing the current source tree inside the container
You can also add Docker-only Ansible settings under runner.docker.ansible; they are
merged with the top-level ansible block when runner_mode: "docker" is active.
runner:
docker:
image_name: "ghcr.io/birnevogel11/yako:latest"
workspace_dir: "/home/ubuntu/workspace"
yako_venv_dir: "/home/ubuntu/app"
yako_src_dir: "/home/ubuntu/yako/src/yako"
extra_args:
- "--user=1000:1000"
host_yako_repo_dir: "." # Mount local yako source for Yako development
ansible:
ansible_playbook:
extra_args:
- "--check"
Global Given
Define defaults that apply to all test cases:
given:
extra_vars:
ansible_os_family: "Debian"
mock_tasks:
- name: "Gather facts"
mock: {}
CLI Usage
Examples below use uvx yako so you can run Yako without installing it. When developing inside this repository, replace uvx yako with uv run yako.
# Run all tests
uvx yako test
# Run tests in specific directories
uvx yako test tests/yako/networking/ tests/yako/storage/
# Filter tests by name
uvx yako test --filter-key "test_deploy"
# Verbose output
uvx yako test -v
# Custom config file
uvx yako test -c custom_yako.yaml
License
Yako is licensed under the GNU General Public License v3.0. See LICENSE for the
full text.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file yako-0.0.9.tar.gz.
File metadata
- Download URL: yako-0.0.9.tar.gz
- Upload date:
- Size: 82.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5eff991bac0b839cfc15b857f98976b3b7dba7c23c9be34d3e6030efe6576338
|
|
| MD5 |
7ce727b6630992681ec519ec5869fc0b
|
|
| BLAKE2b-256 |
95f0e29aba166862a666d528143491c09c951eebfbbb37381bf5abc441f9a462
|
Provenance
The following attestation bundles were made for yako-0.0.9.tar.gz:
Publisher:
pypi.yml on birnevogel11/yako
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
yako-0.0.9.tar.gz -
Subject digest:
5eff991bac0b839cfc15b857f98976b3b7dba7c23c9be34d3e6030efe6576338 - Sigstore transparency entry: 1109275746
- Sigstore integration time:
-
Permalink:
birnevogel11/yako@13f94c9007e3b5f506d144d41aed47fd454938f0 -
Branch / Tag:
refs/tags/v0.0.9 - Owner: https://github.com/birnevogel11
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi.yml@13f94c9007e3b5f506d144d41aed47fd454938f0 -
Trigger Event:
push
-
Statement type:
File details
Details for the file yako-0.0.9-py3-none-any.whl.
File metadata
- Download URL: yako-0.0.9-py3-none-any.whl
- Upload date:
- Size: 59.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6e785fbde43f4f903840ef1d37740b2b48c228a1e166681e98a47ffdfcf9f9b3
|
|
| MD5 |
8c93edb964df76359f596ca6b139a03a
|
|
| BLAKE2b-256 |
7565666c10660b14b93873a0cd6a9f5b754ad1afc4058b5541a4143444087690
|
Provenance
The following attestation bundles were made for yako-0.0.9-py3-none-any.whl:
Publisher:
pypi.yml on birnevogel11/yako
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
yako-0.0.9-py3-none-any.whl -
Subject digest:
6e785fbde43f4f903840ef1d37740b2b48c228a1e166681e98a47ffdfcf9f9b3 - Sigstore transparency entry: 1109275749
- Sigstore integration time:
-
Permalink:
birnevogel11/yako@13f94c9007e3b5f506d144d41aed47fd454938f0 -
Branch / Tag:
refs/tags/v0.0.9 - Owner: https://github.com/birnevogel11
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi.yml@13f94c9007e3b5f506d144d41aed47fd454938f0 -
Trigger Event:
push
-
Statement type: