<aside> 🧭

Module 11 · Testing, Molecule & CI/CD

Proving your automation works before it touches a server. This is the module that separates "I write playbooks" from "I ship automation other people depend on".

🧠 concept → 🧪 exercise → ✅ expected result (hidden) → 🎯 interview questions (answers hidden)

Prerequisite: Modules 01–10.

</aside>


Part A · The testing pyramid

A1 · What can go wrong, and what catches it

<aside> ✍️

The analogy. Think of the stages a piece of writing goes through before it is published. Spellcheck catches typos in a second. Reading it back aloud catches sentences that do not parse. A colleague catches an argument that does not follow. And handing it to a stranger catches the thing all three of you were too close to notice. You run them in that order because each one costs more than the last — there is no sense asking a colleague to read something still full of typos.

yamllint is spellcheck, ansible-lint is reading it back, and Molecule is the stranger. Only the stranger can tell you whether the thing actually works, and only spellcheck is cheap enough to run on every keystroke. The whole of this module is about putting each check at the point in the day where it is worth its cost.

</aside>

<aside> 📖

Official docs: Testing strategies · ansible-lint · Molecule

</aside>

flowchart TD
    A["yamllint<br>milliseconds"] --> A1["Is it valid YAML?<br>Tabs, indentation, line length"]
    B["--syntax-check<br>under a second"] --> B1["Does Ansible parse it?<br>Unknown keywords, bad structure"]
    C["ansible-lint<br>seconds"] --> C1["Is it GOOD Ansible?<br>Unnamed tasks, missing FQCN,<br>shell where a module exists"]
    D["--check --diff<br>seconds, needs hosts"] --> D1["What WOULD change<br>against a real host?"]
    E["Molecule<br>minutes"] --> E1["Does it actually work?<br>Real container, real run,<br>IDEMPOTENCE verified"]
    F["Staging run<br>minutes"] --> F1["Does it work on<br>real infrastructure?"]
    style A fill:#D1FAE5,stroke:#059669
    style E fill:#DDD6FE,stroke:#7C3AED,stroke-width:2px
    style F fill:#FEF3C7,stroke:#D97706

<aside> 🔑

Run them cheapest-first, and gate the pipeline on each. yamllint and --syntax-check cost nothing and catch the whole class of parse errors. ansible-lint catches the design mistakes this course has warned about repeatedly — unnamed tasks, missing FQCN, unquoted file modes, shell where a module exists.

Only Molecule proves idempotency, and idempotency is the property nothing else can verify without actually running twice against something real.

</aside>

A2 · The cheap layers

# 1. YAML validity
yamllint .

# 2. Does Ansible parse it?
ansible-playbook site.yml --syntax-check
ansible-inventory -i inventory/ --graph      # inventory parses too

# 3. Is it good Ansible?
ansible-lint

# 4. What would change?
ansible-playbook site.yml --check --diff --limit staging01
# .yamllint
---
extends: default
rules:
  line-length:
    max: 160
    level: warning
  truthy:
    allowed-values: ["true", "false"]     # catches yes/no/on/off - the Norway problem
  comments:
    min-spaces-from-content: 1
  braces:
    max-spaces-inside: 1                   # allows {{ var }} spacing
ignore: |
  collections/
  galaxy_roles/
  .venv/

<aside> 💡

The truthy rule is worth enabling deliberately. It forces true/false and rejects yes/no/on/off — which is the Module 02 Part A2 Norway problem caught at lint time rather than in production. Similarly, braces: max-spaces-inside: 1 stops yamllint fighting with normal {{ var }} spacing.

</aside>

🎯 Interview questions — Testing strategy


Part B · ansible-lint

B1 · Running it