Skip to content

Commit

Permalink
Update main readme: add hello world
Browse files Browse the repository at this point in the history
  • Loading branch information
Sispheor committed Dec 2, 2022
1 parent 16cc890 commit b3eb7fd
Show file tree
Hide file tree
Showing 5 changed files with 197 additions and 62 deletions.
166 changes: 149 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,25 +23,141 @@ Monkeyble allows, at task level, to:

Monkeyble is designed to be executed by a CI/CD in order to detect regressions when updating an Ansible code base. 🚀

## Quick tour
Complete documentation available [here](https://hewlettpackard.github.io/monkeyble)

## Hello Monkeyble

Let's consider this simple playbook
```yaml
- name: "Hello Monkeyble"
hosts: localhost
connection: local
gather_facts: false
become: false
vars:
who: "Monkeyble"

tasks:
- name: "First task"
set_fact:
hello_to_who: "Hello {{ who }}"

- name: "Second task"
debug:
msg: "{{ hello_to_who }}"

- when: "who != 'Monkeyble'"
name: "Should be skipped task"
debug:
msg: "You said hello to somebody else"

- name: "Push Monkeyble to a fake API"
uri:
url: "example.domain/monkeyble"
method: POST
body:
who: "{{ who }}"
body_format: json
```
Complete documentation available [here](https://hewlettpackard.github.io/monkeyble).
We prepare a yaml file that contains a test scenario
```yaml
# monkeyble_scenarios.yaml
monkeyble_scenarios:
validate_hello_monkey:
name: "Monkeyble hello world"
tasks_to_test:

- task: "First task"
test_output:
- assert_equal:
result_key: result.ansible_facts.hello_to_who
expected: "Hello Monkeyble"

- task: "Second task"
test_input:
- assert_equal:
arg_name: msg
expected: "Hello Monkeyble"

- task: "Should be skipped task"
should_be_skipped: true

- task: "Push Monkeyble to a fake API"
mock:
config:
monkeyble_module:
consider_changed: true
result_dict:
json:
id: 10
message: "monkey added"
```
Ansible resources are models of desired-state. Ansible modules have their own unit tests and guarantee you of their correct functioning.
As such, it's not necessary to test that services are started, packages are installed, or other such things.
Ansible is the system that will ensure these things are declaratively true.
We execute the playbook like by passing
- the dedicated ansible config that load Monkeyble (see install doc)
- the extra var file that contains our scenarios
- one extra var with the selected scenario to validate `validate_hello_monkey`

That being said, an Ansible playbook is commonly a bunch of data manipulation before calling a module that will perform a particular action.
For example, we get data from an API endpoint, or from the result of a module, we register a variable, then use a filter transform the data like combining two dictionary,
transforming into a list, changing the type, extract a specific value, etc... to finally call another module in a new task with the transformed data..
```bash
ANSIBLE_CONFIG="ansible.cfg" ansible-playbook -v \
tests/test_playbook.yml \
-e "@tests/monkeyble_scenarios.yml" \
-e "monkeyble_scenario=validate_hello_monkey"
```

Given a defined list of variable as input we want to be sure that a particular task:
Here is the output:
```
PLAY [Hello Monkeyble] *********************************************************************************************************************************************************************************************************************************************************************************************************************************************************************
🐵 Starting Monkeyble callback
monkeyble_scenario: validate_hello_monkey
Monkeyble scenario: Monkeyble hello world
TASK [First task] **************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************
ok: [localhost] => {"ansible_facts": {"hello_to_who": "Hello Monkeyble"}, "changed": false}
🙊 Monkeyble test output passed ✔
{"task": "First task", "monkeyble_passed_test": [{"test_name": "assert_equal", "tested_value": "Hello Monkeyble", "expected": "Hello Monkeyble"}], "monkeyble_failed_test": []}
TASK [Second task] *************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************
🙈 Monkeyble test input passed ✔
{"monkeyble_passed_test": [{"test_name": "assert_equal", "tested_value": "Hello Monkeyble", "expected": "Hello Monkeyble"}], "monkeyble_failed_test": []}
ok: [localhost] => {
"msg": "Hello Monkeyble"
}
TASK [Should be skipped task] **************************************************************************************************************************************************************************************************************************************************************************************************************************************************************
skipping: [localhost] => {}
🐵 Monkeyble - Task 'Should be skipped task' - expected 'should_be_skipped': True. actual state: True
TASK [Push Monkeyble to a fake API] ********************************************************************************************************************************************************************************************************************************************************************************************************************************************************
🙉 Monkeyble mock module - Before: 'uri' Now: 'monkeyble_module'
changed: [localhost] => {"changed": true, "json": {"id": 10, "message": "monkey added"}, "msg": "Monkeyble Mock module called. Original module: uri"}
PLAY RECAP *********************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************
localhost : ok=3 changed=1 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
🐵 Monkeyble - ALL TESTS PASSED ✔ - scenario: Monkeyble hello world
```

- is well called with the expected instantiated arguments
- produced this exact result
- has been skipped, changed or has failed
All tests have passed. The return code on stderr is **0**.

### Check input
Let's change the test to make it fail. We update the variable `who` at the beginning of the playbook.
```yaml
who: "Dog"
```

We execute the playbook the same way. The result is now the following:
```
ok: [localhost] => {"ansible_facts": {"hello_to_who": "Hello Dog"}, "changed": false}
🙊 Monkeyble failed scenario ❌: Monkeyble hello world
{"task": "First task", "monkeyble_passed_test": [], "monkeyble_failed_test": [{"test_name": "assert_equal", "tested_value": "Hello Dog", "expected": "Hello Monkeyble"}]}
```

This time the test has failed. The return code on stderr is **1**. The CI/CD It would have warned you that something has changed.

## Quick tour

### Test input

Monkeyble allows to check each instantiated argument value when the task is called:

Expand All @@ -66,7 +182,7 @@ Monkeyble support multiple test methods:
- assert_list_equal
- assert_dict_equal

### Check output
### Test output

Monkeyble allows to check the output result dictionary of a task

Expand All @@ -82,7 +198,7 @@ Monkeyble allows to check the output result dictionary of a task

Same methods as the `test_input` are supported.

### Task states
### Test task states

Monkeyble allow to check the states of a task

Expand Down Expand Up @@ -133,14 +249,30 @@ Playbook | Scenario | Test passed

The common testing strategy when using Ansible is to deploy to a staging environment that simulates the production.
When a role or a playbook is updated, we usually run an integration test battery against staging again before pushing in production.
In case of an update of the code base, a new execution will be required on the stating environment before the production one, etc...

But when our playbooks are exposed in an [Ansible Controller/AWX](https://www.ansible.com/products/controller) (ex Tower)
or available as a service in a catalog like [Squest](https://github.com/HewlettPackard/squest), we need to be sure that we don't have any regressions
when updating the code base, especially when modifying a role used by multiple playbooks. This is where Monkeyble is helpful. Placed in a CI/CD it will
be in charge of validating that the legacy code is always working as expected.
when updating the code base, especially when modifying a role used by multiple playbooks. Manually testing each playbook would be costly. We commonly give this kind of task to a CI/CD.

Furthermore, Ansible resources are models of desired-state. Ansible modules have their own unit tests and guarantee you of their correct functioning.
As such, it's not necessary to test that services are started, packages are installed, or other such things.
Ansible is the system that will ensure these things are declaratively true.

So finally, what do we need to test? An Ansible playbook is commonly a bunch of data manipulation before calling a module that will perform a particular action.
For example, we get data from an API endpoint, or from the result of a module, we register a variable, then use a filter transform the data like combining two dictionary,
transforming into a list, changing the type, extract a specific value, etc... to finally call another module in a new task with the transformed data..

Given a defined list of variable defined as input we want to be sure that a particular task:

- is well executed (the playbook could have failed before)
- is well called with the expected instantiated arguments
- produced this exact result
- has been skipped, changed or has failed

Monkeyble is a tool that can help you to enhance the quality of your Ansible code base and can be coupled
with [official best practices](https://docs.ansible.com/ansible/latest/reference_appendices/test_strategies.html).
Placed in a CI/CD it will be in charge of validating that the legacy code is always working as expected.

## Contribute

Expand Down
6 changes: 5 additions & 1 deletion docs/mock.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ monkeyble_scenarios:

Monkeyble comes with a mock module that return a configured dict.

Considering this task from a role or a playbook. We create a VM in a VMware hypervisor.
Consider a scenario where you are working with public cloud API or infrastructure module.
In the context of testing, you do not want to create a real instance of an object in the cloud like a VM or a container orchestrator.
But you still need eventually the returned dictionary so the playbook can be executed entirely.

In the following example, we create a VM in a VMware hypervisor.
```yaml
- name: "Create a virtual machine on given ESXi hostname"
community.vmware.vmware_guest:
Expand Down
2 changes: 1 addition & 1 deletion tests/local_play.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ def main():
tasks=[
# dict(action=dict(module='shell', args='ls'), register='shell_out'),
# dict(name="test_name2", action=dict(module='debug', args=dict(msg='{{ my_var }}')), when="my_var == 'to_be_updated'"),
dict(name="test_name2 with {{ task_name }}", action=dict(module='debug', args=dict(msg='general kenobi'))),
dict(name="other", action=dict(module='debug', args=dict(msg='{{ bla }}'))),
# dict(name="test_name3", action=dict(module='find', args=dict(path='/tmp'))),
# dict(name="test_name3", action=dict(module='set_fact', args=dict(new_var='{{ test_var }}'))),
# dict(name="test_name1", action=dict(module='debug', args=dict(msg='{{ my_var }}'))),
Expand Down
30 changes: 22 additions & 8 deletions tests/monkeyble_scenarios.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,33 @@ my_variable_1: "my_value_1"
hello_there: "Hello Monkeyble"

monkeyble_scenarios:
validate_test_1:
validate_hello_monkey:
name: "Monkeyble hello world"
tasks_to_test:
- task: "debug task"
- task: "First task"
test_output:
- assert_equal:
result_key: result.ansible_facts.hello_to_who
expected: "Hello Monkeyble"

- task: "Second task"
test_input:
- assert_equal:
arg_name: msg
expected: "{{ hello_there }}"
validate_test_2:
name: "Test 2"
tasks_to_test:
- task: "debug task"
test_input: "{{ test_input_config_1 }}"
expected: "Hello Monkeyble"

- task: "Should be skipped task"
should_be_skipped: true

- task: "Push Monkeyble to a fake API"
mock:
config:
monkeyble_module:
consider_changed: true
result_dict:
json:
id: 10
message: "monkey added"

# play: "play"
# role: "ddzdz"
Expand Down
55 changes: 20 additions & 35 deletions tests/test_playbook.yml
Original file line number Diff line number Diff line change
@@ -1,46 +1,31 @@
# ANSIBLE_CONFIG="ansible.cfg" ansible-playbook test_strategy_playbook.yml

- name: "play1"
- name: "Hello Monkeyble"
hosts: localhost
connection: local
gather_facts: false
become: false
vars:
my_var: "Hello Monkeyble"
who: "Monkeyble"

tasks:
- name: "debug task"
debug:
msg: "Hello Monkeyble"
- name: "First task"
set_fact:
hello_to_who: "Hello {{ who }}"

# - name: "task2"
# debug:
# msg: "{{ my_var }}"
- name: "Second task"
debug:
msg: "{{ hello_to_who }}"

# - when: my_var == "var1"
# name: "test_name2"
# debug:
# msg: "{{ my_var }}"
# - name: "test_name2"
# fail:
# msg: "this is a fail"
# ignore_errors: true
# - name: "next after fail"
# debug:
# msg: "test"
# roles:
# - role: test_role
# - role: test_role2
- when: "who != 'Monkeyble'"
name: "Should be skipped task"
debug:
msg: "You said hello to somebody else"

#- name: "Play2"
# hosts: localhost
# connection: local
# gather_facts: false
# become: false
#
# vars:
# my_var: "var2"
#
# tasks:
# - name: "Task test 2"
# debug:
# msg: "This is another test {{ my_var }}"
- name: "Push Monkeyble to a fake API"
uri:
url: "example.domain/monkeyble"
method: POST
body:
who: "{{ who }}"
body_format: json

0 comments on commit b3eb7fd

Please sign in to comment.