├── VERSION ├── PROJECT_NAME ├── examples ├── pythonapi │ ├── app │ │ ├── __init__.py │ │ └── todo.py │ ├── requirements.txt │ ├── PROJECT_NAME │ ├── tests │ │ ├── hitchreqs.in │ │ ├── docstory.yml │ │ ├── docgen.py │ │ └── Dockerfile-hitch │ ├── docs │ │ ├── correct-my-spelling.md │ │ └── add-and-retrieve-todo.md │ ├── story │ │ ├── correct-my-spelling.story │ │ └── add-todo.story │ ├── LICENSE.txt │ └── README.md ├── website │ ├── app │ │ ├── todos │ │ │ ├── __init__.py │ │ │ ├── migrations │ │ │ │ ├── __init__.py │ │ │ │ ├── 0006_remove_todo_deadline.py │ │ │ │ ├── 0007_auto_20191202_0323.py │ │ │ │ ├── 0004_auto_20191202_0004.py │ │ │ │ ├── 0005_auto_20191202_0011.py │ │ │ │ ├── 0008_auto_20191202_0809.py │ │ │ │ ├── 0002_auto_20191201_2357.py │ │ │ │ ├── 0001_initial.py │ │ │ │ └── 0003_auto_20191202_0000.py │ │ │ ├── admin.py │ │ │ ├── apps.py │ │ │ ├── models.py │ │ │ ├── urls.py │ │ │ ├── tests.py │ │ │ ├── templates │ │ │ │ └── todos │ │ │ │ │ ├── base.html │ │ │ │ │ └── index.html │ │ │ └── views.py │ │ ├── todoApp │ │ │ ├── __init__.py │ │ │ ├── views.py │ │ │ ├── wsgi.py │ │ │ └── urls.py │ │ ├── .coveragerc │ │ ├── requirements.in │ │ ├── staticfiles │ │ │ ├── todoApp.png │ │ │ └── css │ │ │ │ └── style.css │ │ ├── requirements.txt │ │ ├── templates │ │ │ └── registration │ │ │ │ └── login.html │ │ └── manage.py │ ├── hitch │ │ ├── PROJECT_NAME │ │ ├── docs │ │ │ ├── login.gif │ │ │ ├── correct-my-spelling.gif │ │ │ ├── add-and-retrieve-todo.gif │ │ │ ├── login-0-load_website.png │ │ │ ├── login-6-should_appear.png │ │ │ ├── correct-my-spelling-0-load_website.png │ │ │ ├── add-and-retrieve-todo-0-load_website.png │ │ │ ├── add-and-retrieve-todo-3-should_appear.png │ │ │ ├── add-and-retrieve-todo-4-should_appear.png │ │ │ ├── add-and-retrieve-todo-6-should_appear.png │ │ │ ├── add-and-retrieve-todo-7-should_appear.png │ │ │ ├── add-and-retrieve-todo-9-should_appear.png │ │ │ ├── correct-my-spelling-3-should_appear.png │ │ │ ├── correct-my-spelling-4-should_appear.png │ │ │ ├── correct-my-spelling-6-should_appear.png │ │ │ ├── correct-my-spelling-7-should_appear.png │ │ │ ├── correct-my-spelling-9-should_appear.png │ │ │ ├── add-and-retrieve-todo.md │ │ │ └── correct-my-spelling.md │ │ ├── selectors │ │ │ ├── selectors.template │ │ │ └── selectors.yml │ │ ├── README.md │ │ ├── directories.py │ │ ├── Dockerfile-playwright │ │ ├── playwright-entrypoint.sh │ │ ├── Dockerfile-hitch │ │ ├── story │ │ │ ├── correct-my-spelling.story │ │ │ ├── story.template │ │ │ ├── add-todo.story │ │ │ └── accounts.story │ │ ├── utils.py │ │ ├── test_integration.py │ │ ├── hitchreqs.in │ │ ├── test_legacy.py │ │ ├── conftest.py │ │ ├── docgen.py │ │ ├── video.py │ │ ├── cli.py │ │ ├── compare_screenshots.py │ │ ├── podman-compose.yml │ │ └── docstory.yml │ ├── Dockerfile │ └── LICENSE.txt ├── commandline │ ├── PROJECT_NAME │ ├── app │ │ ├── data.json │ │ └── main.py │ ├── tests │ │ ├── hitchreqs.in │ │ ├── docstory.yml │ │ ├── Dockerfile-hitch │ │ ├── docgen.py │ │ └── test_integration.py │ ├── Dockerfile │ ├── docs │ │ ├── add-and-retrieve-todo.md │ │ └── correct-my-spelling.md │ ├── story │ │ ├── add-todo.story │ │ └── correct-my-spelling.story │ ├── LICENSE.txt │ └── README.md ├── llm │ ├── .gitignore │ ├── hitch │ │ ├── PROJECT_NAME │ │ ├── README.md │ │ ├── directories.py │ │ ├── hitchreqs.in │ │ ├── Dockerfile-hitch │ │ ├── story │ │ │ ├── barista.story │ │ │ └── buy-coffee.story │ │ ├── cli.py │ │ ├── docgen.py │ │ ├── llm.py │ │ └── hitchreqs.txt │ └── LICENSE.txt ├── restapi │ ├── hitch │ │ ├── PROJECT_NAME │ │ ├── hitchreqs.in │ │ ├── debugrequirements.txt │ │ ├── Dockerfile-mitm │ │ ├── directories.py │ │ ├── docgen.py │ │ ├── docstory.yml │ │ ├── podman.py │ │ ├── Dockerfile-hitch │ │ ├── docs │ │ │ ├── add-and-retrieve-todo.md │ │ │ └── correct-my-spelling.md │ │ ├── utils.py │ │ ├── story │ │ │ ├── correct-my-spelling.story │ │ │ └── add-todo.story │ │ ├── test_integration.py │ │ ├── podman-compose.yml │ │ └── cli.py │ ├── Dockerfile │ ├── LICENSE.txt │ └── app │ │ └── api.py └── README.md ├── .hitchprojectsync ├── .gitattributes ├── hitch ├── hitchreqs.in ├── mockcode │ ├── mockemailchecker.py │ └── mockselenium.py ├── debugrequirements.txt ├── pyproject.toml ├── Dockerfile-hitch ├── story │ ├── no-stories.story │ ├── one-story.story │ ├── inheritance-from-non-existent-story.story │ ├── invalid-stories.story │ ├── expected-exceptions.story │ ├── unique-names.story │ ├── invalid-yaml.story │ ├── abort.story │ ├── matching-strings.story │ ├── success.story │ ├── bugs.story │ ├── documentation-extra-vars-and-functions.story │ ├── matching-json.story │ ├── gradual-typing.story │ ├── shortcut.story │ └── rewrite-subkey-of-argument.story ├── code_that_does_things.py └── envirotest.py ├── setup.py ├── docs ├── public │ ├── using │ │ ├── index.md │ │ ├── setup │ │ │ └── index.md │ │ ├── pytest │ │ │ └── index.md │ │ ├── documentation │ │ │ └── index.md │ │ ├── behavior │ │ │ ├── index.md │ │ │ ├── aborting.md │ │ │ └── run-single-named-story.md │ │ ├── runner │ │ │ ├── index.md │ │ │ ├── run-just-one-story.md │ │ │ └── shortcut-lookup.md │ │ ├── inheritance │ │ │ └── index.md │ │ └── engine │ │ │ ├── index.md │ │ │ ├── match-two-strings.md │ │ │ ├── match-json.md │ │ │ └── gradual-typing.md │ ├── sliced-cucumber.jpg │ ├── approach │ │ ├── triality-example.png │ │ ├── test-or-story.md │ │ ├── index.md │ │ ├── executable-specifications.md │ │ ├── human-writable.md │ │ ├── hermetic-end-to-end-test.md │ │ └── flaky-tests.md │ ├── why │ │ ├── inheritance.md │ │ ├── declarative.md │ │ ├── index.md │ │ ├── principles.md │ │ ├── pure-python-no-cli.md │ │ ├── interesting-to-the-business.md │ │ └── given-when-then.md │ └── why-not │ │ └── index.md └── src │ ├── sliced-cucumber.jpg │ ├── approach │ ├── triality-example.png │ ├── index.md │ ├── test-or-story.md │ ├── executable-specifications.md │ ├── human-writable.md │ ├── hermetic-end-to-end-test.md │ └── flaky-tests.md │ ├── using │ ├── index.md │ ├── behavior │ │ └── index.md │ ├── engine │ │ └── index.md │ ├── runner │ │ └── index.md │ ├── setup │ │ └── index.md │ ├── pytest │ │ └── index.md │ ├── inheritance │ │ └── index.md │ └── documentation │ │ └── index.md │ ├── why │ ├── index.md │ ├── inheritance.md │ ├── declarative.md │ ├── principles.md │ ├── pure-python-no-cli.md │ ├── interesting-to-the-business.md │ └── given-when-then.md │ └── why-not │ └── index.md ├── hitchstory ├── templates │ ├── success.jinja2 │ ├── failure.jinja2 │ └── multiple.jinja2 ├── __init__.py └── story_list.py ├── MANIFEST.in ├── .gitignore ├── .github └── workflows │ ├── docpublish.yml │ ├── regression.yml │ ├── examples.yml │ └── example-website.yml └── pyproject.toml /VERSION: -------------------------------------------------------------------------------- 1 | 0.24.2 2 | -------------------------------------------------------------------------------- /PROJECT_NAME: -------------------------------------------------------------------------------- 1 | hitchstory 2 | -------------------------------------------------------------------------------- /examples/pythonapi/app/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/website/app/todos/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.hitchprojectsync: -------------------------------------------------------------------------------- 1 | source: ../basehitch 2 | -------------------------------------------------------------------------------- /examples/website/app/todoApp/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/commandline/PROJECT_NAME: -------------------------------------------------------------------------------- 1 | todo-cli 2 | -------------------------------------------------------------------------------- /examples/llm/.gitignore: -------------------------------------------------------------------------------- 1 | hitch/OPENAI_API_KEY 2 | -------------------------------------------------------------------------------- /examples/llm/hitch/PROJECT_NAME: -------------------------------------------------------------------------------- 1 | llm-tests 2 | -------------------------------------------------------------------------------- /examples/pythonapi/requirements.txt: -------------------------------------------------------------------------------- 1 | textblob 2 | -------------------------------------------------------------------------------- /examples/website/hitch/PROJECT_NAME: -------------------------------------------------------------------------------- 1 | todo-web 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.story linguist-language=YAML 2 | -------------------------------------------------------------------------------- /examples/pythonapi/PROJECT_NAME: -------------------------------------------------------------------------------- 1 | todo-pythonapi 2 | -------------------------------------------------------------------------------- /examples/restapi/hitch/PROJECT_NAME: -------------------------------------------------------------------------------- 1 | todo-restapi 2 | -------------------------------------------------------------------------------- /examples/website/app/todos/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hitch/hitchreqs.in: -------------------------------------------------------------------------------- 1 | hitchpylibrarytoolkit==0.6.23 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup() 4 | -------------------------------------------------------------------------------- /hitch/mockcode/mockemailchecker.py: -------------------------------------------------------------------------------- 1 | def email_was_sent(): 2 | print("Email was sent") 3 | -------------------------------------------------------------------------------- /examples/website/app/.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | relative_files = True 3 | data_file=single.coverage 4 | -------------------------------------------------------------------------------- /docs/public/using/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Using HitchStory 3 | --- 4 | 5 | How to: 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/src/sliced-cucumber.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitchdev/hitchstory/HEAD/docs/src/sliced-cucumber.jpg -------------------------------------------------------------------------------- /examples/commandline/app/data.json: -------------------------------------------------------------------------------- 1 | ["Resting", "why i can add this?", "fads", "asdfsadfa", "fluff", "dsfdsafdas"] -------------------------------------------------------------------------------- /docs/public/sliced-cucumber.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitchdev/hitchstory/HEAD/docs/public/sliced-cucumber.jpg -------------------------------------------------------------------------------- /examples/website/app/requirements.in: -------------------------------------------------------------------------------- 1 | django==4.2.10 2 | textblob 3 | web-pdb 4 | psycopg2-binary==2.9.1 5 | coverage==7.2.5 6 | -------------------------------------------------------------------------------- /docs/src/approach/triality-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitchdev/hitchstory/HEAD/docs/src/approach/triality-example.png -------------------------------------------------------------------------------- /docs/src/using/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Using HitchStory 3 | --- 4 | 5 | How to: 6 | 7 | {{{{ using-index-contents.txt }}}} 8 | 9 | -------------------------------------------------------------------------------- /examples/website/hitch/docs/login.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitchdev/hitchstory/HEAD/examples/website/hitch/docs/login.gif -------------------------------------------------------------------------------- /docs/public/approach/triality-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitchdev/hitchstory/HEAD/docs/public/approach/triality-example.png -------------------------------------------------------------------------------- /examples/website/app/todos/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Todo 3 | 4 | admin.site.register(Todo) 5 | -------------------------------------------------------------------------------- /examples/website/app/staticfiles/todoApp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitchdev/hitchstory/HEAD/examples/website/app/staticfiles/todoApp.png -------------------------------------------------------------------------------- /examples/website/app/todos/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class TodosConfig(AppConfig): 5 | name = "todos" 6 | -------------------------------------------------------------------------------- /examples/commandline/tests/hitchreqs.in: -------------------------------------------------------------------------------- 1 | hitchstory 2 | click 3 | ipython 4 | commandlib 5 | icommandlib 6 | requests 7 | pip-tools 8 | pytest 9 | -------------------------------------------------------------------------------- /docs/src/using/behavior/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Using HitchStory 3 | --- 4 | 5 | How to: 6 | 7 | {{{{ using-behavior-index-contents.txt }}}} 8 | 9 | -------------------------------------------------------------------------------- /hitch/debugrequirements.txt: -------------------------------------------------------------------------------- 1 | ipykernel 2 | pip 3 | q 4 | pudb 5 | psutil 6 | sure 7 | ensure 8 | web-pdb 9 | pytest 10 | click 11 | pytest 12 | -------------------------------------------------------------------------------- /docs/src/using/engine/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Using HitchStory - Engine 3 | --- 4 | 5 | How to: 6 | 7 | {{{{ using-engine-index-contents.txt }}}} 8 | 9 | -------------------------------------------------------------------------------- /docs/src/using/runner/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Using HitchStory - Runner 3 | --- 4 | 5 | How to: 6 | 7 | {{{{ using-runner-index-contents.txt }}}} 8 | 9 | -------------------------------------------------------------------------------- /docs/src/using/setup/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Using HitchStory - Set up 3 | --- 4 | 5 | How to: 6 | 7 | {{{{ using-setup-index-contents.txt }}}} 8 | 9 | -------------------------------------------------------------------------------- /examples/website/hitch/docs/correct-my-spelling.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitchdev/hitchstory/HEAD/examples/website/hitch/docs/correct-my-spelling.gif -------------------------------------------------------------------------------- /docs/src/using/pytest/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Using HitchStory - With pytest 3 | --- 4 | 5 | How to: 6 | 7 | {{{{ using-pytest-index-contents.txt }}}} 8 | 9 | -------------------------------------------------------------------------------- /examples/restapi/hitch/hitchreqs.in: -------------------------------------------------------------------------------- 1 | hitchstory>=0.22.0 2 | click 3 | ipython 4 | commandlib 5 | icommandlib 6 | requests 7 | pip-tools 8 | pytest 9 | podman-compose -------------------------------------------------------------------------------- /examples/website/hitch/docs/add-and-retrieve-todo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitchdev/hitchstory/HEAD/examples/website/hitch/docs/add-and-retrieve-todo.gif -------------------------------------------------------------------------------- /examples/website/hitch/docs/login-0-load_website.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitchdev/hitchstory/HEAD/examples/website/hitch/docs/login-0-load_website.png -------------------------------------------------------------------------------- /examples/website/hitch/docs/login-6-should_appear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitchdev/hitchstory/HEAD/examples/website/hitch/docs/login-6-should_appear.png -------------------------------------------------------------------------------- /examples/pythonapi/tests/hitchreqs.in: -------------------------------------------------------------------------------- 1 | hitchrunpy 2 | hitchstory 3 | click 4 | ipython 5 | templex 6 | commandlib 7 | requests 8 | pip-tools 9 | pytest 10 | -------------------------------------------------------------------------------- /hitchstory/templates/success.jinja2: -------------------------------------------------------------------------------- 1 | STORY RAN SUCCESSFULLY {{ result.story.filename }}: {{ result.story.name }} in {{ result.duration|round(1, 'ceil') }} seconds. 2 | -------------------------------------------------------------------------------- /docs/src/approach/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: General Approach 3 | --- 4 | 5 | HitchStory best practices are documented here: 6 | 7 | {{{{ approach-index-contents.txt }}}} 8 | -------------------------------------------------------------------------------- /docs/src/using/inheritance/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Using HitchStory - Inheritance 3 | --- 4 | 5 | How to: 6 | 7 | {{{{ using-inheritance-index-contents.txt }}}} 8 | 9 | -------------------------------------------------------------------------------- /examples/commandline/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8-slim-buster 2 | 3 | RUN pip install flask textblob 4 | 5 | ADD app/ /app 6 | 7 | ENTRYPOINT ["python", "/app/main.py"] 8 | -------------------------------------------------------------------------------- /examples/restapi/hitch/debugrequirements.txt: -------------------------------------------------------------------------------- 1 | flake8 2 | ipython #==1.2.1 3 | pyzmq 4 | path.py 5 | q 6 | ipykernel 7 | sure 8 | ensure 9 | python-slugify 10 | pytest 11 | -------------------------------------------------------------------------------- /docs/public/using/setup/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Using HitchStory - Set up 3 | --- 4 | 5 | How to: 6 | 7 | - [Creating a basic command line test runner](basic-cli) 8 | 9 | 10 | -------------------------------------------------------------------------------- /docs/src/using/documentation/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Using HitchStory - Documentation Generation 3 | --- 4 | 5 | How to: 6 | 7 | {{{{ using-documentation-index-contents.txt }}}} 8 | 9 | -------------------------------------------------------------------------------- /examples/website/hitch/docs/correct-my-spelling-0-load_website.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitchdev/hitchstory/HEAD/examples/website/hitch/docs/correct-my-spelling-0-load_website.png -------------------------------------------------------------------------------- /examples/website/hitch/docs/add-and-retrieve-todo-0-load_website.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitchdev/hitchstory/HEAD/examples/website/hitch/docs/add-and-retrieve-todo-0-load_website.png -------------------------------------------------------------------------------- /examples/website/hitch/docs/add-and-retrieve-todo-3-should_appear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitchdev/hitchstory/HEAD/examples/website/hitch/docs/add-and-retrieve-todo-3-should_appear.png -------------------------------------------------------------------------------- /examples/website/hitch/docs/add-and-retrieve-todo-4-should_appear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitchdev/hitchstory/HEAD/examples/website/hitch/docs/add-and-retrieve-todo-4-should_appear.png -------------------------------------------------------------------------------- /examples/website/hitch/docs/add-and-retrieve-todo-6-should_appear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitchdev/hitchstory/HEAD/examples/website/hitch/docs/add-and-retrieve-todo-6-should_appear.png -------------------------------------------------------------------------------- /examples/website/hitch/docs/add-and-retrieve-todo-7-should_appear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitchdev/hitchstory/HEAD/examples/website/hitch/docs/add-and-retrieve-todo-7-should_appear.png -------------------------------------------------------------------------------- /examples/website/hitch/docs/add-and-retrieve-todo-9-should_appear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitchdev/hitchstory/HEAD/examples/website/hitch/docs/add-and-retrieve-todo-9-should_appear.png -------------------------------------------------------------------------------- /examples/website/hitch/docs/correct-my-spelling-3-should_appear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitchdev/hitchstory/HEAD/examples/website/hitch/docs/correct-my-spelling-3-should_appear.png -------------------------------------------------------------------------------- /examples/website/hitch/docs/correct-my-spelling-4-should_appear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitchdev/hitchstory/HEAD/examples/website/hitch/docs/correct-my-spelling-4-should_appear.png -------------------------------------------------------------------------------- /examples/website/hitch/docs/correct-my-spelling-6-should_appear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitchdev/hitchstory/HEAD/examples/website/hitch/docs/correct-my-spelling-6-should_appear.png -------------------------------------------------------------------------------- /examples/website/hitch/docs/correct-my-spelling-7-should_appear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitchdev/hitchstory/HEAD/examples/website/hitch/docs/correct-my-spelling-7-should_appear.png -------------------------------------------------------------------------------- /examples/website/hitch/docs/correct-my-spelling-9-should_appear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hitchdev/hitchstory/HEAD/examples/website/hitch/docs/correct-my-spelling-9-should_appear.png -------------------------------------------------------------------------------- /docs/public/using/pytest/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Using HitchStory - With pytest 3 | --- 4 | 5 | How to: 6 | 7 | - [Self rewriting tests with pytest and hitchstory](rewrite) 8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/restapi/hitch/Dockerfile-mitm: -------------------------------------------------------------------------------- 1 | FROM docker.io/mitmproxy/mitmproxy:10.1.1 2 | 3 | ENV DEBIAN_FRONTEND=noninteractive 4 | 5 | RUN apt-get update && \ 6 | apt-get install netcat -y 7 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include VERSION LICENSE.txt README.md CHANGELOG.md 2 | recursive-include hitchstory/templates *.jinja2 3 | global-exclude __pycache__ 4 | global-exclude *.py[co] 5 | global-exclude .git* 6 | -------------------------------------------------------------------------------- /examples/website/app/todoApp/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import redirect 2 | from django.contrib.auth.decorators import login_required 3 | 4 | @login_required 5 | def index(request): 6 | return redirect('/todos') 7 | -------------------------------------------------------------------------------- /hitch/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "hitchenv-hitchstory" 3 | version = "0.0.0" 4 | requires-python = ">=3.14.0" 5 | dependencies = ["hitchpylibrarytoolkit"] 6 | 7 | [tool.uv.sources] 8 | hitchpylibrarytoolkit = { path = "../hitchpylibrarytoolkit", editable = true } 9 | -------------------------------------------------------------------------------- /docs/public/using/documentation/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Using HitchStory - Documentation Generation 3 | --- 4 | 5 | How to: 6 | 7 | - [Generate documentation with extra variables and functions](extra) 8 | - [Generate documentation from story](generate) 9 | 10 | 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.kate-swp 2 | *.pyc 3 | .pytest_cache 4 | build/ 5 | dist/ 6 | hitchstory.egg-info/ 7 | docs/snippets/ 8 | docs/draft/ 9 | docs/src/using/ 10 | temp/ 11 | hitch/devenv.yml 12 | hitchpylibrarytoolkit 13 | failure.* 14 | examples/website/hitch/artefacts/ 15 | *.coverage 16 | -------------------------------------------------------------------------------- /examples/website/hitch/selectors/selectors.template: -------------------------------------------------------------------------------- 1 | # Docs https://hitchdev.com/hitchpage 2 | 3 | #name of a page: 4 | # element: 5 | # user readable element: playwright selector 6 | 7 | #name of another page: 8 | # element: 9 | # name of element: playwright selector 10 | 11 | -------------------------------------------------------------------------------- /examples/restapi/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8-slim-buster 2 | 3 | 4 | ENV DEBIAN_FRONTEND=noninteractive 5 | 6 | RUN apt-get update && \ 7 | apt-get install netcat -y 8 | 9 | 10 | RUN pip install flask textblob 11 | 12 | ADD app/ /app 13 | 14 | ENTRYPOINT ["python", "/app/api.py"] 15 | -------------------------------------------------------------------------------- /examples/llm/hitch/README.md: -------------------------------------------------------------------------------- 1 | # Hitch Testing Environment 2 | 3 | This environment is based upon the hitchstory testing framework. 4 | 5 | 6 | ## Installation 7 | 8 | Ensure that you have podman and md5sum installed first. 9 | 10 | Then, run: 11 | 12 | ``` 13 | ./run.sh make 14 | ``` 15 | 16 | -------------------------------------------------------------------------------- /examples/website/hitch/README.md: -------------------------------------------------------------------------------- 1 | # Hitch Testing Environment 2 | 3 | This environment is based upon the hitchstory testing framework. 4 | 5 | 6 | ## Installation 7 | 8 | Ensure that you have podman and md5sum installed first. 9 | 10 | Then, run: 11 | 12 | ``` 13 | ./run.sh make 14 | ``` 15 | 16 | -------------------------------------------------------------------------------- /hitchstory/templates/failure.jinja2: -------------------------------------------------------------------------------- 1 | {{ Fore.BLUE }}{{ Style.BRIGHT }}FAILURE IN {{ result.story.filename }}: 2 | "{{ result.story.name }}" in {{ result.duration|round(1, 'ceil') }} seconds. 3 | {{ Style.NORMAL }} 4 | 5 | {{ result.story_failure_snippet }} 6 | {{ Fore.RESET }} 7 | {{ result.stacktrace }} 8 | -------------------------------------------------------------------------------- /examples/website/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM docker.io/python:3.8-slim-buster 2 | 3 | ENV DEBIAN_FRONTEND=noninteractive 4 | 5 | RUN apt-get update && \ 6 | apt-get install netcat -y 7 | 8 | ADD app/ /app 9 | WORKDIR /app 10 | 11 | RUN pip install -r requirements.txt 12 | 13 | ENTRYPOINT ["python", "manage.py"] 14 | -------------------------------------------------------------------------------- /examples/website/app/requirements.txt: -------------------------------------------------------------------------------- 1 | asgiref==3.7.2 2 | asyncore-wsgi==0.0.11 3 | bottle==0.12.25 4 | click==8.1.3 5 | coverage==7.2.5 6 | django==4.2.10 7 | joblib==1.3.1 8 | nltk==3.8.1 9 | psycopg2-binary==2.9.1 10 | regex==2023.6.3 11 | sqlparse==0.4.4 12 | textblob==0.17.1 13 | tqdm==4.65.0 14 | web-pdb==1.6.0 15 | -------------------------------------------------------------------------------- /examples/website/app/staticfiles/css/style.css: -------------------------------------------------------------------------------- 1 | .page-header { 2 | margin-top: 30px; 3 | } 4 | 5 | .fa-trash-alt { 6 | color: red; 7 | float: right; 8 | cursor: pointer; 9 | } 10 | 11 | .todo-complete { 12 | text-decoration: line-through; 13 | } 14 | 15 | .todo-status-checkbox { 16 | cursor: pointer; 17 | margin-right: 10px; 18 | } -------------------------------------------------------------------------------- /examples/llm/hitch/directories.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | ROOT = Path(__file__).absolute().parents[0].parent 4 | 5 | 6 | class DIR: 7 | PROJECT = ROOT 8 | APP = ROOT / "app" 9 | ARTEFACTS = ROOT / "hitch" / "artefacts" 10 | DOCS = ROOT / "hitch" / "docs" 11 | STORY = ROOT / "hitch" / "story" 12 | HITCH = ROOT / "hitch" 13 | -------------------------------------------------------------------------------- /docs/public/using/behavior/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Using HitchStory 3 | --- 4 | 5 | How to: 6 | 7 | - [Abort a story with ctrl-C](aborting) 8 | - [Upgrade breaking changes between v0.14 and v0.15](breaking-changes-between-v014-and-v015) 9 | - [Handling failing tests](failing-tests) 10 | - [Running a single named story successfully](run-single-named-story) 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/restapi/hitch/directories.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | ROOT = Path(__file__).absolute().parents[0].parent 4 | 5 | 6 | class DIR: 7 | PROJECT = ROOT 8 | APP = ROOT / "app" 9 | ARTEFACTS = ROOT / "artefacts" 10 | DOCS = ROOT / "hitch" / "docs" 11 | STORY = ROOT / "hitch" / "story" 12 | DATACACHE = Path("/gen") 13 | HITCH = ROOT / "hitch" -------------------------------------------------------------------------------- /examples/llm/hitch/hitchreqs.in: -------------------------------------------------------------------------------- 1 | # Requirements for running the integration tests 2 | # This is compiled into pinned versions in the hitchreqs.txt file. 3 | # This is separate from the application requirements.txt / requirements.in 4 | # Run "./run.sh make hitchreqs" to rebuild hitchreqs.txt. 5 | 6 | hitchstory>=0.23.0 # for strictyaml story interpretation 7 | openai>=1.12.0 # for interacting with openai 8 | -------------------------------------------------------------------------------- /examples/website/app/todos/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Todo(models.Model): 5 | title = models.CharField(max_length=100) 6 | created_at = models.DateTimeField("Created", auto_now_add=True) 7 | update_at = models.DateTimeField("Updated", auto_now=True) 8 | isCompleted = models.BooleanField(default=False) 9 | 10 | def __str__(self): 11 | return self.title 12 | -------------------------------------------------------------------------------- /examples/website/app/todos/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from . import views 3 | from django.urls import path, include 4 | 5 | app_name = "todos" 6 | urlpatterns = [ 7 | path("", views.IndexView.as_view(), name="index"), 8 | path("/delete", views.delete, name="delete"), 9 | path("/update", views.update, name="update"), 10 | path("add/", views.add, name="add"), 11 | ] 12 | -------------------------------------------------------------------------------- /docs/src/why/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Design Justifications 3 | --- 4 | 5 | HitchStory is the result of some carefully considered, although 6 | controversial design decisions. These are justified here. 7 | 8 | {{{{ why-index-contents.txt }}}} 9 | 10 | Rebuttals and critiques, especially from users and designers of 11 | competing tools are welcome. Either raise a ticket on github 12 | or open a pull request with a link. 13 | -------------------------------------------------------------------------------- /docs/public/using/runner/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Using HitchStory - Runner 3 | --- 4 | 5 | How to: 6 | 7 | - [Continue on failure when playing multiple stories](continue-on-failure) 8 | - [Flaky story detection](flaky-story-detection) 9 | - [Play multiple stories in sequence](play-multiple-stories-in-sequence) 10 | - [Run one story in collection](run-just-one-story) 11 | - [Shortcut lookup for story names](shortcut-lookup) 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/website/hitch/directories.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | ROOT = Path(__file__).absolute().parents[0].parent 4 | 5 | 6 | class DIR: 7 | PROJECT = ROOT 8 | APP = ROOT / "app" 9 | ARTEFACTS = ROOT / "hitch" / "artefacts" 10 | DOCS = ROOT / "hitch" / "docs" 11 | STORY = ROOT / "hitch" / "story" 12 | HITCH = ROOT / "hitch" 13 | SELECTORS = ROOT / "hitch" / "selectors" 14 | DATACACHE = Path("/gen") 15 | -------------------------------------------------------------------------------- /examples/pythonapi/docs/correct-my-spelling.md: -------------------------------------------------------------------------------- 1 | # Correct my spelling 2 | 3 | In this story we call the Python API and send it misspellings. 4 | 5 | The API uses TextBlob (https://textblob.readthedocs.io/en/dev/) 6 | to detect misspellings and raises an exception with a correction. 7 | 8 | 9 | 10 | ```python 11 | import todo 12 | todo.add_item("biuy breod") 13 | 14 | ``` 15 | 16 | 17 | 18 | ```python 19 | Did you mean "buy bread"? 20 | ``` 21 | 22 | -------------------------------------------------------------------------------- /examples/restapi/hitch/docgen.py: -------------------------------------------------------------------------------- 1 | from hitchstory import StoryCollection 2 | from test_integration import Engine 3 | from directories import DIR 4 | 5 | 6 | storydocs = ( 7 | StoryCollection(DIR.STORY.glob("*.story"), Engine()) 8 | .with_documentation(DIR.HITCH.joinpath("docstory.yml").read_text()) 9 | .ordered_by_file() 10 | ) 11 | 12 | for story in storydocs: 13 | DIR.DOCS.joinpath(story.slug + ".md").write_text(story.documentation()) 14 | -------------------------------------------------------------------------------- /examples/website/app/todos/migrations/0006_remove_todo_deadline.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.7 on 2019-12-01 20:59 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('todos', '0005_auto_20191202_0011'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='todo', 15 | name='deadline', 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /examples/website/hitch/selectors/selectors.yml: -------------------------------------------------------------------------------- 1 | login: 2 | element: 3 | username: input[data-testid="username"] 4 | password: input[data-testid="password"] 5 | submit: button[data-testid="submit"] 6 | dashboard: 7 | element: 8 | first todo list item: .test-todo-list-item>>nth=0 9 | todo text: input[data-testid="todo-text"] 10 | error: div[data-testid="error"] 11 | title: h1[data-testid="title"] 12 | add: button[data-testid="add"] -------------------------------------------------------------------------------- /docs/public/using/inheritance/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Using HitchStory - Inheritance 3 | --- 4 | 5 | How to: 6 | 7 | - [Inherit one story from another simply](about) 8 | - [Story inheritance - given mapping preconditions overridden](override-given-mapping) 9 | - [Story inheritance - override given scalar preconditions](override-given-scalar) 10 | - [Story inheritance - parameters](parameters) 11 | - [Story inheritance - steps](steps) 12 | - [Variations](variations) 13 | 14 | 15 | -------------------------------------------------------------------------------- /docs/src/why-not/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Why not X? 3 | --- 4 | 5 | There are a number of other ways of testing and documenting which might seem 6 | very similar to hitchstory. I have tried to document how they differ and 7 | why I chose hitchstory's approach here: 8 | 9 | {{{{ why-not-index-contents.txt }}}} 10 | 11 | If you'd like to write or link to a rebuttal to any argument raised 12 | here or ask for a comparison to something not listed here, 13 | feel free to raise a ticket. 14 | -------------------------------------------------------------------------------- /docs/public/why/inheritance.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Why is inheritance a feature of hitchstory stories? 3 | --- 4 | 5 | Hitchstory stories allow inheritance to allow stories that branch 6 | from other stories. 7 | 8 | ## Dissenting views 9 | 10 | Inheritance is a feature which was deliberately left out of Cucumber: 11 | 12 | https://stackoverflow.com/questions/41872376/can-a-cucumber-feature-file-inherit-from-a-parent-feature-file 13 | 14 | However, it was deliberately added to hitchstory. 15 | -------------------------------------------------------------------------------- /docs/src/why/inheritance.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Why is inheritance a feature of hitchstory stories? 3 | --- 4 | 5 | Hitchstory stories allow inheritance to allow stories that branch 6 | from other stories. 7 | 8 | ## Dissenting views 9 | 10 | Inheritance is a feature which was deliberately left out of Cucumber: 11 | 12 | https://stackoverflow.com/questions/41872376/can-a-cucumber-feature-file-inherit-from-a-parent-feature-file 13 | 14 | However, it was deliberately added to hitchstory. 15 | -------------------------------------------------------------------------------- /examples/website/app/todoApp/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for todoApp project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'todoApp.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /examples/pythonapi/tests/docstory.yml: -------------------------------------------------------------------------------- 1 | story: | 2 | # {{ name }} 3 | 4 | {{ about }} 5 | 6 | {% for step in steps %} 7 | {{ step.documentation() }} 8 | {% endfor %} 9 | steps: 10 | run: | 11 | ```python 12 | {{ code }} 13 | ``` 14 | {% if will_output %} 15 | Will output: 16 | ``` 17 | {{ will_output }} 18 | ``` 19 | {% endif %} 20 | 21 | {% if raises %} 22 | ```python 23 | {{ raises['message'] }} 24 | ``` 25 | {% endif %} 26 | -------------------------------------------------------------------------------- /examples/website/app/todos/migrations/0007_auto_20191202_0323.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.7 on 2019-12-01 21:53 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('todos', '0006_remove_todo_deadline'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RenameField( 14 | model_name='todo', 15 | old_name='name', 16 | new_name='title', 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /examples/commandline/tests/docstory.yml: -------------------------------------------------------------------------------- 1 | story: | 2 | # {{ name }} 3 | 4 | {{ about }} 5 | 6 | {% for step in steps %} 7 | {{ step.documentation() }} 8 | {% endfor %} 9 | steps: 10 | expect: | 11 | * When `{{ text }}` appears. 12 | 13 | display: | 14 | Should display: 15 | 16 | ``` 17 | {{ expected_text }} 18 | ``` 19 | enter text: | 20 | * When `{{ text }}` is entered. 21 | 22 | exit successfully: | 23 | * And the app should exit successfully. 24 | 25 | 26 | -------------------------------------------------------------------------------- /examples/pythonapi/story/correct-my-spelling.story: -------------------------------------------------------------------------------- 1 | Correct my spelling: 2 | about: | 3 | In this story we call the Python API and send it misspellings. 4 | 5 | The API uses TextBlob (https://textblob.readthedocs.io/en/dev/) 6 | to detect misspellings and raises an exception with a correction. 7 | steps: 8 | - run: 9 | code: | 10 | import todo 11 | todo.add_item("biuy breod") 12 | raises: 13 | type: todo.Misspelling 14 | message: Did you mean "buy bread"? 15 | -------------------------------------------------------------------------------- /examples/restapi/hitch/docstory.yml: -------------------------------------------------------------------------------- 1 | story: | 2 | # {{ name }} 3 | 4 | {{ about }} 5 | 6 | {% for step in steps %} 7 | {{ step.documentation() }} 8 | {% endfor %} 9 | steps: 10 | call api: | 11 | ## {{ request["method"] }} request 12 | 13 | Request on {{ request["path"] }} 14 | 15 | {% if "content" in request %} 16 | ```json 17 | {{ request["content"] }} 18 | ``` 19 | {% endif %} 20 | 21 | Will respond with: 22 | ```json 23 | {{ response["content"] }} 24 | ``` 25 | -------------------------------------------------------------------------------- /hitch/Dockerfile-hitch: -------------------------------------------------------------------------------- 1 | FROM quay.io/podman/stable 2 | 3 | RUN yum install \ 4 | zlib zlib-devel bzip2 bzip2-devel xz xz-devel \ 5 | openssl openssl-devel sqlite sqlite-devel readline-devel \ 6 | python-devel libuv-devel ncurses-devel libffi-devel \ 7 | python3-virtualenv gcc git curl make vim podman uv \ 8 | -y && yum clean all 9 | 10 | # Ensure podman-in-podman is stored in volume 11 | RUN sed -i 's|/var/lib/containers/storage|/gen/containers|g' /etc/containers/storage.conf 12 | 13 | RUN mkdir /src 14 | WORKDIR /src 15 | -------------------------------------------------------------------------------- /examples/website/app/templates/registration/login.html: -------------------------------------------------------------------------------- 1 | {% extends 'todos/base.html' %} 2 | 3 | 4 | {% block content %} 5 |
6 |
7 |
8 | 16 |
17 |
18 |
19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /examples/website/app/todos/migrations/0004_auto_20191202_0004.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.7 on 2019-12-01 18:34 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('todos', '0003_auto_20191202_0000'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='todo', 15 | name='deadline', 16 | field=models.DateTimeField(blank=True, verbose_name='Deadline'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /docs/src/approach/test-or-story.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: What is the difference betweeen a test and a story? 3 | --- 4 | 5 | While a test is *turing complete* code that tests behavior, a 6 | story is turing-incomplete code which *defines* behavior. A combination 7 | of stories makes up an [executable specification](../executable-specifications) 8 | 9 | A story must be played using an execution engine. 10 | 11 | A story can be translated into readable documentation, often with 12 | the aid of artefacts created when running the story (e.g. 13 | screenshots). 14 | -------------------------------------------------------------------------------- /examples/pythonapi/tests/docgen.py: -------------------------------------------------------------------------------- 1 | from hitchstory import StoryCollection 2 | from test_integration import Engine 3 | from pathlib import Path 4 | 5 | PROJECTDIR = Path(__file__).absolute().parents[0].parent 6 | 7 | storydocs = ( 8 | StoryCollection(PROJECTDIR.joinpath("story").glob("*.story"), Engine()) 9 | .with_documentation(PROJECTDIR.joinpath("tests", "docstory.yml").read_text()) 10 | .ordered_by_file() 11 | ) 12 | 13 | for story in storydocs: 14 | PROJECTDIR.joinpath("docs", story.slug + ".md").write_text(story.documentation()) 15 | -------------------------------------------------------------------------------- /examples/website/hitch/Dockerfile-playwright: -------------------------------------------------------------------------------- 1 | # If you bump this version, change the version in hitchreqs.in also and 2 | # run ./run.sh make hitchreqs 3 | ARG PLAYWRIGHT_VERSION=1.39.0 4 | 5 | FROM mcr.microsoft.com/playwright:v1.39.0-focal 6 | RUN apt-get update 7 | RUN DEBIAN_FRONTEND=noninteractive apt-get install -y tightvncserver netcat 8 | WORKDIR /root 9 | RUN npm install playwright@1.39.0 && ./node_modules/.bin/playwright install 10 | COPY playwright-entrypoint.sh /entrypoint.sh 11 | RUN chmod +x /entrypoint.sh 12 | 13 | CMD ["/entrypoint.sh"] 14 | -------------------------------------------------------------------------------- /docs/public/approach/test-or-story.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: What is the difference betweeen a test and a story? 3 | --- 4 | 5 | While a test is *turing complete* code that tests behavior, a 6 | story is turing-incomplete code which *defines* behavior. A combination 7 | of stories makes up an [executable specification](../executable-specifications) 8 | 9 | A story must be played using an execution engine. 10 | 11 | A story can be translated into readable documentation, often with 12 | the aid of artefacts created when running the story (e.g. 13 | screenshots). 14 | -------------------------------------------------------------------------------- /examples/commandline/tests/Dockerfile-hitch: -------------------------------------------------------------------------------- 1 | FROM quay.io/podman/stable 2 | 3 | RUN yum install \ 4 | zlib zlib-devel bzip2 bzip2-devel xz xz-devel \ 5 | openssl openssl-devel sqlite sqlite-devel readline-devel \ 6 | python-devel libuv-devel ncurses-devel libffi-devel \ 7 | python3-virtualenv gcc git curl make vim podman \ 8 | -y && yum clean all 9 | 10 | # Ensure podman-in-podman is stored in volume 11 | RUN sed -i 's|/var/lib/containers/storage|/gen/containers|g' /etc/containers/storage.conf 12 | 13 | RUN mkdir /src 14 | WORKDIR /src 15 | -------------------------------------------------------------------------------- /examples/commandline/tests/docgen.py: -------------------------------------------------------------------------------- 1 | from hitchstory import StoryCollection 2 | from test_integration import Engine 3 | from pathlib import Path 4 | 5 | PROJECTDIR = Path(__file__).absolute().parents[0].parent 6 | 7 | storydocs = ( 8 | StoryCollection(PROJECTDIR.joinpath("story").glob("*.story"), Engine()) 9 | .with_documentation(PROJECTDIR.joinpath("tests", "docstory.yml").read_text()) 10 | .ordered_by_file() 11 | ) 12 | 13 | for story in storydocs: 14 | PROJECTDIR.joinpath("docs", story.slug + ".md").write_text(story.documentation()) 15 | -------------------------------------------------------------------------------- /examples/pythonapi/tests/Dockerfile-hitch: -------------------------------------------------------------------------------- 1 | FROM quay.io/podman/stable 2 | 3 | RUN yum install \ 4 | zlib zlib-devel bzip2 bzip2-devel xz xz-devel \ 5 | openssl openssl-devel sqlite sqlite-devel readline-devel \ 6 | python-devel libuv-devel ncurses-devel libffi-devel \ 7 | python3-virtualenv gcc git curl make vim podman \ 8 | -y && yum clean all 9 | 10 | # Ensure podman-in-podman is stored in volume 11 | RUN sed -i 's|/var/lib/containers/storage|/gen/containers|g' /etc/containers/storage.conf 12 | 13 | RUN mkdir /src 14 | WORKDIR /src 15 | -------------------------------------------------------------------------------- /examples/website/app/todos/migrations/0005_auto_20191202_0011.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.7 on 2019-12-01 18:41 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('todos', '0004_auto_20191202_0004'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='todo', 15 | name='deadline', 16 | field=models.DateTimeField(blank=True, null=True, verbose_name='Deadline'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /examples/website/hitch/playwright-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | if [[ "$VNC" == "yes" ]]; then 3 | USER=root Xvnc -geometry ${VNCSCREENSIZE:-1024x768} -depth 24 :1 & 4 | 5 | # Wait for port to be ready 6 | while ! nc -z localhost 5901; do 7 | sleep 0.1 8 | done 9 | DISPLAY=:1 PWDEBUG=console ./node_modules/.bin/playwright run-server --port 3605 10 | else 11 | ./node_modules/.bin/playwright run-server --port 3605 12 | fi 13 | -------------------------------------------------------------------------------- /hitch/story/no-stories.story: -------------------------------------------------------------------------------- 1 | No stories: 2 | given: 3 | files: 4 | example.story: | 5 | # hello 6 | Example story 7 | engine.py: | 8 | from hitchstory import BaseEngine 9 | 10 | class Engine(BaseEngine): 11 | pass 12 | steps: 13 | - Run: 14 | code: | 15 | from hitchstory import StoryCollection 16 | from pathlib import Path 17 | from engine import Engine 18 | 19 | StoryCollection(Path(".").glob("*.story"), Engine()).ordered_by_name().play() 20 | will output: No stories found 21 | -------------------------------------------------------------------------------- /examples/restapi/hitch/podman.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | 4 | class App: 5 | """Interact directly with the app via podman.""" 6 | 7 | def __init__(self, podman): 8 | self._podman = podman 9 | 10 | def start(self): 11 | self._podman("run", "-v", "/src/app:/app", "-d", "app").output() 12 | 13 | def wait_until_ready(self): 14 | # Really bad way to do it 15 | time.sleep(1) 16 | 17 | def stop(self): 18 | self._podman("stop", "--latest", "--time", "1").output() 19 | 20 | def logs(self): 21 | self._podman("logs", "--latest").run() 22 | -------------------------------------------------------------------------------- /examples/llm/hitch/Dockerfile-hitch: -------------------------------------------------------------------------------- 1 | FROM quay.io/podman/stable 2 | 3 | RUN yum install \ 4 | zlib zlib-devel bzip2 bzip2-devel xz xz-devel \ 5 | openssl openssl-devel sqlite sqlite-devel readline-devel \ 6 | python-devel libuv-devel ncurses-devel libffi-devel \ 7 | python3-virtualenv gcc git curl make vim \ 8 | podman podman-compose gcc-c++ \ 9 | -y && yum clean all 10 | 11 | # Ensure podman-in-podman is stored in gen volume 12 | RUN sed -i 's|/var/lib/containers/storage|/gen/containers|g' /etc/containers/storage.conf 13 | 14 | ENV PATH="$PATH:/gen/venv/bin/" 15 | RUN mkdir /src 16 | WORKDIR /src 17 | -------------------------------------------------------------------------------- /examples/llm/hitch/story/barista.story: -------------------------------------------------------------------------------- 1 | Basic barista: 2 | given: 3 | agent instructions: | 4 | You are a barista selling only following items: 5 | 6 | * flat white 7 | * cappuccino coffee 8 | * black coffee 9 | * single espresso 10 | * double espresso 11 | * brownie 12 | 13 | If a customer asks questions, tries to order something not on the list produce JSON of the form: 14 | 15 | {"message": "{{ your answer }}"} 16 | 17 | If a customer orders one of these, produce JSON of the form: 18 | 19 | {"purchase": "{{ product chosen by customer }}"} 20 | -------------------------------------------------------------------------------- /examples/website/app/todos/migrations/0008_auto_20191202_0809.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.7 on 2019-12-02 02:39 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('todos', '0007_auto_20191202_0323'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='todo', 15 | name='description', 16 | ), 17 | migrations.AddField( 18 | model_name='todo', 19 | name='isCompleted', 20 | field=models.BooleanField(default=False), 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /examples/restapi/hitch/Dockerfile-hitch: -------------------------------------------------------------------------------- 1 | FROM quay.io/podman/stable 2 | # This container contains some magic to make podman-in-podman work well 3 | 4 | RUN yum install \ 5 | zlib zlib-devel bzip2 bzip2-devel xz xz-devel \ 6 | openssl openssl-devel sqlite sqlite-devel readline-devel \ 7 | python-devel libuv-devel ncurses-devel libffi-devel \ 8 | python3-virtualenv gcc git curl make vim podman \ 9 | -y && yum clean all 10 | 11 | # Ensure podman-in-podman containers/volumes are stored in gen volume 12 | RUN sed -i 's|/var/lib/containers/storage|/gen/containers|g' /etc/containers/storage.conf 13 | 14 | RUN mkdir /src 15 | WORKDIR /src 16 | -------------------------------------------------------------------------------- /docs/public/why-not/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Why not X? 3 | --- 4 | 5 | There are a number of other ways of testing and documenting which might seem 6 | very similar to hitchstory. I have tried to document how they differ and 7 | why I chose hitchstory's approach here: 8 | 9 | - [Why use Hitchstory instead of Behave, Lettuce or Cucumber (Gherkin)?](gherkin) 10 | - [Why not use the Robot Framework?](robot) 11 | - [Why use hitchstory instead of a unit testing framework?](unit-test) 12 | 13 | 14 | If you'd like to write or link to a rebuttal to any argument raised 15 | here or ask for a comparison to something not listed here, 16 | feel free to raise a ticket. 17 | -------------------------------------------------------------------------------- /.github/workflows/docpublish.yml: -------------------------------------------------------------------------------- 1 | name: Docpublish 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | docpublish: 8 | timeout-minutes: 30 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: checkout repo 12 | uses: actions/checkout@v2 13 | 14 | - name: build 15 | run: | 16 | mkdir -p ~/.ssh/ 17 | touch ~/.ssh/id_rsa 18 | touch ~/.ssh/id_rsa.pub 19 | echo test | podman secret create pypitoken - 20 | echo ${{ secrets.GITHUB_TOKEN }} | podman secret create githubtoken - 21 | ./key.sh make 22 | 23 | - name: docpublish 24 | run: ./key.sh publishdocs 25 | -------------------------------------------------------------------------------- /examples/website/app/todos/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from unittest.mock import MagicMock, patch 3 | from django.contrib.auth.models import User 4 | from django.test.client import RequestFactory 5 | from django.shortcuts import get_object_or_404 6 | from todos.views import delete 7 | 8 | 9 | class DeleteTodo(TestCase): 10 | @patch('todos.views.get_object_or_404') 11 | def test_delete(self, get_object_or_404): 12 | user = User.objects.create_user( 13 | username="u", email="e", password="pwd" 14 | ) 15 | request = RequestFactory().get("/delete/") 16 | request.user = user 17 | delete(request, 1) 18 | -------------------------------------------------------------------------------- /examples/restapi/hitch/docs/add-and-retrieve-todo.md: -------------------------------------------------------------------------------- 1 | # Add and retrieve todo 2 | 3 | In this story we call the API to buy bread 4 | and then see that bread is on the list. 5 | 6 | 7 | 8 | ## POST request 9 | 10 | Request on /todo 11 | 12 | 13 | ```json 14 | { 15 | "item": "buy bread" 16 | } 17 | 18 | ``` 19 | 20 | 21 | Will respond with: 22 | ```json 23 | { 24 | "data": { 25 | "id": "243e6384-298b-4443-a9c9-0cb5d18b92be", 26 | "timestamp": 1683888169 27 | }, 28 | "message": "Item added successfully" 29 | } 30 | 31 | ``` 32 | 33 | ## GET request 34 | 35 | Request on /todo 36 | 37 | 38 | 39 | Will respond with: 40 | ```json 41 | [ 42 | "buy bread" 43 | ] 44 | 45 | ``` 46 | -------------------------------------------------------------------------------- /examples/restapi/hitch/docs/correct-my-spelling.md: -------------------------------------------------------------------------------- 1 | # Correct my spelling 2 | 3 | In this story we call the API and send it misspellings. 4 | 5 | The API uses TextBlob (https://textblob.readthedocs.io/en/dev/) 6 | to detect misspellings and replies to the API with a suggestion 7 | instead of adding it to the to do list. 8 | 9 | 10 | 11 | ## POST request 12 | 13 | Request on /todo 14 | 15 | 16 | ```json 17 | { 18 | "item": "biuy breod" 19 | } 20 | 21 | ``` 22 | 23 | 24 | Will respond with: 25 | ```json 26 | { 27 | "message": "buy bread" 28 | } 29 | 30 | ``` 31 | 32 | ## GET request 33 | 34 | Request on /todo 35 | 36 | 37 | 38 | Will respond with: 39 | ```json 40 | [] 41 | 42 | ``` 43 | -------------------------------------------------------------------------------- /hitchstory/__init__.py: -------------------------------------------------------------------------------- 1 | from hitchstory.story_collection import StoryCollection 2 | from hitchstory.engine import validate 3 | from hitchstory.engine import no_stacktrace_for 4 | from hitchstory.engine import about 5 | from hitchstory.engine import InfoDefinition 6 | from hitchstory.engine import InfoProperty 7 | from hitchstory.engine import GivenDefinition 8 | from hitchstory.engine import GivenProperty 9 | from hitchstory.engine import BaseEngine 10 | from hitchstory.exceptions import HitchStoryException 11 | from hitchstory.exceptions import Failure 12 | from hitchstory.matchers import strings_match 13 | from hitchstory.matchers import json_match 14 | 15 | 16 | __version__ = "DEVELOPMENT_VERSION" 17 | -------------------------------------------------------------------------------- /examples/website/app/todos/migrations/0002_auto_20191201_2357.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.7 on 2019-12-01 18:27 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('todos', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='todo', 15 | name='created_date', 16 | field=models.DateTimeField(verbose_name='Created'), 17 | ), 18 | migrations.AlterField( 19 | model_name='todo', 20 | name='deadline', 21 | field=models.DateTimeField(verbose_name='Deadline'), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /examples/website/hitch/Dockerfile-hitch: -------------------------------------------------------------------------------- 1 | FROM quay.io/podman/stable 2 | 3 | RUN yum install \ 4 | zlib zlib-devel bzip2 bzip2-devel xz xz-devel \ 5 | openssl openssl-devel sqlite sqlite-devel readline-devel \ 6 | python-devel libuv-devel ncurses-devel libffi-devel \ 7 | python3-virtualenv gcc git curl make vim \ 8 | podman podman-compose gcc-c++ \ 9 | -y && yum clean all 10 | 11 | # To process playwright videos 12 | RUN yum install ffmpeg-free ImageMagick -y 13 | 14 | # Ensure podman-in-podman is stored in gen volume 15 | RUN sed -i 's|/var/lib/containers/storage|/gen/containers|g' /etc/containers/storage.conf 16 | 17 | ENV PATH="$PATH:/gen/venv/bin/" 18 | RUN mkdir /src 19 | WORKDIR /src 20 | -------------------------------------------------------------------------------- /examples/website/app/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "todoApp.settings") 9 | try: 10 | from django.core.management import execute_from_command_line 11 | except ImportError as exc: 12 | raise ImportError( 13 | "Couldn't import Django. Are you sure it's installed and " 14 | "available on your PYTHONPATH environment variable? Did you " 15 | "forget to activate a virtual environment?" 16 | ) from exc 17 | execute_from_command_line(sys.argv) 18 | 19 | 20 | if __name__ == "__main__": 21 | main() 22 | -------------------------------------------------------------------------------- /examples/website/hitch/story/correct-my-spelling.story: -------------------------------------------------------------------------------- 1 | Correct my spelling: 2 | about: | 3 | The user tries to add "biuy breod" to the to do list 4 | but the application tries to correct the spelling. 5 | 6 | # custom metadata: 7 | docs: yes # for interesting stories 8 | context: | 9 | The website uses TextBlob (https://textblob.readthedocs.io/en/dev/) 10 | to detect misspellings and replies to the API with a suggestion 11 | instead of adding it to the to do list. 12 | 13 | based on: login 14 | 15 | following steps: 16 | - enter: 17 | on: todo text 18 | text: biuy breod 19 | 20 | - click: add 21 | 22 | - should appear: 23 | text: Did you mean 'buy bread'? 24 | on: error 25 | -------------------------------------------------------------------------------- /examples/website/hitch/story/story.template: -------------------------------------------------------------------------------- 1 | #Name of your story: 2 | # about: | 3 | # Here you write context, description, justifications 4 | # or anything else that belongs in the docs. 5 | # docs: yes 6 | # given: 7 | # browser: chromium 8 | # data: 9 | # table_name: 10 | # id: 11 | # string_column: value 12 | # timestamp_column: 2023-05-08T16:29:41.595Z 13 | # update_at: 2023-05-08T16:29:41.595Z 14 | # bool_column: no 15 | # steps: 16 | # - load website: url/ 17 | 18 | # - page appears: from selectors.yml 19 | 20 | # - enter: 21 | # text: admin 22 | # on: see selectors.yml 23 | 24 | # - should appear: 25 | # on: see selectors.yml 26 | # text: Todo List 27 | 28 | -------------------------------------------------------------------------------- /examples/restapi/hitch/utils.py: -------------------------------------------------------------------------------- 1 | from hitchstory import Failure 2 | import time 3 | import socket 4 | 5 | 6 | def port_open(port_number: int, timeout=2.5): 7 | try: 8 | with socket.create_connection(("localhost", port_number), timeout=timeout): 9 | return True 10 | except OSError: 11 | return False 12 | 13 | 14 | def wait_for_port(port_number: int, timeout=10.0): 15 | start_time = time.perf_counter() 16 | 17 | while True: 18 | if not port_open(port_number): 19 | time.sleep(0.05) 20 | if time.perf_counter() - start_time >= timeout: 21 | raise Failure( 22 | f"Port {port_number} on localhost not responding after {timeout} seconds." 23 | ) 24 | else: 25 | break 26 | -------------------------------------------------------------------------------- /examples/website/hitch/utils.py: -------------------------------------------------------------------------------- 1 | from hitchstory import Failure 2 | import time 3 | import socket 4 | 5 | 6 | def port_open(port_number: int, timeout=2.5): 7 | try: 8 | with socket.create_connection(("localhost", port_number), timeout=timeout): 9 | return True 10 | except OSError: 11 | return False 12 | 13 | 14 | def wait_for_port(port_number: int, timeout=10.0): 15 | start_time = time.perf_counter() 16 | 17 | while True: 18 | if not port_open(port_number): 19 | time.sleep(0.05) 20 | if time.perf_counter() - start_time >= timeout: 21 | raise Failure( 22 | f"Port {port_number} on localhost not responding after {timeout} seconds." 23 | ) 24 | else: 25 | break 26 | -------------------------------------------------------------------------------- /examples/website/app/todos/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.7 on 2019-12-01 17:46 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Todo', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('name', models.CharField(max_length=100)), 19 | ('created_date', models.DateField(verbose_name='Created')), 20 | ('deadline', models.DateField(verbose_name='Deadline')), 21 | ('description', models.CharField(max_length=200)), 22 | ], 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /docs/public/using/engine/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Using HitchStory - Engine 3 | --- 4 | 5 | How to: 6 | 7 | - [Hiding stacktraces for expected exceptions](expected-exceptions) 8 | - [Given preconditions](given) 9 | - [Gradual typing of story steps](gradual-typing) 10 | - [Match two JSON snippets](match-json) 11 | - [Match two strings and show diff on failure](match-two-strings) 12 | - [Extra story metadata - e.g. adding JIRA ticket numbers to stories](metadata) 13 | - [Story with parameters](parameterized-stories) 14 | - [Story that rewrites given preconditions](rewrite-given) 15 | - [Story that rewrites itself](rewrite-story) 16 | - [Story that rewrites the sub key of an argument](rewrite-subkey-of-argument) 17 | - [Raising a Failure exception to conceal the stacktrace](special-failure-exception) 18 | - [Arguments to steps](steps-and-step-arguments) 19 | - [Strong typing](strong-typing) 20 | 21 | 22 | -------------------------------------------------------------------------------- /examples/website/hitch/test_integration.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains the: 3 | 4 | * Code that translates all of the YAML stories into pytest tests. 5 | """ 6 | from engine import Engine 7 | from hitchstory import StoryCollection 8 | from directories import DIR 9 | from os import getenv 10 | from pathlib import Path 11 | 12 | 13 | collection = StoryCollection( 14 | # Grab all stories from all *.story files in the story directory. 15 | DIR.STORY.glob("*.story"), 16 | Engine( 17 | rewrite=getenv("STORYMODE", "") == "rewrite", 18 | vnc=getenv("STORYMODE", "") == "vnc", 19 | coverage=getenv("STORYMODE", "") == "coverage", 20 | timeout=10.0, 21 | ), 22 | ) 23 | 24 | # Turn all stories into pytest tests 25 | collection.with_external_test_runner().only_uninherited().ordered_by_name().add_pytests_to( 26 | module=__import__(__name__) # This module 27 | ) 28 | -------------------------------------------------------------------------------- /docs/public/why/declarative.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Declarative User Stories 3 | --- 4 | 5 | HitchStory StoryFiles are declarative and not turing complete. 6 | 7 | At their core, they essentially just contain marked up data - a set of preconditions (in 'given'), 8 | a set of steps and arguments and the ability to parameterize the preconditions, step arguments 9 | and inherit one story from another. 10 | 11 | No loops. No if statements. Deliberately turing incomplete. 12 | 13 | Using a less powerful language to write tests in sounds counter-intuitive. Surely you want 14 | as much power as possible when writing tests so you can do as much as possible? 15 | 16 | Except you don't want that power, because you don't need it, and if that power is there it 17 | causes a few follow on problems: 18 | 19 | * Technical debt 20 | 21 | * Readability suffers 22 | 23 | * You lose the ability to generate and process the data 24 | -------------------------------------------------------------------------------- /docs/src/why/declarative.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Declarative User Stories 3 | --- 4 | 5 | HitchStory StoryFiles are declarative and not turing complete. 6 | 7 | At their core, they essentially just contain marked up data - a set of preconditions (in 'given'), 8 | a set of steps and arguments and the ability to parameterize the preconditions, step arguments 9 | and inherit one story from another. 10 | 11 | No loops. No if statements. Deliberately turing incomplete. 12 | 13 | Using a less powerful language to write tests in sounds counter-intuitive. Surely you want 14 | as much power as possible when writing tests so you can do as much as possible? 15 | 16 | Except you don't want that power, because you don't need it, and if that power is there it 17 | causes a few follow on problems: 18 | 19 | * Technical debt 20 | 21 | * Readability suffers 22 | 23 | * You lose the ability to generate and process the data 24 | -------------------------------------------------------------------------------- /examples/website/hitch/story/add-todo.story: -------------------------------------------------------------------------------- 1 | Add and retrieve todo: 2 | about: | 3 | The user adds "buy bread" to the to do list 4 | and sees it showing up. 5 | 6 | # custom metadata 7 | jiras: FEATURE-341, FEATURE-441 8 | docs: yes # turn this story into markdown docs 9 | 10 | # inherit from login story in accounts.story 11 | based on: login 12 | 13 | given: 14 | browser: chromium 15 | data: 16 | todos.todo: 17 | # Also includes peppers and cereal from "Login" 18 | 12: 19 | title: Buy a toaster 20 | created_at: 2023-01-01T00:00:00.000Z 21 | update_at: 2023-01-01T00:00:00.000Z 22 | isCompleted: yes 23 | 24 | following steps: 25 | - enter: 26 | on: todo text 27 | text: Add bread 28 | 29 | - click: add 30 | 31 | - should appear: 32 | text: Add bread 33 | on: first todo list item 34 | -------------------------------------------------------------------------------- /examples/restapi/hitch/story/correct-my-spelling.story: -------------------------------------------------------------------------------- 1 | Correct my spelling: 2 | about: | 3 | In this story we call the API and send it misspellings. 4 | 5 | The API uses TextBlob (https://textblob.readthedocs.io/en/dev/) 6 | to detect misspellings and replies to the API with a suggestion 7 | instead of adding it to the to do list. 8 | steps: 9 | - call api: 10 | request: 11 | method: POST 12 | path: /todo 13 | headers: 14 | Content-Type: application/json 15 | content: | 16 | { 17 | "item": "biuy breod" 18 | } 19 | 20 | response: 21 | code: 400 22 | content: | 23 | { 24 | "message": "buy bread" 25 | } 26 | 27 | 28 | - call api: 29 | request: 30 | method: GET 31 | path: /todo 32 | response: 33 | content: | 34 | [] 35 | -------------------------------------------------------------------------------- /examples/restapi/hitch/test_integration.py: -------------------------------------------------------------------------------- 1 | from os import getenv 2 | from hitchstory import ( 3 | StoryCollection, 4 | BaseEngine, 5 | exceptions, 6 | validate, 7 | no_stacktrace_for, 8 | ) 9 | from hitchstory import GivenDefinition, GivenProperty, InfoDefinition, InfoProperty 10 | from hitchstory import Failure, json_match 11 | from strictyaml import Optional, Str, Map, Int, Bool, Enum, load, MapPattern 12 | from shlex import split 13 | from podman import App 14 | from commandlib import Command 15 | import requests 16 | import time 17 | import json 18 | from engine import Engine 19 | from directories import DIR 20 | 21 | 22 | collection = StoryCollection( 23 | DIR.STORY.glob("*.story"), 24 | Engine(rewrite=getenv("STORYMODE", "") == "rewrite"), 25 | ) 26 | 27 | collection.with_external_test_runner().ordered_by_name().add_pytests_to( 28 | module=__import__(__name__) # This module 29 | ) 30 | -------------------------------------------------------------------------------- /hitch/mockcode/mockselenium.py: -------------------------------------------------------------------------------- 1 | class Webdriver(object): 2 | def __init__(self, name=None, platform=None, version=None, dimensions=None): 3 | if name is not None: 4 | print("\nBrowser name: {}".format(name)) 5 | if platform is not None: 6 | print("Platform: {}".format(platform)) 7 | if version is not None: 8 | print("Version: {}".format(version)) 9 | if dimensions is not None: 10 | print("Dimensions: {height} x {width}".format(**dimensions)) 11 | 12 | def visit(self, website): 13 | print("\nVisiting {}".format(website)) 14 | 15 | def fill_form(self, name, content): 16 | if "\n" in content: 17 | print("In {} entering text:\n{}\n".format(name, content)) 18 | else: 19 | print("Entering text {} in {}".format(content, name)) 20 | 21 | def click(self, name): 22 | print("Clicking on {}".format(name)) 23 | -------------------------------------------------------------------------------- /hitch/story/one-story.story: -------------------------------------------------------------------------------- 1 | Run one story in collection: 2 | docs: runner/run-just-one-story 3 | about: | 4 | If you have just one story in your collection, 5 | you can run it directly by using .one(). 6 | given: 7 | files: 8 | example.story: | 9 | Do thing: 10 | steps: 11 | - Do thing 12 | engine.py: | 13 | from hitchstory import BaseEngine 14 | from code_that_does_things import * 15 | 16 | class Engine(BaseEngine): 17 | def do_thing(self): 18 | pass 19 | setup: | 20 | from hitchstory import StoryCollection 21 | from pathlib import Path 22 | from engine import Engine 23 | 24 | 25 | story = StoryCollection(Path(".").glob("*.story"), Engine()).one() 26 | steps: 27 | - Run: 28 | code: story.play() 29 | will output: RUNNING Do thing in /path/to/working/example.story ... SUCCESS 30 | in 0.1 seconds. 31 | -------------------------------------------------------------------------------- /examples/commandline/docs/add-and-retrieve-todo.md: -------------------------------------------------------------------------------- 1 | # Add and retrieve todo 2 | 3 | In this story we call the API to buy bread 4 | and then see that bread is on the list. 5 | 6 | 7 | 8 | Should display: 9 | 10 | ``` 11 | To-do list: 12 | Options: 13 | 1. Add item 14 | 2. Remove item 15 | 3. Quit 16 | Enter your choice: 17 | ``` 18 | 19 | * When `1` is entered. 20 | 21 | Should display: 22 | 23 | ``` 24 | To-do list: 25 | Options: 26 | 1. Add item 27 | 2. Remove item 28 | 3. Quit 29 | Enter your choice: 1 30 | Enter a to-do item: 31 | ``` 32 | 33 | * When `Buy bread` is entered. 34 | 35 | Should display: 36 | 37 | ``` 38 | To-do list: 39 | Options: 40 | 1. Add item 41 | 2. Remove item 42 | 3. Quit 43 | Enter your choice: 1 44 | Enter a to-do item: Buy bread 45 | To-do list: 46 | 1. Buy bread 47 | Options: 48 | 1. Add item 49 | 2. Remove item 50 | 3. Quit 51 | Enter your choice: 52 | ``` 53 | 54 | * When `3` is entered. 55 | 56 | * And the app should exit successfully. 57 | -------------------------------------------------------------------------------- /hitch/story/inheritance-from-non-existent-story.story: -------------------------------------------------------------------------------- 1 | Attempt inheritance from non-existent story: 2 | given: 3 | files: 4 | example.story: | 5 | Write to file: 6 | based on: Create files 7 | steps: 8 | - Do thing two 9 | setup: | 10 | from hitchstory import StoryCollection, BaseEngine 11 | from strictyaml import Map, Str 12 | from pathlib import Path 13 | 14 | 15 | class Engine(BaseEngine): 16 | def do_thing_one(self): 17 | print("thing one") 18 | 19 | def do_thing_two(self): 20 | print("thing two") 21 | 22 | steps: 23 | - Run: 24 | code: StoryCollection(Path(".").glob("*.story"), Engine()).named("Write to file").play() 25 | raises: 26 | type: hitchstory.exceptions.BasedOnStoryNotFound 27 | message: Story 'Create files' which 'Write to file' in '/path/to/working/example.story' 28 | is based upon not found. 29 | -------------------------------------------------------------------------------- /.github/workflows/regression.yml: -------------------------------------------------------------------------------- 1 | name: Regression 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - master 8 | paths-ignore: 9 | - examples/** 10 | pull_request: 11 | 12 | jobs: 13 | regression: 14 | timeout-minutes: 30 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: checkout repo 18 | uses: actions/checkout@v2 19 | 20 | #- name: ensure qemu installed so arm containers work 21 | #run: | 22 | #sudo apt-get update 23 | #sudo apt-get install qemu-system-arm qemu-efi qemu-user-static -y 24 | 25 | - name: build 26 | run: | 27 | mkdir -p ~/.ssh/ 28 | touch ~/.ssh/id_rsa 29 | touch ~/.ssh/id_rsa.pub 30 | echo test | podman secret create pypitoken - 31 | echo ${{ secrets.GITHUB_TOKEN }} | podman secret create githubtoken - 32 | ./key.sh make 33 | 34 | - name: regression 35 | run: ./key.sh regression 36 | -------------------------------------------------------------------------------- /examples/website/app/todos/migrations/0003_auto_20191202_0000.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.7 on 2019-12-01 18:30 2 | 3 | from django.db import migrations, models 4 | import django.utils.timezone 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('todos', '0002_auto_20191201_2357'), 11 | ] 12 | 13 | operations = [ 14 | migrations.RemoveField( 15 | model_name='todo', 16 | name='created_date', 17 | ), 18 | migrations.AddField( 19 | model_name='todo', 20 | name='created_at', 21 | field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now, verbose_name='Created'), 22 | preserve_default=False, 23 | ), 24 | migrations.AddField( 25 | model_name='todo', 26 | name='update_at', 27 | field=models.DateTimeField(auto_now=True, verbose_name='Updated'), 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /examples/restapi/hitch/podman-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | # !!! WARNING !!! 4 | 5 | # ALWAYS ADD network_mode: host to ALL NEW SERVICES 6 | # ALWAYS add docker.io/ to the beginning of any images. 7 | # ALWAYS add a healthcheck 8 | # Otherwise, you can mostly use it like you would any docker-compose.yml. 9 | 10 | services: 11 | app: 12 | network_mode: host 13 | build: 14 | context: ../ 15 | dockerfile: Dockerfile 16 | stop_signal: SIGINT 17 | image: app 18 | volumes: 19 | - /src/app:/app 20 | healthcheck: 21 | test: netcat -vz localhost 5000 22 | interval: 3s 23 | timeout: 2s 24 | retries: 3 25 | 26 | mitm: 27 | network_mode: host 28 | build: 29 | context: . 30 | dockerfile: Dockerfile-mitm 31 | ports: 32 | - "8080:8080" 33 | command: mitmdump 34 | healthcheck: 35 | test: netcat -vz localhost 8080 36 | interval: 3s 37 | timeout: 2s 38 | retries: 3 39 | 40 | -------------------------------------------------------------------------------- /examples/restapi/hitch/story/add-todo.story: -------------------------------------------------------------------------------- 1 | Add and retrieve todo: 2 | about: | 3 | In this story we call the API to buy bread 4 | and then see that bread is on the list. 5 | steps: 6 | - call api: 7 | request: 8 | method: POST 9 | headers: 10 | Content-Type: application/json 11 | path: /todo 12 | content: | 13 | { 14 | "item": "buy bread" 15 | } 16 | 17 | response: 18 | content: | 19 | { 20 | "data": { 21 | "id": "243e6384-298b-4443-a9c9-0cb5d18b92be", 22 | "timestamp": 1683888169 23 | }, 24 | "message": "Item added successfully" 25 | } 26 | varying: 27 | data/id: uuid 28 | data/timestamp: timestamp 29 | 30 | - call api: 31 | request: 32 | method: GET 33 | path: /todo 34 | response: 35 | content: | 36 | [ 37 | "buy bread" 38 | ] 39 | -------------------------------------------------------------------------------- /docs/public/why/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Design Justifications 3 | --- 4 | 5 | HitchStory is the result of some carefully considered, although 6 | controversial design decisions. These are justified here. 7 | 8 | - [Declarative User Stories](declarative) 9 | - [Why does hitchstory mandate the use of given but not when and then?](given-when-then) 10 | - [Why is inheritance a feature of hitchstory stories?](inheritance) 11 | - [Why does hitchstory not have an opinion on what counts as interesting to "the business"?](interesting-to-the-business) 12 | - [Why does hitchstory not have a command line interface?](no-cli) 13 | - [Principles](principles) 14 | - [Why does HitchStory have no CLI runner - only a pure python API?](pure-python-no-cli) 15 | - [Why Rewritable Test Driven Development (RTDD)?](rewrite) 16 | - [Why does HitchStory use StrictYAML?](strictyaml) 17 | 18 | 19 | Rebuttals and critiques, especially from users and designers of 20 | competing tools are welcome. Either raise a ticket on github 21 | or open a pull request with a link. 22 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # HitchStory Examples 2 | 3 | Type-safe [StrictYAML](why/strictyaml) integration tests run from pytest. 4 | 5 | ## Rewrite themselves from program output 6 | 7 | ![Test rewriting itself](https://hitchdev-videos.netlify.app/rewrite-demo.gif) 8 | 9 | ## Autogenerate documentation about the app 10 | 11 | ![Test writing docs](https://hitchdev-videos.netlify.app/rewrite-docs-demo.gif) 12 | 13 | The four folders contain four versions of the same project - 14 | [this great little to do app](https://github.com/ovinokurov/ToDo) 15 | built by [Oleg Vinokurov](https://github.com/ovinokurov) which was built 16 | with a command line, REST and web interface. 17 | 18 | * [Website](https://github.com/hitchdev/hitchstory/tree/master/examples/website) 19 | * [REST API](https://github.com/hitchdev/hitchstory/tree/master/examples/restapi) 20 | * [Interactive command line app](https://github.com/hitchdev/hitchstory/tree/master/examples/commandline) 21 | * [Python API](https://github.com/hitchdev/hitchstory/tree/master/examples/pythonapi) 22 | -------------------------------------------------------------------------------- /examples/commandline/story/add-todo.story: -------------------------------------------------------------------------------- 1 | Add and retrieve todo: 2 | about: | 3 | In this story we call the API to buy bread 4 | and then see that bread is on the list. 5 | steps: 6 | 7 | - display: |- 8 | To-do list: 9 | Options: 10 | 1. Add item 11 | 2. Remove item 12 | 3. Quit 13 | Enter your choice: 14 | 15 | - enter text: 1 16 | 17 | - display: |- 18 | To-do list: 19 | Options: 20 | 1. Add item 21 | 2. Remove item 22 | 3. Quit 23 | Enter your choice: 1 24 | Enter a to-do item: 25 | 26 | - enter text: Buy bread 27 | 28 | - display: |- 29 | To-do list: 30 | Options: 31 | 1. Add item 32 | 2. Remove item 33 | 3. Quit 34 | Enter your choice: 1 35 | Enter a to-do item: Buy bread 36 | To-do list: 37 | 1. Buy bread 38 | Options: 39 | 1. Add item 40 | 2. Remove item 41 | 3. Quit 42 | Enter your choice: 43 | 44 | - enter text: 3 45 | 46 | - exit successfully 47 | -------------------------------------------------------------------------------- /examples/pythonapi/docs/add-and-retrieve-todo.md: -------------------------------------------------------------------------------- 1 | # Add and retrieve todo 2 | 3 | In this story we call the Python API to add 4 | "buy bread" to the to do list and then 5 | and then see that bread is on the list. 6 | 7 | 8 | 9 | ```python 10 | import todo 11 | 12 | print("Adding an item") 13 | todo.add_item("buy bread") 14 | 15 | print() 16 | print("List items:") 17 | todo.list_items() 18 | 19 | ``` 20 | 21 | Will output: 22 | ``` 23 | Adding an item 24 | To do added buy bread 25 | 26 | List items: 27 | buy bread 28 | ``` 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /examples/llm/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Colm O'Connor 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /examples/pythonapi/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Colm O'Connor 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /examples/restapi/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Colm O'Connor 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /examples/website/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Colm O'Connor 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /examples/commandline/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Colm O'Connor 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /examples/website/hitch/hitchreqs.in: -------------------------------------------------------------------------------- 1 | # Requirements for running the integration tests 2 | # This is compiled into pinned versions in the hitchreqs.txt file. 3 | # This is separate from the application requirements.txt / requirements.in 4 | # Run "./run.sh make hitchreqs" to rebuild hitchreqs.txt. 5 | 6 | pytest # tests 7 | hitchstory>=0.23.0 # for strictyaml story interpretation -> pytests 8 | hitchpage # for using the page config pattern with playwright 9 | hitchdb # for database fixtures 10 | ipython # REPL for use in the test environment 11 | nest_asyncio # To make ipython play well with playwright 12 | commandlib # Syntactic sugar for running UNIX commands 13 | pip-tools # For rebuilding hitchreqs.txt from this file. 14 | pytest-split # For parallelizing test runs 15 | inflect # Helper functions for generating docs. 16 | pillow # Image manipulation (for screenshots) 17 | pixelmatch # For snapshot testing of screenshots 18 | coverage # For code coverage 19 | 20 | # Keep this tied to the version in Dockerfile-playwright 21 | playwright==1.39.0 22 | -------------------------------------------------------------------------------- /hitch/story/invalid-stories.story: -------------------------------------------------------------------------------- 1 | Invalid story collections: 2 | given: 3 | setup: | 4 | from hitchstory import StoryCollection, BaseEngine 5 | variations: 6 | Should be a list or iterator: 7 | steps: 8 | - Run: 9 | code: StoryCollection("invalid", BaseEngine()).one().play() 10 | raises: 11 | type: hitchstory.exceptions.InvalidStoryPaths 12 | message: storypaths should be a list or iterator returning a list of story 13 | files (e.g. using pathlib.Path.glob). Instead it was string 'invalid'. 14 | Nonexistent files: 15 | steps: 16 | - Run: 17 | code: StoryCollection(["nonexistent", ], BaseEngine()).one().play() 18 | raises: 19 | type: hitchstory.exceptions.InvalidStoryPaths 20 | message: Story file name 'nonexistent' does not exist. 21 | 22 | Is a directory, not a .story file: 23 | steps: 24 | - Run: 25 | code: StoryCollection([".", ], BaseEngine()).one().play() 26 | raises: 27 | type: hitchstory.exceptions.InvalidStoryPaths 28 | message: Story path '.' is a directory. 29 | -------------------------------------------------------------------------------- /docs/public/approach/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: General Approach 3 | --- 4 | 5 | HitchStory best practices are documented here: 6 | 7 | - [Is HitchStory a BDD tool? How do I do BDD with hitchstory?](bdd) 8 | - [Complementary tools](complementary-tools) 9 | - [Domain Appropriate Scenario Language (DASL)](domain-appropriate-scenario-language-dasl) 10 | - [Executable specifications](executable-specifications) 11 | - [Flaky Tests](flaky-tests) 12 | - [The Hermetic End to End Testing Pattern](hermetic-end-to-end-test) 13 | - [ANTIPATTERN - Analysts writing stories for the developer](human-writable) 14 | - [Separation of Test Concerns](separation-of-test-concerns) 15 | - [Snapshot Test Driven Development (STDD)](snapshot-test-driven-development-stdd) 16 | - [Test Artefact Environment Isolation](test-artefact-environment-isolation) 17 | - [Test concern leakage](test-concern-leakage) 18 | - [Tests as an investment](test-investment) 19 | - [What is the difference betweeen a test and a story?](test-or-story) 20 | - [The importance of test realism](test-realism) 21 | - [Testing non-deterministic code](testing-nondeterministic-code) 22 | - [Specification Documentation Test Triality](triality) 23 | 24 | -------------------------------------------------------------------------------- /examples/pythonapi/story/add-todo.story: -------------------------------------------------------------------------------- 1 | Add and retrieve todo: 2 | about: | 3 | In this story we call the Python API to add 4 | "buy bread" to the to do list and then 5 | and then see that bread is on the list. 6 | steps: 7 | - run: 8 | code: | 9 | import todo 10 | 11 | print("Adding an item") 12 | todo.add_item("buy bread") 13 | 14 | print() 15 | print("List items:") 16 | todo.list_items() 17 | will output: |- 18 | Adding an item 19 | To do added buy bread 20 | 21 | List items: 22 | buy bread 23 | -------------------------------------------------------------------------------- /examples/website/hitch/story/accounts.story: -------------------------------------------------------------------------------- 1 | # If you remove .only_uninherited() at the end 2 | # of test_integration.py this story will be turned 3 | # into a test - "test_login" just like all the 4 | # other stories are. 5 | 6 | # Because it is used in other tests, it's assumed 7 | # that it doesn't need to be run by itself. 8 | 9 | Login: 10 | about: | 11 | Login as admin user. 12 | given: 13 | browser: chromium 14 | data: 15 | todos.todo: 16 | 10: 17 | title: Buy peppers 18 | created_at: 2023-01-01T00:00:00.000Z 19 | update_at: 2023-01-01T00:00:00.000Z 20 | isCompleted: no 21 | 11: 22 | title: Buy cereal 23 | created_at: 2023-01-01T00:00:00.000Z 24 | update_at: 2023-01-01T00:00:00.000Z 25 | isCompleted: yes 26 | steps: 27 | - load website: login/ 28 | 29 | - page appears: login 30 | 31 | - enter: 32 | text: admin 33 | on: username 34 | 35 | - enter: 36 | text: password 37 | on: password 38 | 39 | - click: submit 40 | 41 | - page appears: dashboard 42 | 43 | - should appear: 44 | on: title 45 | text: Todo List 46 | 47 | -------------------------------------------------------------------------------- /hitch/story/expected-exceptions.story: -------------------------------------------------------------------------------- 1 | Hiding stacktraces for expected exceptions: 2 | docs: engine/expected-exceptions 3 | based on: handling failing tests 4 | about: | 5 | For common and expected exceptions where you do not want 6 | the entire stacktrace to be spewed out in the error message, 7 | you can apply the "@no_stacktrace_for" decorator to the step. 8 | 9 | See also: 10 | 11 | * [Raise a Failure exception](../special-failure-exception) 12 | * [Compare two strings](../match-two-strings) 13 | given: 14 | files: 15 | example.story: | 16 | Failing story: 17 | steps: 18 | - Failing step without stacktrace 19 | steps: 20 | - Run: 21 | code: story_collection.one().play() 22 | will output: |- 23 | RUNNING Failing story in /path/to/working/example.story ... FAILED in 0.1 seconds. 24 | 25 | Failing story: 26 | steps: 27 | - Failing step without stacktrace 28 | 29 | 30 | code_that_does_things.ExampleException 31 | 32 | This is a demonstration exception docstring. 33 | 34 | It spreads across multiple lines. 35 | 36 | Expected exception 37 | -------------------------------------------------------------------------------- /examples/restapi/hitch/cli.py: -------------------------------------------------------------------------------- 1 | from hitchstory import StoryCollection 2 | from click import argument, group, pass_context 3 | from engine import Engine 4 | from directories import DIR 5 | from os import getenv 6 | 7 | 8 | def _collection(**args): 9 | return StoryCollection( 10 | DIR.STORY.glob("*.story"), 11 | Engine(**args), 12 | ) 13 | 14 | 15 | @group(invoke_without_command=True) 16 | @pass_context 17 | def cli(ctx): 18 | """Integration test command line interface.""" 19 | pass 20 | 21 | 22 | @cli.command() 23 | @argument("keywords", nargs=-1) 24 | def rbdd(keywords): 25 | """ 26 | Run story with name containing keywords and rewrite. 27 | """ 28 | _collection(rewrite=True).shortcut(*keywords).play() 29 | 30 | 31 | @cli.command() 32 | @argument("keywords", nargs=-1) 33 | def bdd(keywords): 34 | """ 35 | Run story with name containing keywords. 36 | """ 37 | _collection().shortcut(*keywords).play() 38 | 39 | 40 | @cli.command() 41 | def regression(): 42 | """ 43 | Continuos integration - lint and run all stories. 44 | """ 45 | _collection().only_uninherited().ordered_by_name().play() 46 | 47 | 48 | if __name__ == "__main__": 49 | cli() 50 | -------------------------------------------------------------------------------- /hitch/story/unique-names.story: -------------------------------------------------------------------------------- 1 | All stories must have a unique name: 2 | category: behavior 3 | about: | 4 | Note that "Create file" and "create file" are not exactly 5 | the same name, but their slugified names are identical. 6 | given: 7 | files: 8 | example1.story: | 9 | Create file: 10 | steps: 11 | - Create file 12 | example2.story: | 13 | create-file: 14 | steps: 15 | - Create file 16 | setup: | 17 | from hitchstory import StoryCollection, BaseEngine 18 | from pathlib import Path 19 | 20 | class Engine(BaseEngine): 21 | def create_file(self, filename="step1.txt", content="example"): 22 | with open(filename, 'w') as handle: 23 | handle.write(content) 24 | steps: 25 | - Run: 26 | code: | 27 | StoryCollection(Path(".").glob("*.story"), Engine()).ordered_by_file().play() 28 | raises: 29 | type: hitchstory.exceptions.DuplicateStoryNames 30 | message: Story 'create-file' in '/path/to/working/example2.story' and 'Create 31 | file' in '/path/to/working/example1.story' are identical when slugified 32 | ('create-file' and 'create-file'). 33 | -------------------------------------------------------------------------------- /examples/llm/hitch/cli.py: -------------------------------------------------------------------------------- 1 | from hitchstory import StoryCollection 2 | from click import argument, group, pass_context 3 | from engine import Engine 4 | from directories import DIR 5 | from os import getenv 6 | 7 | 8 | def _collection(**args): 9 | return StoryCollection( 10 | DIR.STORY.glob("*.story"), 11 | Engine(**args), 12 | ) 13 | 14 | 15 | @group(invoke_without_command=True) 16 | @pass_context 17 | def cli(ctx): 18 | """Integration test command line interface.""" 19 | pass 20 | 21 | 22 | @cli.command() 23 | @argument("keywords", nargs=-1) 24 | def rbdd(keywords): 25 | """ 26 | Run story with name containing keywords and rewrite. 27 | """ 28 | _collection(rewrite=True).shortcut(*keywords).play() 29 | 30 | 31 | @cli.command() 32 | @argument("keywords", nargs=-1) 33 | def bdd(keywords): 34 | """ 35 | Run story with name containing keywords. 36 | """ 37 | _collection(rewrite=False).shortcut(*keywords).play() 38 | 39 | 40 | @cli.command() 41 | def regression(): 42 | """ 43 | Continuous integration - run all stories. 44 | """ 45 | _collection(print_output=False).only_uninherited().ordered_by_name().play() 46 | 47 | 48 | if __name__ == "__main__": 49 | cli() 50 | -------------------------------------------------------------------------------- /examples/llm/hitch/story/buy-coffee.story: -------------------------------------------------------------------------------- 1 | Espresso purchase: 2 | based on: basic barista 3 | steps: 4 | - speak: 5 | message: Can I order an espresso? 6 | expect json: |- 7 | {"purchase": "single espresso"} 8 | 9 | Try to order a pizza: 10 | based on: basic barista 11 | steps: 12 | - speak: 13 | message: Can I order a pizza? 14 | expect answer: 15 | - question: Did the barista let you order a pizza? Answer yes or no. 16 | response: no 17 | - question: Was the barista polite? Answer yes or no. 18 | response: yes 19 | 20 | Order a pizza, change my mind order a brownie: 21 | based on: basic barista 22 | steps: 23 | - speak: 24 | previous: 25 | - user: Can I order a pizza? 26 | - assistant: I'm sorry, but we only sell flat white, cappuccino coffee, black coffee, single espresso, double espresso, and brownie. 27 | message: Ok, how about a brownie? 28 | expect json: |- 29 | {"purchase": "brownie"} 30 | 31 | Try to order a cookie: 32 | based on: basic barista 33 | steps: 34 | - speak: 35 | message: Can I order a cookie? 36 | expect answer: 37 | - question: Did the barista let you order a cookie? Answer yes or no. 38 | response: no 39 | -------------------------------------------------------------------------------- /docs/public/using/runner/run-just-one-story.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Run one story in collection 3 | --- 4 | 5 | 6 | 7 | If you have just one story in your collection, 8 | you can run it directly by using .one(). 9 | 10 | 11 | # Code Example 12 | 13 | 14 | 15 | example.story: 16 | 17 | ```yaml 18 | Do thing: 19 | steps: 20 | - Do thing 21 | ``` 22 | engine.py: 23 | 24 | ```python 25 | from hitchstory import BaseEngine 26 | from code_that_does_things import * 27 | 28 | class Engine(BaseEngine): 29 | def do_thing(self): 30 | pass 31 | ``` 32 | 33 | With code: 34 | 35 | ```python 36 | from hitchstory import StoryCollection 37 | from pathlib import Path 38 | from engine import Engine 39 | 40 | 41 | story = StoryCollection(Path(".").glob("*.story"), Engine()).one() 42 | 43 | ``` 44 | 45 | 46 | 47 | 48 | 49 | 50 | ```python 51 | story.play() 52 | ``` 53 | 54 | Will output: 55 | ``` 56 | RUNNING Do thing in /path/to/working/example.story ... SUCCESS in 0.1 seconds. 57 | ``` 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | !!! note "Executable specification" 68 | 69 | Documentation automatically generated from 70 | one-story.story 71 | storytests. 72 | 73 | -------------------------------------------------------------------------------- /examples/website/hitch/test_legacy.py: -------------------------------------------------------------------------------- 1 | from test_integration import Engine 2 | from playwright.sync_api import expect 3 | from pytest import fixture 4 | from os import getenv 5 | import nest_asyncio 6 | 7 | nest_asyncio.apply() 8 | 9 | 10 | @fixture 11 | def my_website(): 12 | return Engine( 13 | rewrite=getenv("STORYMODE", "") == "rewrite", 14 | vnc=getenv("STORYMODE", "") == "vnc", 15 | coverage=getenv("STORYMODE", "") == "coverage", 16 | timeout=10.0, 17 | ) 18 | 19 | 20 | def test_add_and_retrieve_todo(my_website): 21 | """Demonstates the normal way UI tests are written.""" 22 | my_website._given = {"browser": "chromium"} 23 | my_website.set_up() 24 | page = my_website._page 25 | 26 | try: 27 | # Arrange 28 | page.goto("http://localhost:8000/login") 29 | page.get_by_test_id("username").fill("admin") 30 | page.get_by_test_id("password").fill("password") 31 | page.get_by_test_id("submit").click() 32 | 33 | # Act 34 | page.get_by_test_id("todo-text").fill("Add bread") 35 | page.get_by_test_id("add").click() 36 | 37 | # Assert 38 | expect(page.locator(".test-todo-list-item").nth(0)).to_contain_text("Add bread") 39 | finally: 40 | my_website.tear_down() 41 | -------------------------------------------------------------------------------- /examples/website/hitch/conftest.py: -------------------------------------------------------------------------------- 1 | from os import getenv 2 | from docgen import generate_docs 3 | from commandlib import python_bin 4 | from directories import DIR 5 | 6 | 7 | def pytest_sessionfinish(session, exitstatus): 8 | """Run after all tests have run - regenerate markdown docs?""" 9 | if exitstatus == 0: 10 | if getenv("STORYMODE", "") == "rewrite": 11 | generate_docs() 12 | 13 | if getenv("STORYMODE", "") == "coverage": 14 | combined_file = DIR.APP / "combined.coverage" 15 | if combined_file.exists(): 16 | combined_file.unlink() 17 | coverage_files = list(DIR.ARTEFACTS.glob("*.coverage")) 18 | 19 | python_bin.coverage( 20 | "combine", "--keep", f"--data-file={combined_file}", *coverage_files 21 | ).in_dir(DIR.APP).output() 22 | 23 | python_bin.coverage( 24 | "html", 25 | "--data-file=combined.coverage", 26 | f"--directory={DIR.ARTEFACTS}/htmlcov", 27 | ).in_dir(DIR.APP).output() 28 | 29 | python_bin.coverage( 30 | "xml", 31 | "--data-file=combined.coverage", 32 | "-o", 33 | f"{DIR.ARTEFACTS}/coverage.xml", 34 | ).in_dir(DIR.APP).output() 35 | -------------------------------------------------------------------------------- /examples/llm/hitch/docgen.py: -------------------------------------------------------------------------------- 1 | """ 2 | Generates the markdown docs in docs/ using docstory.yml 3 | and the YAML stories marked for documentation as the input. 4 | """ 5 | 6 | from pathlib import Path 7 | from hitchstory import StoryCollection 8 | from test_integration import Engine 9 | import inflect 10 | 11 | PROJECTDIR = Path(__file__).absolute().parents[0].parent 12 | 13 | IENG = inflect.engine() 14 | 15 | 16 | def ordinal(num): 17 | """0 -> first, 1 -> second, -1 -> last, etc.""" 18 | if num >= 0: 19 | return IENG.number_to_words(IENG.ordinal(num + 1)) 20 | elif num == -1: 21 | return "last" 22 | else: 23 | return "{} last".format(IENG.number_to_words(IENG.ordinal(num * -1))) 24 | 25 | 26 | def generate_docs(): 27 | storydocs = ( 28 | StoryCollection(PROJECTDIR.joinpath("hitch", "story").glob("*.story"), Engine()) 29 | .with_documentation( 30 | PROJECTDIR.joinpath("hitch", "docstory.yml").read_text(), 31 | extra={"ordinal": ordinal}, 32 | ) 33 | .filter(lambda story: story.info.get("docs")) 34 | .ordered_by_file() 35 | ) 36 | 37 | for story in storydocs: 38 | PROJECTDIR.joinpath("hitch", "docs", story.slug + ".md").write_text( 39 | story.documentation() 40 | ) 41 | 42 | 43 | if __name__ == "__main__": 44 | generate_docs() 45 | -------------------------------------------------------------------------------- /examples/website/hitch/docgen.py: -------------------------------------------------------------------------------- 1 | """ 2 | Generates the markdown docs in docs/ using docstory.yml 3 | and the YAML stories marked for documentation as the input. 4 | """ 5 | 6 | from pathlib import Path 7 | from hitchstory import StoryCollection 8 | from test_integration import Engine 9 | import inflect 10 | 11 | PROJECTDIR = Path(__file__).absolute().parents[0].parent 12 | 13 | IENG = inflect.engine() 14 | 15 | 16 | def ordinal(num): 17 | """0 -> first, 1 -> second, -1 -> last, etc.""" 18 | if num >= 0: 19 | return IENG.number_to_words(IENG.ordinal(num + 1)) 20 | elif num == -1: 21 | return "last" 22 | else: 23 | return "{} last".format(IENG.number_to_words(IENG.ordinal(num * -1))) 24 | 25 | 26 | def generate_docs(): 27 | storydocs = ( 28 | StoryCollection(PROJECTDIR.joinpath("hitch", "story").glob("*.story"), Engine()) 29 | .with_documentation( 30 | PROJECTDIR.joinpath("hitch", "docstory.yml").read_text(), 31 | extra={"ordinal": ordinal}, 32 | ) 33 | .filter(lambda story: story.info.get("docs")) 34 | .ordered_by_file() 35 | ) 36 | 37 | for story in storydocs: 38 | PROJECTDIR.joinpath("hitch", "docs", story.slug + ".md").write_text( 39 | story.documentation() 40 | ) 41 | 42 | 43 | if __name__ == "__main__": 44 | generate_docs() 45 | -------------------------------------------------------------------------------- /docs/public/why/principles.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Principles 3 | --- 4 | 5 | This library was dogfooded for years to TDD / BDD, test and autodocument a variety 6 | of different kinds of software - web apps, python libraries, command line apps, 7 | replacing all other forms of unit, integration and end to end tests. 8 | 9 | Unlike traditional "BDD" frameworks like Cucumber, hitchstory is not primarily designed for 10 | "[business readability](https://www.martinfowler.com/bliki/BusinessReadableDSL.html)", 11 | but rather for simplicity ease of maintenance by developers. 12 | 13 | This means: 14 | 15 | * Stories can *and should* inherit from one another, because *specifications ought to be DRY too*. 16 | * Stories are defined and validated using strongly typed StrictYAML. Step arguments and precondition ('given') schemas can be strictly defined by the programmer. 17 | * The execution engine can be programmed to rewrite the executing story based upon certain kinds of behavior changes (e.g. output strings, screen output changes, messages in a web app). 18 | * Running stories is done via a python API rather than the command line so you can easily program customized test workflows. 19 | * There is built in story parameterization so you can do property based testing. 20 | * Stories can be easily tested for flakiness. 21 | * The stories are designed to be easily used to build readable documentation. 22 | -------------------------------------------------------------------------------- /docs/src/why/principles.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Principles 3 | --- 4 | 5 | This library was dogfooded for years to TDD / BDD, test and autodocument a variety 6 | of different kinds of software - web apps, python libraries, command line apps, 7 | replacing all other forms of unit, integration and end to end tests. 8 | 9 | Unlike traditional "BDD" frameworks like Cucumber, hitchstory is not primarily designed for 10 | "[business readability](https://www.martinfowler.com/bliki/BusinessReadableDSL.html)", 11 | but rather for simplicity ease of maintenance by developers. 12 | 13 | This means: 14 | 15 | * Stories can *and should* inherit from one another, because *specifications ought to be DRY too*. 16 | * Stories are defined and validated using strongly typed StrictYAML. Step arguments and precondition ('given') schemas can be strictly defined by the programmer. 17 | * The execution engine can be programmed to rewrite the executing story based upon certain kinds of behavior changes (e.g. output strings, screen output changes, messages in a web app). 18 | * Running stories is done via a python API rather than the command line so you can easily program customized test workflows. 19 | * There is built in story parameterization so you can do property based testing. 20 | * Stories can be easily tested for flakiness. 21 | * The stories are designed to be easily used to build readable documentation. 22 | -------------------------------------------------------------------------------- /examples/website/hitch/video.py: -------------------------------------------------------------------------------- 1 | """ 2 | Convert playwright's webm to a gif that plays at a reasonable 3 | speed with a file size that is reasonably small. 4 | 5 | GIFs are nice because the documentation can have a repeating 6 | video at the top of the docs demonstrating the feature, and it 7 | displays on github (provided you're not looking at it using the 8 | github app). 9 | """ 10 | 11 | from pathlib import Path 12 | from commandlib import Command 13 | 14 | 15 | def convert_to_slow_gif(webm_path: Path): 16 | gif_path = webm_path.parent / f"{webm_path.stem}.gif" 17 | webm_temp = webm_path.parent / "webm_temp.webm" 18 | palette_path = webm_path.parent / "palette.png" 19 | 20 | ffmpeg = Command("ffmpeg", "-y") 21 | 22 | # Cut first second - just blank loading 23 | ffmpeg("-i", webm_path, "-ss", "1", "-fflags", "+genpts", webm_temp).output() 24 | 25 | # Convert to GIF 26 | ffmpeg("-i", webm_temp, "-vf", "palettegen", palette_path).output() 27 | ffmpeg( 28 | "-i", 29 | webm_temp, 30 | "-i", 31 | palette_path, 32 | "-filter_complex", 33 | "paletteuse", 34 | "-r", 35 | "10", 36 | gif_path, 37 | ).output() 38 | 39 | # Slow down GIF 40 | Command("convert", "-delay", "10x100", gif_path, gif_path).run() 41 | 42 | # Clean up 43 | webm_path.unlink() 44 | webm_temp.unlink() 45 | palette_path.unlink() 46 | -------------------------------------------------------------------------------- /examples/website/app/todos/templates/todos/base.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | {% block title %} 15 | {% endblock %} 16 | 17 | 18 | 19 | {% block content %} 20 | {% endblock %} 21 | 22 | 23 | -------------------------------------------------------------------------------- /hitchstory/templates/multiple.jinja2: -------------------------------------------------------------------------------- 1 | {% if results.one_test %} 2 | {% if results.at_least_one_failure %} 3 | {% for result in results.failures %} 4 | {% set stacktrace = result.stacktrace %} 5 | {% include 'stacktrace_default.jinja2' with context %} 6 | {% endfor %} 7 | 8 | {{ Fore.RED }}{{ Style.BRIGHT }}FAILED IN {{ results.duration|round(1, 'floor') }} seconds{{ Style.RESET_ALL}}{{ Fore.RESET }} 9 | {% else %} 10 | {{ Fore.GREEN }}TEST PASSED IN {{ results.duration|round(1, 'floor') }} seconds{{ Fore.RESET }} 11 | {% endif %} 12 | {% else %} 13 | {% if results.at_least_one_failure %} 14 | 15 | {% for result in results.failures %} 16 | {% set stacktrace = result.stacktrace %} 17 | {% include 'stacktrace_default.jinja2' with context %} 18 | {% endfor %} 19 | 20 | {% for result in results.failures %} 21 | -- {{ result.test.name }} {% if result.test.tags %}({% for tag in result.test.tags %}{{ tag }}{% if not loop.last %},{% endif %}{% endfor %}){% endif %} 22 | {% endfor %} 23 | 24 | {% if results.failedfast %} 25 | {{ Fore.RED }}TEST FAILURE IN {{ results.duration|round(1, 'floor') }} seconds.{{ Fore.RESET }} 26 | {% else %} 27 | {{ Fore.RED }}{{ results.total_failures }}/{{ results.total }} TEST FAILURES IN {{ results.duration|round(1, 'floor') }} seconds.{{ Fore.RESET }} 28 | {% endif %} 29 | 30 | {% else %} 31 | {{ Fore.GREEN }}ALL {{ results.total }} TESTS PASSED IN {{ results.duration|round(1, 'floor') }} seconds{{ Fore.RESET }} 32 | {% endif %} 33 | {% endif %} 34 | -------------------------------------------------------------------------------- /hitch/story/invalid-yaml.story: -------------------------------------------------------------------------------- 1 | Invalid YAML: 2 | about: | 3 | When a story has invalid YAML it will trigger 4 | a failure even when *other* stories are run. 5 | 6 | Names of stories and their filenames should 7 | be reported. 8 | given: 9 | files: 10 | example1.story: | 11 | Invalid YAML: 12 | steps 13 | - Do something 14 | note the ^^^^ invalid YAML 15 | example2.story: | 16 | Valid YAML: 17 | steps: 18 | - Do something: | 19 | text 20 | example3.story: | 21 | Invalid YAML: 22 | steps: 23 | - Do something: text 24 | engine.py: | 25 | from hitchstory import BaseEngine 26 | 27 | class Engine(BaseEngine): 28 | def do_something(self, text): 29 | pass 30 | setup: | 31 | from hitchstory import StoryCollection 32 | from engine import Engine 33 | from pathlib import Path 34 | steps: 35 | - Run: 36 | code: | 37 | StoryCollection(Path(".").glob("*.story"), Engine()).named("Valid YAML").play() 38 | raises: 39 | type: hitchstory.exceptions.StoryYAMLError 40 | message: |- 41 | YAML Error in file 'example1.story': 42 | when expecting a mapping 43 | found arbitrary text 44 | in "", line 1, column 1: 45 | Invalid YAML: steps - Do somethin ... 46 | ^ (line: 1) 47 | -------------------------------------------------------------------------------- /examples/commandline/docs/correct-my-spelling.md: -------------------------------------------------------------------------------- 1 | # Correct my spelling 2 | 3 | In this story we call the API and send it misspellings. 4 | 5 | The API uses TextBlob (https://textblob.readthedocs.io/en/dev/) 6 | to detect misspellings and replies to the API with a suggestion 7 | instead of adding it to the to do list. 8 | 9 | 10 | 11 | Should display: 12 | 13 | ``` 14 | To-do list: 15 | Options: 16 | 1. Add item 17 | 2. Remove item 18 | 3. Quit 19 | Enter your choice: 20 | ``` 21 | 22 | * When `1` is entered. 23 | 24 | Should display: 25 | 26 | ``` 27 | To-do list: 28 | Options: 29 | 1. Add item 30 | 2. Remove item 31 | 3. Quit 32 | Enter your choice: 1 33 | Enter a to-do item: 34 | ``` 35 | 36 | * When `biuy breod` is entered. 37 | 38 | Should display: 39 | 40 | ``` 41 | To-do list: 42 | Options: 43 | 1. Add item 44 | 2. Remove item 45 | 3. Quit 46 | Enter your choice: 1 47 | Enter a to-do item: biuy breod 48 | Did you mean "buy bread"? 49 | Enter Y to confirm, or any other key to re-enter: 50 | ``` 51 | 52 | * When `Y` is entered. 53 | 54 | Should display: 55 | 56 | ``` 57 | To-do list: 58 | Options: 59 | 1. Add item 60 | 2. Remove item 61 | 3. Quit 62 | Enter your choice: 1 63 | Enter a to-do item: biuy breod 64 | Did you mean "buy bread"? 65 | Enter Y to confirm, or any other key to re-enter: Y 66 | To-do list: 67 | 1. buy bread 68 | Options: 69 | 1. Add item 70 | 2. Remove item 71 | 3. Quit 72 | Enter your choice: 73 | ``` 74 | 75 | * When `3` is entered. 76 | 77 | * And the app should exit successfully. 78 | -------------------------------------------------------------------------------- /docs/public/why/pure-python-no-cli.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Why does HitchStory have no CLI runner - only a pure python API? 3 | --- 4 | 5 | HitchStory aims to be usable as both as a self-contained library running 6 | within pytest or on its own via a custom made runner. 7 | 8 | There are three reasons for this: 9 | 10 | 11 | ## 1. Easy integration with pytest 12 | 13 | Most people already use pytest as a test runner. The pure python 14 | API makes it easy to integrate hitchstory with it. 15 | 16 | ## 2. It's still easy to create a command line runner if you like 17 | 18 | The [the skeleton runner is documented here](../../using/setup/basic-cli) 19 | if you'd prefer not to use pytest. 20 | 21 | 22 | ## 2. For complex test strategies the flexibility of a Python API is very valuable 23 | 24 | After dogfooding this framework for a long while, I have come to realize that 25 | the requirements for running tests vary significantly and usually require 26 | unique customization on a project specific basis. Examples include: 27 | 28 | * Running the same set of tests under a new and an old version of python. 29 | * Running tests against either a local version of the application or a deployed version. 30 | * Running a large set of parameterized tests on a full run, others on a quick validation run. 31 | * Orchestrating the tests from a different machine to the machine that the tests are run on, for parallelization purposes. 32 | 33 | Some of these things can be achieved by writing bash scripts or plugins, 34 | but python still gives you more options to customize. 35 | 36 | -------------------------------------------------------------------------------- /docs/src/why/pure-python-no-cli.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Why does HitchStory have no CLI runner - only a pure python API? 3 | --- 4 | 5 | HitchStory aims to be usable as both as a self-contained library running 6 | within pytest or on its own via a custom made runner. 7 | 8 | There are three reasons for this: 9 | 10 | 11 | ## 1. Easy integration with pytest 12 | 13 | Most people already use pytest as a test runner. The pure python 14 | API makes it easy to integrate hitchstory with it. 15 | 16 | ## 2. It's still easy to create a command line runner if you like 17 | 18 | The [the skeleton runner is documented here](../../using/setup/basic-cli) 19 | if you'd prefer not to use pytest. 20 | 21 | 22 | ## 2. For complex test strategies the flexibility of a Python API is very valuable 23 | 24 | After dogfooding this framework for a long while, I have come to realize that 25 | the requirements for running tests vary significantly and usually require 26 | unique customization on a project specific basis. Examples include: 27 | 28 | * Running the same set of tests under a new and an old version of python. 29 | * Running tests against either a local version of the application or a deployed version. 30 | * Running a large set of parameterized tests on a full run, others on a quick validation run. 31 | * Orchestrating the tests from a different machine to the machine that the tests are run on, for parallelization purposes. 32 | 33 | Some of these things can be achieved by writing bash scripts or plugins, 34 | but python still gives you more options to customize. 35 | 36 | -------------------------------------------------------------------------------- /.github/workflows/examples.yml: -------------------------------------------------------------------------------- 1 | name: Example Tests Running on Github Actions 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - examples/commandline/** 9 | - examples/restapi/** 10 | - examples/pythonapi/** 11 | 12 | jobs: 13 | regression: 14 | timeout-minutes: 30 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: checkout repo 18 | uses: actions/checkout@v2 19 | 20 | #- name: enable arm containers 21 | #run: | 22 | #sudo apt-get update 23 | #sudo apt-get install qemu-system-arm qemu-efi qemu-user-static -y 24 | 25 | # REST API Integration Tests 26 | - name: build restapi 27 | run: | 28 | cd examples/restapi 29 | ./run.sh make 30 | 31 | - name: regression test restapi 32 | run: | 33 | cd examples/restapi 34 | ./run.sh regression 35 | 36 | # Interactive Command Line Integration Tests 37 | - name: build command line 38 | run: | 39 | cd examples/commandline 40 | ./run.sh make 41 | 42 | #- name: regression test commandline example 43 | # run: | 44 | # cd examples/commandline 45 | # ./run.sh pytest 46 | 47 | 48 | # Python API Integration Tests 49 | - name: build python api 50 | run: | 51 | cd examples/pythonapi 52 | ./run.sh make 53 | 54 | - name: regression test python api 55 | run: | 56 | cd examples/pythonapi 57 | ./run.sh pytest 58 | -------------------------------------------------------------------------------- /hitch/code_that_does_things.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | 4 | class ExampleException(Exception): 5 | """ 6 | This is a demonstration exception docstring. 7 | 8 | It spreads across multiple lines. 9 | """ 10 | 11 | pass 12 | 13 | 14 | def should_run(which): 15 | with open("should{0}.txt".format(which), "w") as handle: 16 | handle.write("ran!") 17 | 18 | 19 | def should_not_run(): 20 | raise RuntimeError("This shouldn't have happened") 21 | 22 | 23 | def raise_example_exception(text=""): 24 | raise ExampleException(text) 25 | 26 | 27 | def output(contents): 28 | with open("output.txt", "w") as handle: 29 | handle.write("{0}\n".format(contents)) 30 | 31 | 32 | def append(contents): 33 | with open("output.txt", "a") as handle: 34 | handle.write("{0}\n".format(contents)) 35 | 36 | 37 | def reticulate_splines(): 38 | with open("splines_reticulated.txt", "w") as handle: 39 | handle.write("{0}\n".format("splines_reticulated")) 40 | 41 | 42 | def kick_llamas_ass(): 43 | with open("kicked_llamas_ass.txt", "w") as handle: 44 | handle.write("{0}\n".format("kicked_llamas_ass")) 45 | 46 | 47 | def tear_down_was_run(): 48 | with open("tear_down_was_run.txt", "w") as handle: 49 | handle.write("{0}\n".format("tear_down_was_run")) 50 | 51 | 52 | def fill_form(name, value): 53 | with open("{0}.txt".format(name), "w") as handle: 54 | handle.write(value) 55 | 56 | 57 | def click(name): 58 | with open("click.txt".format(name), "a") as handle: 59 | handle.write(name) 60 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.setuptools] 6 | packages = ["hitchstory"] 7 | 8 | [project] 9 | name = "hitchstory" 10 | authors = [ 11 | {name = "Colm O'Connor", email = "colm.oconnor.github@gmail.com"}, 12 | ] 13 | description = "Type-safe YAML-based example specification driven development framework for python." 14 | license = {file = "LICENSE.txt"} 15 | requires-python = ">=3.7.0" 16 | keywords = ["yaml", "hitchdev", "bdd", "tdd", "testing", "tests"] 17 | classifiers = [ 18 | "Programming Language :: Python :: 3", 19 | "Topic :: Software Development :: Quality Assurance", 20 | "Topic :: Software Development :: Testing", 21 | "Topic :: Text Processing :: Markup", 22 | "Topic :: Software Development :: Libraries", 23 | "Natural Language :: English", 24 | "Environment :: Console", 25 | ] 26 | dependencies = [ 27 | "strictyaml>=1.4.3", 28 | "path.py>=9.0", 29 | "jinja2>=2.9", 30 | "colorama>=0.3.8", 31 | "python-slugify>=1.2.1", 32 | "prettystack>=0.3.0", 33 | "psutil>=5.0.0", 34 | "mergedeep>=1.2.0", 35 | "click>=7.1.2" 36 | ] 37 | dynamic = ["version", "readme"] 38 | 39 | [project.urls] 40 | homepage = "https://hitchdev.com/hitchstory" 41 | documentation = "https://hitchdev.com/hitchstory/using" 42 | repository = "https://github.com/hitchdev/hitchstory" 43 | changelog = "https://hitchdev.com/hitchstory/changelog" 44 | 45 | [tool.setuptools.dynamic] 46 | readme = {file = ["README.md",], content-type = "text/markdown"} 47 | version = {file = "VERSION"} 48 | -------------------------------------------------------------------------------- /.github/workflows/example-website.yml: -------------------------------------------------------------------------------- 1 | name: Example Website Tests Running on Github Actions 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | # Only pay attention to the website folder 9 | paths: 10 | - examples/website/** 11 | 12 | jobs: 13 | regression: 14 | timeout-minutes: 30 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: checkout repo 18 | uses: actions/checkout@v2 19 | 20 | #- name: enable arm containers 21 | #run: | 22 | #sudo apt-get update 23 | #sudo apt-get install qemu-system-arm qemu-efi qemu-user-static -y 24 | 25 | # Website Integration Tests 26 | - name: build environment 27 | run: | 28 | cd examples/website 29 | ./run.sh make 30 | 31 | - name: regression test 32 | run: | 33 | cd examples/website 34 | ./run.sh regression 35 | 36 | - name: save regression test artefacts 37 | if: always() 38 | uses: actions/upload-artifact@v3 39 | with: 40 | name: regression-test-artefacts 41 | path: | 42 | examples/website/hitch/artefacts/** 43 | 44 | #- name: Code Coverage Report 45 | # uses: irongut/CodeCoverageSummary@v1.3.0 46 | # with: 47 | # filename: examples/website/artefacts/coverage.xml 48 | # badge: true 49 | # fail_below_min: true 50 | # format: markdown 51 | # hide_branch_rate: false 52 | # hide_complexity: false 53 | # indicators: true 54 | # output: both 55 | # thresholds: '60 80' 56 | -------------------------------------------------------------------------------- /examples/commandline/story/correct-my-spelling.story: -------------------------------------------------------------------------------- 1 | Correct my spelling: 2 | about: | 3 | In this story we call the API and send it misspellings. 4 | 5 | The API uses TextBlob (https://textblob.readthedocs.io/en/dev/) 6 | to detect misspellings and replies to the API with a suggestion 7 | instead of adding it to the to do list. 8 | steps: 9 | - display: |- 10 | To-do list: 11 | Options: 12 | 1. Add item 13 | 2. Remove item 14 | 3. Quit 15 | Enter your choice: 16 | 17 | - enter text: 1 18 | 19 | - display: |- 20 | To-do list: 21 | Options: 22 | 1. Add item 23 | 2. Remove item 24 | 3. Quit 25 | Enter your choice: 1 26 | Enter a to-do item: 27 | 28 | - enter text: biuy breod 29 | 30 | - display: |- 31 | To-do list: 32 | Options: 33 | 1. Add item 34 | 2. Remove item 35 | 3. Quit 36 | Enter your choice: 1 37 | Enter a to-do item: biuy breod 38 | Did you mean "buy bread"? 39 | Enter Y to confirm, or any other key to re-enter: 40 | 41 | - enter text: Y 42 | 43 | - display: |- 44 | To-do list: 45 | Options: 46 | 1. Add item 47 | 2. Remove item 48 | 3. Quit 49 | Enter your choice: 1 50 | Enter a to-do item: biuy breod 51 | Did you mean "buy bread"? 52 | Enter Y to confirm, or any other key to re-enter: Y 53 | To-do list: 54 | 1. buy bread 55 | Options: 56 | 1. Add item 57 | 2. Remove item 58 | 3. Quit 59 | Enter your choice: 60 | 61 | - enter text: 3 62 | 63 | - exit successfully 64 | -------------------------------------------------------------------------------- /examples/pythonapi/app/todo.py: -------------------------------------------------------------------------------- 1 | import json 2 | from textblob import TextBlob 3 | 4 | 5 | class Misspelling(Exception): 6 | pass 7 | 8 | 9 | def load_data(): 10 | try: 11 | with open("data.json", "r") as f: 12 | data = json.load(f) 13 | except FileNotFoundError: 14 | data = [] 15 | return data 16 | 17 | 18 | def save_data(data): 19 | with open("data.json", "w") as f: 20 | json.dump(data, f) 21 | 22 | 23 | def list_items(): 24 | for item in load_data(): 25 | print(item) 26 | return 27 | 28 | 29 | def add_item(item): 30 | corrected_item = correct_spelling(item) 31 | if corrected_item != item: 32 | raise Misspelling(f'Did you mean "{corrected_item}"?') 33 | print(f"To do added {item}") 34 | data.append(item) 35 | save_data(data) 36 | 37 | 38 | def remove_item(): 39 | print("Current to-do list:") 40 | for i, item in enumerate(data): 41 | print(f"{i + 1}. {item}") 42 | try: 43 | index = int(input("Enter the number of the item to remove: ")) 44 | data.pop(index - 1) 45 | save_data(data) 46 | except (ValueError, IndexError): 47 | print("Invalid input. Please enter a valid number.") 48 | 49 | 50 | def correct_spelling(text): 51 | blob = TextBlob(text) 52 | corrected = str(blob.correct()) 53 | if corrected != text: 54 | return suggest_spelling(text) 55 | else: 56 | return corrected 57 | 58 | 59 | def suggest_spelling(text): 60 | blob = TextBlob(text) 61 | suggestion = str(blob.correct()) 62 | return suggestion 63 | 64 | 65 | data = load_data() 66 | -------------------------------------------------------------------------------- /hitch/story/abort.story: -------------------------------------------------------------------------------- 1 | Abort a story with ctrl-C: 2 | docs: behavior/aborting 3 | about: | 4 | When an in-progress story is hit with any of the 5 | following termination signals: 6 | 7 | * SIGTERM 8 | * SIGINT 9 | * SIGQUIT 10 | * SIGHUP 11 | 12 | Then it triggers the tear_down method of the 13 | engine. 14 | 15 | In practical terms this means that if you are running 16 | a series of stories, Ctrl-C should halt current execution, 17 | run tear_down and then not run any more stories. 18 | given: 19 | files: 20 | example.story: | 21 | Create files: 22 | steps: 23 | - Pause forever 24 | 25 | Should never run: 26 | steps: 27 | - Should not happen 28 | engine.py: | 29 | from hitchstory import BaseEngine 30 | from code_that_does_things import reticulate_splines 31 | import psutil 32 | 33 | class Engine(BaseEngine): 34 | def pause_forever(self): 35 | psutil.Process().terminate() 36 | 37 | def should_not_happen(self): 38 | raise Exception("This exception should never be triggered") 39 | 40 | def tear_down(self): 41 | print("Reticulate splines") 42 | setup: | 43 | from hitchstory import StoryCollection 44 | from pathlib import Path 45 | from engine import Engine 46 | steps: 47 | - Run: 48 | code: StoryCollection(Path(".").glob("*.story"), Engine()).ordered_by_name().play() 49 | will output: |- 50 | RUNNING Create files in /path/to/working/example.story ... Aborted 51 | Reticulate splines 52 | -------------------------------------------------------------------------------- /docs/public/approach/executable-specifications.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Executable specifications 3 | --- 4 | 5 | An executable specification is an idea taken from 6 | [agile philosophy](https://agilemodeling.com/essays/executablespecifications.htm): 7 | 8 | >When trying to understand a class or operation most programmers will first look for sample code that already invokes it. Well-written unit/developers tests do exactly this – they provide a working specification of your functional code – and as a result unit tests effectively become a significant portion of your technical documentation. Similarly, acceptance tests can form an important part of your requirements documentation. This makes a lot of sense when you stop and think about it. Your acceptance tests define exactly what your stakeholders expect of your system, therefore they specify your critical requirements. 9 | 10 | It differs from a normal test primarily because it doubles as a means of clearly *describing* 11 | the software behavior *as well as* something you can feed to a machine that will 12 | test your code. 13 | 14 | The three preconditions for this are: 15 | 16 | * A clear separation of test concerns between specification and execution. 17 | * A clear segregation barrier between the environment that *executes* your tests and the environment *under* test. 18 | * The executable specifications are described using declarative markup instead of turing complete code. 19 | 20 | Executable specifications are formed at the nexus of Behavior Driven Development and Acceptance Test Driven Development. 21 | 22 | In order for Executable specifications to be used effectively they need to be written in a domain appropriate scenario language. 23 | -------------------------------------------------------------------------------- /docs/src/approach/executable-specifications.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Executable specifications 3 | --- 4 | 5 | An executable specification is an idea taken from 6 | [agile philosophy](https://agilemodeling.com/essays/executablespecifications.htm): 7 | 8 | >When trying to understand a class or operation most programmers will first look for sample code that already invokes it. Well-written unit/developers tests do exactly this – they provide a working specification of your functional code – and as a result unit tests effectively become a significant portion of your technical documentation. Similarly, acceptance tests can form an important part of your requirements documentation. This makes a lot of sense when you stop and think about it. Your acceptance tests define exactly what your stakeholders expect of your system, therefore they specify your critical requirements. 9 | 10 | It differs from a normal test primarily because it doubles as a means of clearly *describing* 11 | the software behavior *as well as* something you can feed to a machine that will 12 | test your code. 13 | 14 | The three preconditions for this are: 15 | 16 | * A clear separation of test concerns between specification and execution. 17 | * A clear segregation barrier between the environment that *executes* your tests and the environment *under* test. 18 | * The executable specifications are described using declarative markup instead of turing complete code. 19 | 20 | Executable specifications are formed at the nexus of Behavior Driven Development and Acceptance Test Driven Development. 21 | 22 | In order for Executable specifications to be used effectively they need to be written in a domain appropriate scenario language. 23 | -------------------------------------------------------------------------------- /hitch/story/matching-strings.story: -------------------------------------------------------------------------------- 1 | Match two strings and show diff on failure: 2 | docs: engine/match-two-strings 3 | based on: handling failing tests 4 | about: | 5 | While you could use `assert expected == actual` to match 6 | two strings in a story step, if you use `strings_match(expected, actual)` 7 | instead then when it fails: 8 | 9 | * It will show the actual string, expected string *and a diff*. 10 | * It will raise a Failure exception and avoid polluting the error message with the full stacktrace. 11 | 12 | An example is shown below: 13 | given: 14 | files: 15 | example.story: | 16 | Failing story: 17 | steps: 18 | - Pass because strings match 19 | - Fail because strings don't match 20 | engine.py: | 21 | from hitchstory import BaseEngine, strings_match 22 | 23 | class Engine(BaseEngine): 24 | def pass_because_strings_match(self): 25 | strings_match("hello", "hello") 26 | 27 | def fail_because_strings_dont_match(self): 28 | strings_match("hello", "goodbye") 29 | 30 | steps: 31 | - Run: 32 | code: story_collection.one().play() 33 | will output: |- 34 | RUNNING Failing story in /path/to/working/example.story ... FAILED in 0.1 seconds. 35 | 36 | steps: 37 | - Pass because strings match 38 | - Fail because strings don't match 39 | 40 | 41 | hitchstory.exceptions.Failure 42 | 43 | Test failed. 44 | 45 | ACTUAL: 46 | goodbye 47 | 48 | EXPECTED: 49 | hello 50 | 51 | DIFF: 52 | - hello+ goodbye 53 | -------------------------------------------------------------------------------- /examples/website/hitch/docs/add-and-retrieve-todo.md: -------------------------------------------------------------------------------- 1 | # Add and retrieve todo 2 | 3 | The user adds "buy bread" to the to do list 4 | and sees it showing up. 5 | 6 | 7 | * Fake related JIRAS: [FEATURE-341](https://myproject.jira.com/FEATURE-341), [FEATURE-441](https://myproject.jira.com/FEATURE-441) 8 | 9 | ## Video 10 | 11 | 15 | 16 | ## Steps 17 | 18 | 19 | * When the website is loaded 20 | 21 | 25 | 26 | 27 | 28 | * Enter text `admin` on `username`. 29 | 30 | * Enter text `password` on `password`. 31 | 32 | * Click on `submit`. 33 | 34 | 35 | 36 | 37 | * Then text `Todo List` should appear on `title`. 38 | 39 | 40 | 44 | 45 | * Enter text `Add bread` on `todo text`. 46 | 47 | * Click on `add`. 48 | 49 | 50 | * Then text `Add bread` should appear on `first todo list item`. 51 | 52 | 53 | 57 | 58 | 59 | 60 | 61 | ## Autogenerated 62 | 63 | This markdown page was automatically generated from [this story](https://github.com/hitchdev/hitchstory/blob/master/examples/website/hitch/story/add-todo.story) [with this template](https://github.com/hitchdev/hitchstory/blob/master/examples/website/hitch/docstory.yml). 64 | 65 | The screenshots and video recordings were autogenerated via playwright playing the story with this [story engine](https://github.com/hitchdev/hitchstory/blob/master/examples/website/tests/test_integration.py). 66 | -------------------------------------------------------------------------------- /examples/llm/hitch/llm.py: -------------------------------------------------------------------------------- 1 | from openai import OpenAI 2 | from pathlib import Path 3 | import json 4 | 5 | OPENAI_API_KEY = Path(__file__).parents[0].joinpath("OPENAI_API_KEY").read_text().rstrip() 6 | 7 | 8 | class LLMAnswers: 9 | def __init__(self): 10 | self._client = OpenAI(api_key=OPENAI_API_KEY) 11 | self._prompt = "You should answer questions about the following text:\n\n{context}" 12 | 13 | def ask(self, context, question): 14 | response = self._client.chat.completions.create( 15 | model="gpt-3.5-turbo", 16 | messages=[ 17 | {"role": "system", "content": self._prompt.format(context=context)}, 18 | {"role": "user", "content": question}, 19 | ], 20 | temperature=1, 21 | max_tokens=256, 22 | top_p=1, 23 | frequency_penalty=0, 24 | presence_penalty=0, 25 | ) 26 | return json.loads(response.json())["choices"][0]["message"]["content"] 27 | 28 | 29 | class LLMServer: 30 | def __init__(self, prompt): 31 | self._client = OpenAI(api_key=OPENAI_API_KEY) 32 | self._prompt = prompt 33 | 34 | def run(self, messages): 35 | response = self._client.chat.completions.create( 36 | response_format={"type": "json_object"}, 37 | model="gpt-3.5-turbo-0125", 38 | messages=[{"role": "system", "content": self._prompt}] + messages, 39 | temperature=1, 40 | max_tokens=256, 41 | top_p=1, 42 | frequency_penalty=0, 43 | presence_penalty=0, 44 | ) 45 | return json.loads(response.json())["choices"][0]["message"]["content"] 46 | -------------------------------------------------------------------------------- /examples/pythonapi/README.md: -------------------------------------------------------------------------------- 1 | # HitchStory Python API Tests Example 2 | 3 | ## Run them yourself 4 | 5 | **Podman must be installed on your system first.** 6 | 7 | All other functionality is automated and can be run via one of the 8 | four run.sh scripts. 9 | 10 | To begin: 11 | 12 | ```bash 13 | $ git clone https://github.com/hitchdev/hitchstory.git 14 | $ cd hitchstory/examples/pythonapi 15 | $ ./run.sh make 16 | ``` 17 | 18 | `./run.sh make` downloads and builds the container and python packages the 19 | tests need to run in an isolated environment for each of the respective projects. 20 | 21 | 22 | ## Run all tests 23 | 24 | ``` 25 | $ ./run.sh regression 26 | ``` 27 | 28 | ## Run a single test 29 | 30 | This command can be used to craft a new feature and do 31 | acceptance test driven development on it: 32 | 33 | ``` 34 | $ ./run.sh atdd correct 35 | ``` 36 | 37 | "correct" is a unique keyword used in the name of one of the stories. 38 | 39 | ## Run singular test in rewrite mode 40 | 41 | If you tweak the wordings in the command line app and run this, it will 42 | update the story accordingly. 43 | 44 | ``` 45 | $ ./run.sh ratdd correct 46 | ``` 47 | 48 | ## Generate documentation from stories 49 | 50 | This will regenerate all of the markdown docs for the project: 51 | 52 | ``` 53 | $ ./run.sh docgen 54 | ``` 55 | 56 | ## Clean up everything 57 | 58 | Everything runs in one podman container and volume. This deletes them: 59 | 60 | ``` 61 | $ ./run.sh clean all 62 | ``` 63 | 64 | # Github Actions 65 | 66 | These integration tests are run via github actions on every push. See here: 67 | 68 | * [Github actions YAML](https://github.com/hitchdev/hitchstory/blob/master/.github/workflows/examples.yml) 69 | * [Runner](https://github.com/hitchdev/hitchstory/actions/workflows/examples.yml) 70 | -------------------------------------------------------------------------------- /examples/website/hitch/docs/correct-my-spelling.md: -------------------------------------------------------------------------------- 1 | # Correct my spelling 2 | 3 | The user tries to add "biuy breod" to the to do list 4 | but the application tries to correct the spelling. 5 | 6 | 7 | 8 | 9 | ## Video 10 | 11 | 15 | 16 | ## Steps 17 | 18 | 19 | * When the website is loaded 20 | 21 | 25 | 26 | 27 | 28 | * Enter text `admin` on `username`. 29 | 30 | * Enter text `password` on `password`. 31 | 32 | * Click on `submit`. 33 | 34 | 35 | 36 | 37 | * Then text `Todo List` should appear on `title`. 38 | 39 | 40 | 44 | 45 | * Enter text `biuy breod` on `todo text`. 46 | 47 | * Click on `add`. 48 | 49 | 50 | * Then text `Did you mean 'buy bread'?` should appear on `error`. 51 | 52 | 53 | 57 | 58 | 59 | 60 | ## Background context 61 | 62 | The website uses TextBlob (https://textblob.readthedocs.io/en/dev/) 63 | to detect misspellings and replies to the API with a suggestion 64 | instead of adding it to the to do list. 65 | 66 | 67 | 68 | ## Autogenerated 69 | 70 | This markdown page was automatically generated from [this story](https://github.com/hitchdev/hitchstory/blob/master/examples/website/story/correct-my-spelling.story) [with this template](https://github.com/hitchdev/hitchstory/blob/master/examples/website/tests/docstory.yml). 71 | 72 | The screenshots and video recordings were autogenerated via playwright playing the story with this [story engine](https://github.com/hitchdev/hitchstory/blob/master/examples/website/hitch/engine.py). 73 | -------------------------------------------------------------------------------- /hitch/story/success.story: -------------------------------------------------------------------------------- 1 | Running a single named story successfully: 2 | docs: behavior/run-single-named-story 3 | about: | 4 | How a story runs when it is successful - i.e. when no exception 5 | is raised during its run. 6 | given: 7 | files: 8 | example.story: | 9 | Create files: 10 | steps: 11 | - Create file 12 | - Create file: step2.txt 13 | - Create file: 14 | file name: step3.txt 15 | content: third step 16 | engine.py: | 17 | from hitchstory import BaseEngine 18 | 19 | 20 | class Engine(BaseEngine): 21 | def create_file(self, file_name="step1.txt", content="example"): 22 | with open(file_name, 'w') as handle: 23 | handle.write(content) 24 | 25 | def on_success(self): 26 | print("splines reticulated") 27 | 28 | with open("ranstory.txt", 'w') as handle: 29 | handle.write(self.story.name) 30 | setup: | 31 | from hitchstory import StoryCollection 32 | from pathlib import Path 33 | from engine import Engine 34 | steps: 35 | - Run: 36 | code: | 37 | StoryCollection(Path(".").glob("*.story"), Engine()).named("Create files").play() 38 | will output: |- 39 | RUNNING Create files in /path/to/working/example.story ... splines reticulated 40 | SUCCESS in 0.1 seconds. 41 | - File was created with: 42 | filename: step1.txt 43 | contents: example 44 | - File was created with: 45 | filename: step2.txt 46 | contents: example 47 | - File was created with: 48 | filename: step3.txt 49 | contents: third step 50 | - File was created with: 51 | filename: ranstory.txt 52 | contents: Create files 53 | -------------------------------------------------------------------------------- /examples/llm/hitch/hitchreqs.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.12 3 | # by the following command: 4 | # 5 | # pip-compile --output-file=hitch/hitchreqs.txt hitch/hitchreqs.in 6 | # 7 | annotated-types==0.6.0 8 | # via pydantic 9 | anyio==4.3.0 10 | # via 11 | # httpx 12 | # openai 13 | certifi==2024.2.2 14 | # via 15 | # httpcore 16 | # httpx 17 | click==8.1.7 18 | # via hitchstory 19 | colorama==0.4.6 20 | # via 21 | # hitchstory 22 | # prettystack 23 | distro==1.9.0 24 | # via openai 25 | h11==0.14.0 26 | # via httpcore 27 | hitchstory==0.24.0 28 | # via -r hitch/hitchreqs.in 29 | httpcore==1.0.4 30 | # via httpx 31 | httpx==0.27.0 32 | # via openai 33 | idna==3.6 34 | # via 35 | # anyio 36 | # httpx 37 | jinja2==3.1.3 38 | # via 39 | # hitchstory 40 | # prettystack 41 | markupsafe==2.1.5 42 | # via jinja2 43 | mergedeep==1.3.4 44 | # via hitchstory 45 | openai==1.12.0 46 | # via -r hitch/hitchreqs.in 47 | path==16.10.0 48 | # via path-py 49 | path-py==12.5.0 50 | # via 51 | # hitchstory 52 | # prettystack 53 | prettystack==0.4.0 54 | # via hitchstory 55 | psutil==5.9.8 56 | # via hitchstory 57 | pydantic==2.6.2 58 | # via openai 59 | pydantic-core==2.16.3 60 | # via pydantic 61 | python-dateutil==2.8.2 62 | # via strictyaml 63 | python-slugify==8.0.4 64 | # via hitchstory 65 | six==1.16.0 66 | # via python-dateutil 67 | sniffio==1.3.0 68 | # via 69 | # anyio 70 | # httpx 71 | # openai 72 | strictyaml==1.7.3 73 | # via hitchstory 74 | text-unidecode==1.3 75 | # via python-slugify 76 | tqdm==4.66.2 77 | # via openai 78 | typing-extensions==4.9.0 79 | # via 80 | # openai 81 | # pydantic 82 | # pydantic-core 83 | -------------------------------------------------------------------------------- /examples/website/hitch/cli.py: -------------------------------------------------------------------------------- 1 | from hitchstory import StoryCollection 2 | from click import argument, group, pass_context 3 | from engine import Engine 4 | from directories import DIR 5 | from os import getenv 6 | 7 | 8 | def _collection(**args): 9 | return StoryCollection( 10 | DIR.STORY.glob("*.story"), 11 | Engine(**args), 12 | ) 13 | 14 | 15 | @group(invoke_without_command=True) 16 | @pass_context 17 | def cli(ctx): 18 | """Integration test command line interface.""" 19 | pass 20 | 21 | 22 | @cli.command() 23 | @argument("keywords", nargs=-1) 24 | def rbdd(keywords): 25 | """ 26 | Run story with name containing keywords and rewrite. 27 | """ 28 | _collection(rewrite=True).shortcut(*keywords).play() 29 | 30 | 31 | @cli.command() 32 | @argument("keywords", nargs=-1) 33 | def bdd(keywords): 34 | """ 35 | Run story with name containing keywords. 36 | """ 37 | _collection(rewrite=False).shortcut(*keywords).play() 38 | 39 | 40 | @cli.command() 41 | @argument("keywords", nargs=-1) 42 | def vbdd(keywords): 43 | """ 44 | Run story with name containing keywords. 45 | """ 46 | _collection(vnc=True).shortcut(*keywords).play() 47 | 48 | 49 | @cli.command() 50 | def regression(): 51 | """ 52 | Continuos integration - lint and run all stories. 53 | """ 54 | _collection().only_uninherited().ordered_by_name().play() 55 | 56 | 57 | @cli.command() 58 | def docdb(): 59 | """ 60 | Rebuild a current representation of the database. 61 | """ 62 | from services import Services 63 | from db_fixtures import DbFixture 64 | 65 | services = Services( 66 | env={"VNC": "no"}, 67 | ports=[5432], 68 | timeout=5, 69 | ) 70 | services.start(DbFixture({})) 71 | services.stop() 72 | 73 | 74 | if __name__ == "__main__": 75 | cli() 76 | -------------------------------------------------------------------------------- /docs/public/using/behavior/aborting.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Abort a story with ctrl-C 3 | --- 4 | 5 | 6 | 7 | When an in-progress story is hit with any of the 8 | following termination signals: 9 | 10 | * SIGTERM 11 | * SIGINT 12 | * SIGQUIT 13 | * SIGHUP 14 | 15 | Then it triggers the tear_down method of the 16 | engine. 17 | 18 | In practical terms this means that if you are running 19 | a series of stories, Ctrl-C should halt current execution, 20 | run tear_down and then not run any more stories. 21 | 22 | 23 | # Code Example 24 | 25 | 26 | 27 | example.story: 28 | 29 | ```yaml 30 | Create files: 31 | steps: 32 | - Pause forever 33 | 34 | Should never run: 35 | steps: 36 | - Should not happen 37 | ``` 38 | engine.py: 39 | 40 | ```python 41 | from hitchstory import BaseEngine 42 | from code_that_does_things import reticulate_splines 43 | import psutil 44 | 45 | class Engine(BaseEngine): 46 | def pause_forever(self): 47 | psutil.Process().terminate() 48 | 49 | def should_not_happen(self): 50 | raise Exception("This exception should never be triggered") 51 | 52 | def tear_down(self): 53 | print("Reticulate splines") 54 | ``` 55 | 56 | With code: 57 | 58 | ```python 59 | from hitchstory import StoryCollection 60 | from pathlib import Path 61 | from engine import Engine 62 | 63 | ``` 64 | 65 | 66 | 67 | 68 | 69 | 70 | ```python 71 | StoryCollection(Path(".").glob("*.story"), Engine()).ordered_by_name().play() 72 | ``` 73 | 74 | Will output: 75 | ``` 76 | RUNNING Create files in /path/to/working/example.story ... Aborted 77 | Reticulate splines 78 | ``` 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | !!! note "Executable specification" 89 | 90 | Documentation automatically generated from 91 | abort.story 92 | storytests. 93 | 94 | -------------------------------------------------------------------------------- /hitch/story/bugs.story: -------------------------------------------------------------------------------- 1 | Re-use of with_documentation fails: 2 | about: | 3 | Original bug - second .ordered_by_file() would 4 | result in WithDocumentationMissing exception. 5 | based on: base documentation 6 | steps: 7 | - Run: 8 | code: | 9 | print( 10 | jenv.from_string(Path("index.jinja2").read_text()).render( 11 | story_list=story_collection.with_documentation( 12 | Path("document.yaml").read_text(), 13 | ).ordered_by_file() 14 | ) 15 | ) 16 | 17 | print( 18 | jenv.from_string(Path("index.jinja2").read_text()).render( 19 | story_list=story_collection.with_documentation( 20 | Path("document.yaml").read_text(), 21 | ).ordered_by_file() 22 | ) 23 | ) 24 | 25 | Rewrite step in inherited story: 26 | based on: Story that rewrites itself 27 | given: 28 | files: 29 | inherited.story: | 30 | Following steps: 31 | based on: Append text to file 32 | following steps: 33 | - run and get output: 34 | command: cat mytext.txt 35 | will output: old value 36 | 37 | replacement steps: 38 | - Run: 39 | code: | 40 | StoryCollection(Path(".").glob("*.story"), Engine(rewrite=True)).named("Following steps").play() 41 | will output: RUNNING Following steps in /path/to/working/inherited.story ... 42 | SUCCESS in 0.1 seconds. 43 | 44 | 45 | - File contents will be: 46 | filename: inherited.story 47 | contents: |- 48 | Following steps: 49 | based on: Append text to file 50 | following steps: 51 | - run and get output: 52 | command: cat mytext.txt 53 | will output: |- 54 | hello 55 | hello 56 | 57 | 58 | -------------------------------------------------------------------------------- /examples/website/hitch/compare_screenshots.py: -------------------------------------------------------------------------------- 1 | """ 2 | Rudimentary screenshot testing done with the amazing 3 | https://github.com/whtsky/pixelmatch-py library. 4 | 5 | On failure, it spits out a "diff" image. 6 | 7 | Screenshot testing can be a bit flaky, especially on complex 8 | apps. I don't necessarily recommend it for all use cases. 9 | 10 | This flakiness can happen because of: 11 | 12 | * Playwright quirks. 13 | * Small differences in timings - e.g. icons loading fast on one environment and showing up and slower on another. 14 | * Nondeterministic rendering - e.g. lists displayed in a non-guaranteed order. 15 | * Time differences - e.g. if a date is displayed at the top of a page. 16 | """ 17 | from pixelmatch.contrib.PIL import pixelmatch 18 | from hitchstory import Failure 19 | from pathlib import Path 20 | from io import BytesIO 21 | from PIL import Image 22 | 23 | 24 | def compare_screenshots( 25 | screenshot: bytes, 26 | golden_snapshot_path: Path, 27 | diff_snapshot_path: Path, 28 | threshold=0.1, 29 | ): 30 | image = Image.open(BytesIO(screenshot)) 31 | golden = Image.open(golden_snapshot_path) 32 | 33 | if image.width != golden.width: 34 | raise Failure( 35 | f"Snapshot failure. Screenshot width {image.width}, Golden master width {golden.width}" 36 | ) 37 | 38 | if image.height != golden.height: 39 | raise Failure( 40 | f"Snapshot failure. Screenshot height {image.height}, Golden master height {golden.height}" 41 | ) 42 | 43 | diff_pixels = pixelmatch(image, golden, threshold=threshold) 44 | 45 | # Currently failing in github actions 46 | if diff_pixels != 0: 47 | image_diff = Image.new("RGBA", image.size) 48 | pixelmatch(image, golden, image_diff, threshold=threshold) 49 | image_diff.save(diff_snapshot_path) 50 | raise Failure("Screenshot test failure") 51 | -------------------------------------------------------------------------------- /docs/src/approach/human-writable.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: ANTIPATTERN - Analysts writing stories for the developer 3 | --- 4 | 5 | >Gherkin allows Business Analysts to document acceptance tests in a language developers, QA & the business can understand (i.e. the language of Gherkin). By having a common language to describe acceptance tests, it encourages collaboration and a common understanding of the tests being run. - [Gherkin for business analysts](https://www.modernanalyst.com/Resources/Articles/tabid/115/ID/3810/Gherkin-for-Business-Analysts.aspx) 6 | 7 | Having analysts write and read stories instead of programmers was an explicit goal of most BDD tools like Cucumber. This was an enticing prospect - the developer wouldn't have to write tests (what a chore!). The product owner 8 | 9 | However, the reality of these tools is that stakeholders are interested in even reading these stories, let alone writing them. 10 | 11 | This is an explicit non-goal of hitchstory. The framework is designed such that it does not have loops, conditionals or other such accoutrements of a programming language and *could* be written by a sophisticated product manager, but it is still a tool squarely aimed at developers. 12 | 13 | ## Story maintenance and writing is a bit like programming 14 | 15 | Well maintained stories will typically be readable and comprehensible by non-programmers with a good knowledge of the domain (unlike code), but they will likely not be very good at writing or maintaining high quality stories - at least, not without prior training. 16 | 17 | That said, *pairing* with business analysts while writing and maintaining stories, especially on an ad hoc basis can be a supremely effective workflow for doing [stakeholder collaboration](../bdd). 18 | 19 | 20 | ## Should testers write stories? 21 | 22 | While developers need to be involved in writing the stories, the developers primarily writing the stories need not necessarily be the developers writing the application. 23 | -------------------------------------------------------------------------------- /docs/public/approach/human-writable.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: ANTIPATTERN - Analysts writing stories for the developer 3 | --- 4 | 5 | >Gherkin allows Business Analysts to document acceptance tests in a language developers, QA & the business can understand (i.e. the language of Gherkin). By having a common language to describe acceptance tests, it encourages collaboration and a common understanding of the tests being run. - [Gherkin for business analysts](https://www.modernanalyst.com/Resources/Articles/tabid/115/ID/3810/Gherkin-for-Business-Analysts.aspx) 6 | 7 | Having analysts write and read stories instead of programmers was an explicit goal of most BDD tools like Cucumber. This was an enticing prospect - the developer wouldn't have to write tests (what a chore!). The product owner 8 | 9 | However, the reality of these tools is that stakeholders are interested in even reading these stories, let alone writing them. 10 | 11 | This is an explicit non-goal of hitchstory. The framework is designed such that it does not have loops, conditionals or other such accoutrements of a programming language and *could* be written by a sophisticated product manager, but it is still a tool squarely aimed at developers. 12 | 13 | ## Story maintenance and writing is a bit like programming 14 | 15 | Well maintained stories will typically be readable and comprehensible by non-programmers with a good knowledge of the domain (unlike code), but they will likely not be very good at writing or maintaining high quality stories - at least, not without prior training. 16 | 17 | That said, *pairing* with business analysts while writing and maintaining stories, especially on an ad hoc basis can be a supremely effective workflow for doing [stakeholder collaboration](../bdd). 18 | 19 | 20 | ## Should testers write stories? 21 | 22 | While developers need to be involved in writing the stories, the developers primarily writing the stories need not necessarily be the developers writing the application. 23 | -------------------------------------------------------------------------------- /hitch/envirotest.py: -------------------------------------------------------------------------------- 1 | import pyenv 2 | import sys 3 | import random 4 | 5 | 6 | def run_test( 7 | pyenv_build, 8 | pyproject_toml, 9 | test_package, 10 | prerequisites, 11 | strategy_name, 12 | _storybook, 13 | _doctests, 14 | ): 15 | if strategy_name == "latest": 16 | strategies = [ 17 | lambda versions: versions[-1], 18 | ] 19 | elif strategy_name == "earliest": 20 | strategies = [ 21 | lambda versions: versions[0], 22 | ] 23 | elif strategy_name in ("full", "maxi"): 24 | strategies = ( 25 | [ 26 | lambda versions: versions[0], 27 | ] 28 | + [random.choice] * 2 29 | if strategy_name == "full" 30 | else 5 + [lambda versions: versions[-1]] 31 | ) 32 | else: 33 | raise Exception(f"Strategy name {strategy_name} not found") 34 | 35 | for strategy in strategies: 36 | envirotestvenv = pyenv.EnvirotestVirtualenv( 37 | pyenv_build=pyenv_build, 38 | pyproject_toml=pyproject_toml, 39 | picker=strategy, 40 | test_package=test_package, 41 | prerequisites=prerequisites, 42 | ) 43 | envirotestvenv.build() 44 | 45 | python_path = envirotestvenv.venv.python_path 46 | 47 | _doctests(python_path) 48 | results = ( 49 | _storybook(python_path=python_path) 50 | .only_uninherited() 51 | .ordered_by_name() 52 | .play() 53 | ) 54 | if not results.all_passed: 55 | print("FAILED") 56 | print("COPY the following into hitch/devenv.yml:\n\n") 57 | print("python version: {}".format(envirotestvenv.python_version)) 58 | print("packages:") 59 | for package, version in envirotestvenv.picked_versions.items(): 60 | print(" {}: {}".format(package, version)) 61 | sys.exit(1) 62 | -------------------------------------------------------------------------------- /docs/src/approach/hermetic-end-to-end-test.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: The Hermetic End to End Testing Pattern 3 | --- 4 | # The Hermetic End to End Testing Pattern 5 | 6 | The hermetic end to end testing pattern is a pattern where: 7 | 8 | * The entire app is run as a cohesive whole. 9 | 10 | * The entire "outside world" for this app is run in a strictly controlled mock environment. For example, if it: 11 | 12 | ** Calls an external sandbox REST API. 13 | ** Uses a staging database whose exact data isn't controlled. 14 | ** Accesses the the internet. 15 | 16 | Then it is not hermetic. If it: 17 | 18 | ** Calls a mock REST API (e.g. using wiremock or mitmproxy). 19 | ** Uses a locally built database with consistent pre-defined fixtures. 20 | ** Doesn't access the internet. 21 | 22 | Then it is hermetic. 23 | 24 | ## Benefits 25 | 26 | Hermetic tests are: 27 | 28 | * Faster. 29 | * More consistent. 30 | * Trivially parallelizable 31 | 32 | ## Partially hermetic end to end tests 33 | 34 | Partial hermeticism is when every part of the end to end test is mocked, but it can be run in a mode that partially interacts with the outside world. 35 | 36 | An example of this would be a hermetic end to end test that calls a mocked Paypal REST API using wiremock or mitmprox, but which can be run in a mode that uses the sandboxed Paypal REST API instead. 37 | 38 | Partial hermeticism can be used as: 39 | 40 | * An efficient way of creating mocks (by recording the actual request / response). 41 | * A way of consistently testing "outside world" changes - e.g. if an API the app calls still works the same way tomorrow as it did yesterday. 42 | 43 | 44 | ## Further reading 45 | 46 | * [Google Testing Blog](https://testing.googleblog.com/2012/10/hermetic-servers.html) 47 | * [My Recent Obsession with Hermetic Tests](https://blog.testproject.io/2021/09/13/my-recent-obsession-with-hermetic-tests/) 48 | * [Hermetic Unit Testing](https://medium.com/geekculture/hermetic-unit-testing-8c49276e3acd) 49 | * [TestContainers](https://www.testcontainers.org/) 50 | -------------------------------------------------------------------------------- /docs/public/approach/hermetic-end-to-end-test.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: The Hermetic End to End Testing Pattern 3 | --- 4 | # The Hermetic End to End Testing Pattern 5 | 6 | The hermetic end to end testing pattern is a pattern where: 7 | 8 | * The entire app is run as a cohesive whole. 9 | 10 | * The entire "outside world" for this app is run in a strictly controlled mock environment. For example, if it: 11 | 12 | ** Calls an external sandbox REST API. 13 | ** Uses a staging database whose exact data isn't controlled. 14 | ** Accesses the the internet. 15 | 16 | Then it is not hermetic. If it: 17 | 18 | ** Calls a mock REST API (e.g. using wiremock or mitmproxy). 19 | ** Uses a locally built database with consistent pre-defined fixtures. 20 | ** Doesn't access the internet. 21 | 22 | Then it is hermetic. 23 | 24 | ## Benefits 25 | 26 | Hermetic tests are: 27 | 28 | * Faster. 29 | * More consistent. 30 | * Trivially parallelizable 31 | 32 | ## Partially hermetic end to end tests 33 | 34 | Partial hermeticism is when every part of the end to end test is mocked, but it can be run in a mode that partially interacts with the outside world. 35 | 36 | An example of this would be a hermetic end to end test that calls a mocked Paypal REST API using wiremock or mitmprox, but which can be run in a mode that uses the sandboxed Paypal REST API instead. 37 | 38 | Partial hermeticism can be used as: 39 | 40 | * An efficient way of creating mocks (by recording the actual request / response). 41 | * A way of consistently testing "outside world" changes - e.g. if an API the app calls still works the same way tomorrow as it did yesterday. 42 | 43 | 44 | ## Further reading 45 | 46 | * [Google Testing Blog](https://testing.googleblog.com/2012/10/hermetic-servers.html) 47 | * [My Recent Obsession with Hermetic Tests](https://blog.testproject.io/2021/09/13/my-recent-obsession-with-hermetic-tests/) 48 | * [Hermetic Unit Testing](https://medium.com/geekculture/hermetic-unit-testing-8c49276e3acd) 49 | * [TestContainers](https://www.testcontainers.org/) 50 | -------------------------------------------------------------------------------- /examples/website/app/todos/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render, get_object_or_404, redirect 2 | from django.views import generic 3 | from .models import Todo 4 | from django.http import HttpResponseRedirect 5 | from textblob import TextBlob 6 | from django.contrib.auth.decorators import login_required 7 | from django.utils.decorators import method_decorator 8 | 9 | 10 | def correct_spelling(text): 11 | blob = TextBlob(text) 12 | corrected = str(blob.correct()) 13 | if corrected != text: 14 | return suggest_spelling(text) 15 | else: 16 | return corrected 17 | 18 | 19 | def suggest_spelling(text): 20 | blob = TextBlob(text) 21 | suggestion = str(blob.correct()) 22 | return suggestion 23 | 24 | 25 | @method_decorator(login_required, name="dispatch") 26 | class IndexView(generic.ListView): 27 | template_name = "todos/index.html" 28 | context_object_name = "todo_list" 29 | 30 | def get_queryset(self): 31 | """Return all the latest todos.""" 32 | return Todo.objects.order_by("-created_at") 33 | 34 | 35 | @login_required 36 | def add(request): 37 | title = request.POST["title"] 38 | if correct_spelling(title) != title: 39 | return render( 40 | request, 41 | "todos/index.html", 42 | {"error_message": "Did you mean '{}'?".format(correct_spelling(title))}, 43 | ) 44 | else: 45 | Todo.objects.create(title=title) 46 | return redirect("todos:index") 47 | 48 | 49 | @login_required 50 | def delete(request, todo_id): 51 | todo = get_object_or_404(Todo, pk=todo_id) 52 | todo.delete() 53 | 54 | return redirect("todos:index") 55 | 56 | 57 | @login_required 58 | def update(request, todo_id): 59 | todo = get_object_or_404(Todo, pk=todo_id) 60 | isCompleted = request.POST.get("isCompleted", False) 61 | if isCompleted == "on": 62 | isCompleted = True 63 | 64 | todo.isCompleted = isCompleted 65 | 66 | todo.save() 67 | return redirect("todos:index") 68 | -------------------------------------------------------------------------------- /examples/commandline/README.md: -------------------------------------------------------------------------------- 1 | # HitchStory Command Line Tests Example 2 | 3 | ## Run them yourself 4 | 5 | **Podman must be installed on your system first.** 6 | 7 | All other functionality is automated and can be run via one of the 8 | four run.sh scripts. 9 | 10 | To begin: 11 | 12 | ```bash 13 | $ git clone https://github.com/hitchdev/hitchstory.git 14 | $ cd hitchstory/examples/commandline 15 | $ ./run.sh make 16 | ``` 17 | 18 | `./run.sh make` downloads and builds the container and python packages the 19 | tests need to run in an isolated environment for each of the respective projects. 20 | 21 | 22 | ## Run all tests 23 | 24 | ``` 25 | $ ./run.sh pytest 26 | ``` 27 | 28 | ## Run a single test 29 | 30 | This runs "Add and retrieve todo" from `story/add-todo.story`: 31 | 32 | ``` 33 | $ ./run.sh pytest -k test_add_and_retrieve_todo 34 | ``` 35 | 36 | "correct" is a unique keyword used in the name of one of the stories. 37 | 38 | ## Run singular test in rewrite mode 39 | 40 | If you tweak the wordings in the command line app and run this, it will 41 | update the story: 42 | 43 | ``` 44 | $ STORYMODE=rewrite ./run.sh pytest -k test_add_and_retrieve_todo 45 | ``` 46 | 47 | ## Generate documentation from stories 48 | 49 | This will regenerate all of the markdown docs for the project: 50 | 51 | ``` 52 | $ ./run.sh docgen 53 | ``` 54 | 55 | ## Clean up everything 56 | 57 | Everything runs in one podman container and volume. This deletes them: 58 | 59 | ``` 60 | $ ./run.sh clean all 61 | ``` 62 | 63 | # Github Actions 64 | 65 | These integration tests are run via github actions on every push. See here: 66 | 67 | * [Github actions YAML](https://github.com/hitchdev/hitchstory/blob/master/.github/workflows/examples.yml) 68 | * [Runner](https://github.com/hitchdev/hitchstory/actions/workflows/examples.yml) 69 | 70 | 71 | # Architecture 72 | 73 | The tests in this project are run from a podman container and the command line app is run in a container run *inside* that container: 74 | 75 | ```mermaid 76 | graph TD; 77 | TestContainer-->AppContainer; 78 | ``` 79 | -------------------------------------------------------------------------------- /docs/public/approach/flaky-tests.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Flaky Tests 3 | --- 4 | 5 | Flaky tests are tests which do not pass or fail consistently. 6 | 7 | The probability of flakiness increases with the amount of code being tested. Extreme flakiness can lead to test failure habituation and, in extreme 8 | cases, test abandonment. 9 | 10 | Higher level tests suffer more from flakiness than low level tests, although 11 | this tends to be only because high level tests are testing more code. 12 | 13 | Flakiness in any test is an undesirable property. 14 | 15 | ## Detecting flakiness with HitchStory 16 | 17 | Some kinds of flakiness (e.g. race conditions) can be detected by running tests multiple times. A practical approach to doing this is documented in the [flaky story detection how to](../../using/runner/flaky-story-detection). 18 | 19 | ## Causes 20 | 21 | There are a number of common different causes of flaky tests: 22 | 23 | * Timing issues when interacting with interfaces (e.g. web pages on selenium) 24 | ** A common but very hacky fix for this is using sleeps. The "proper" way to fix this is with expected condition waits with timeouts. 25 | 26 | * Dependencies behaving in unexpected ways: 27 | ** Upgraded packages in the test environment. 28 | ** New data from a downloaded database. 29 | 30 | * Non deterministic code. 31 | ** SELECT statements without ORDER BY will often jumble the order of returned items, causing test failures 32 | 33 | * Bugs and indeterminism in testing code. 34 | 35 | Flaky tests can almost always be solved through more: 36 | 37 | * Increasing isolation - e.g. containerizing and upgrading containers consistently. 38 | * Making code behave in a more deterministic fashion - e.g. always a LIMIT to all database select code. 39 | * Accomodating indeterminism - making user stories accept the full range of potential outputs. 40 | 41 | ## Useful flakiness 42 | 43 | Mostly test flakiness is just an irritation. 44 | 45 | However, sometimes, flakiness is actually useful in that it highlights a 46 | bug that would previously have remained uncovered - e.g. a race condition. 47 | -------------------------------------------------------------------------------- /docs/src/approach/flaky-tests.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Flaky Tests 3 | --- 4 | 5 | Flaky tests are tests which do not pass or fail consistently. 6 | 7 | The probability of flakiness increases with the amount of code being tested. Extreme flakiness can lead to test failure habituation and, in extreme 8 | cases, test abandonment. 9 | 10 | Higher level tests suffer more from flakiness than low level tests, although 11 | this tends to be only because high level tests are testing more code. 12 | 13 | Flakiness in any test is an undesirable property. 14 | 15 | ## Detecting flakiness with HitchStory 16 | 17 | Some kinds of flakiness (e.g. race conditions) can be detected by running tests multiple times. A practical approach to doing this is documented in the [flaky story detection how to](../../using/runner/flaky-story-detection). 18 | 19 | ## Causes 20 | 21 | There are a number of common different causes of flaky tests: 22 | 23 | * Timing issues when interacting with interfaces (e.g. web pages on selenium) 24 | ** A common but very hacky fix for this is using sleeps. The "proper" way to fix this is with expected condition waits with timeouts. 25 | 26 | * Dependencies behaving in unexpected ways: 27 | ** Upgraded packages in the test environment. 28 | ** New data from a downloaded database. 29 | 30 | * Non deterministic code. 31 | ** SELECT statements without ORDER BY will often jumble the order of returned items, causing test failures 32 | 33 | * Bugs and indeterminism in testing code. 34 | 35 | Flaky tests can almost always be solved through more: 36 | 37 | * Increasing isolation - e.g. containerizing and upgrading containers consistently. 38 | * Making code behave in a more deterministic fashion - e.g. always a LIMIT to all database select code. 39 | * Accomodating indeterminism - making user stories accept the full range of potential outputs. 40 | 41 | ## Useful flakiness 42 | 43 | Mostly test flakiness is just an irritation. 44 | 45 | However, sometimes, flakiness is actually useful in that it highlights a 46 | bug that would previously have remained uncovered - e.g. a race condition. 47 | -------------------------------------------------------------------------------- /docs/public/using/engine/match-two-strings.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Match two strings and show diff on failure 3 | --- 4 | 5 | 6 | 7 | While you could use `assert expected == actual` to match 8 | two strings in a story step, if you use `strings_match(expected, actual)` 9 | instead then when it fails: 10 | 11 | * It will show the actual string, expected string *and a diff*. 12 | * It will raise a Failure exception and avoid polluting the error message with the full stacktrace. 13 | 14 | An example is shown below: 15 | 16 | 17 | # Code Example 18 | 19 | 20 | 21 | example.story: 22 | 23 | ```yaml 24 | Failing story: 25 | steps: 26 | - Pass because strings match 27 | - Fail because strings don't match 28 | ``` 29 | engine.py: 30 | 31 | ```python 32 | from hitchstory import BaseEngine, strings_match 33 | 34 | class Engine(BaseEngine): 35 | def pass_because_strings_match(self): 36 | strings_match("hello", "hello") 37 | 38 | def fail_because_strings_dont_match(self): 39 | strings_match("hello", "goodbye") 40 | ``` 41 | 42 | With code: 43 | 44 | ```python 45 | from hitchstory import StoryCollection 46 | from engine import Engine 47 | from pathlib import Path 48 | 49 | story_collection = StoryCollection(Path(".").glob("*.story"), Engine()) 50 | 51 | ``` 52 | 53 | 54 | 55 | 56 | 57 | 58 | ```python 59 | story_collection.one().play() 60 | ``` 61 | 62 | Will output: 63 | ``` 64 | RUNNING Failing story in /path/to/working/example.story ... FAILED in 0.1 seconds. 65 | 66 | steps: 67 | - Pass because strings match 68 | - Fail because strings don't match 69 | 70 | 71 | hitchstory.exceptions.Failure 72 | 73 | Test failed. 74 | 75 | ACTUAL: 76 | goodbye 77 | 78 | EXPECTED: 79 | hello 80 | 81 | DIFF: 82 | - hello+ goodbye 83 | ``` 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | !!! note "Executable specification" 94 | 95 | Documentation automatically generated from 96 | matching-strings.story 97 | storytests. 98 | 99 | -------------------------------------------------------------------------------- /examples/website/app/todoApp/urls.py: -------------------------------------------------------------------------------- 1 | """todoApp URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/2.2/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import path, include 18 | from django.conf.urls.static import static 19 | from django.contrib.staticfiles.urls import staticfiles_urlpatterns 20 | from . import settings, views 21 | from django.contrib.auth import views as auth_views 22 | 23 | from django.contrib.auth.forms import AuthenticationForm, UsernameField 24 | 25 | from django import forms 26 | 27 | 28 | class UserLoginForm(AuthenticationForm): 29 | def __init__(self, *args, **kwargs): 30 | super(UserLoginForm, self).__init__(*args, **kwargs) 31 | 32 | username = UsernameField( 33 | widget=forms.TextInput( 34 | attrs={"data-testid": "username"} 35 | ) 36 | ) 37 | password = forms.CharField(widget=forms.PasswordInput( 38 | attrs={"data-testid": "password"} 39 | ) 40 | ) 41 | 42 | 43 | urlpatterns = [ 44 | path('todos/', include('todos.urls')), 45 | path('admin/', admin.site.urls), 46 | path('accounts/', include("django.contrib.auth.urls")), 47 | path( 48 | 'login/', auth_views.LoginView.as_view( 49 | template_name="registration/login.html", 50 | authentication_form=UserLoginForm, 51 | ), 52 | name='login', 53 | ), 54 | path('', views.index), 55 | ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 56 | urlpatterns += staticfiles_urlpatterns() 57 | -------------------------------------------------------------------------------- /hitch/story/documentation-extra-vars-and-functions.story: -------------------------------------------------------------------------------- 1 | Generate documentation with extra variables and functions: 2 | based on: base documentation 3 | docs: documentation/extra 4 | status: experimental 5 | about: | 6 | Using extra=, you can use additional functions and variables 7 | defined outside of the template. 8 | given: 9 | files: 10 | document.yaml: | 11 | story: | 12 | # {{ name }} 13 | 14 | URL : {{ WEBSITE }}/stories/{{ slug }}.html 15 | 16 | {{ info.jiras.documentation() }} 17 | 18 | {{ about }} 19 | info: 20 | jiras: | 21 | {% for jira in jiras -%} 22 | * {{ jira_url(jira) }} 23 | {% endfor %} 24 | steps: 25 | - run: 26 | code: | 27 | extra = { 28 | "WEBSITE": "http://www.yourdocumentation.com/", 29 | "jira_url": lambda jira: f"https://yourproject.jira.com/JIRAS/{jira}", 30 | } 31 | 32 | print( 33 | jenv.from_string(Path("index.jinja2").read_text()).render( 34 | story_list=story_collection.with_documentation( 35 | Path("document.yaml").read_text(), extra=extra 36 | ).ordered_by_file() 37 | ) 38 | ) 39 | will output: |- 40 | # Login 41 | 42 | URL : http://www.yourdocumentation.com//stories/login.html 43 | 44 | * https://yourproject.jira.com/JIRAS/AZT-344 45 | * https://yourproject.jira.com/JIRAS/AZT-345 46 | 47 | 48 | Simple log in. 49 | 50 | # Log in on another url 51 | 52 | URL : http://www.yourdocumentation.com//stories/log-in-on-another-url.html 53 | 54 | * https://yourproject.jira.com/JIRAS/AZT-344 55 | * https://yourproject.jira.com/JIRAS/AZT-589 56 | 57 | 58 | Alternate log in URL. 59 | 60 | # Log in as president 61 | 62 | URL : http://www.yourdocumentation.com//stories/log-in-as-president.html 63 | 64 | * https://yourproject.jira.com/JIRAS/AZT-611 65 | 66 | 67 | For stories that involve Trump. 68 | -------------------------------------------------------------------------------- /examples/website/hitch/podman-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | # !!! WARNING !!! 4 | 5 | # ALWAYS ADD network_mode: host to ALL NEW SERVICES 6 | # ALWAYS add docker.io/ to the beginning of any images. 7 | # ALWAYS add a healthcheck 8 | # Otherwise, you can mostly use it like you would any docker-compose.yml. 9 | 10 | services: 11 | playwright: 12 | network_mode: host 13 | build: 14 | dockerfile: Dockerfile-playwright 15 | image: playwright 16 | environment: 17 | VNC: ${VNC} 18 | VNCSCREENSIZE: ${VNCSCREENSIZE} 19 | healthcheck: 20 | test: netcat -vz localhost 3605 21 | interval: 2s 22 | timeout: 3s 23 | retries: 2 24 | 25 | app: 26 | network_mode: host 27 | build: 28 | context: ../ 29 | dockerfile: Dockerfile 30 | command: ${APPCMD:-runserver} 31 | entrypoint: ${APPENTRYPOINT:-python manage.py} 32 | stop_signal: SIGINT 33 | image: app 34 | volumes: 35 | - /src/app:/app 36 | environment: 37 | - SQL_ENGINE=django.db.backends.postgresql 38 | - SQL_DATABASE=hello_django_dev 39 | - SQL_USER=hello_django 40 | - SQL_PASSWORD=hello_django 41 | - SQL_HOST=localhost 42 | - SQL_PORT=5432 43 | depends_on: 44 | db: 45 | condition: service_healthy 46 | 47 | healthcheck: 48 | test: netcat -vz localhost 8000 49 | interval: 3s 50 | timeout: 2s 51 | retries: 3 52 | 53 | db: 54 | network_mode: host 55 | 56 | # speeds up tests: https://www.endpointdev.com/blog/2012/06/speeding-up-integration-tests-postgresql/ 57 | command: -c fsync=off -c synchronous_commit=off -c full_page_writes=off 58 | image: docker.io/postgres:13.0-alpine 59 | volumes: 60 | - type: volume 61 | source: db-data 62 | target: /var/lib/postgresql/data/ 63 | environment: 64 | - POSTGRES_USER=hello_django 65 | - POSTGRES_PASSWORD=hello_django 66 | - POSTGRES_DB=hello_django_dev 67 | healthcheck: 68 | test: pg_isready -U postgres 69 | interval: 2s 70 | timeout: 4s 71 | retries: 2 72 | 73 | 74 | volumes: 75 | db-data: 76 | -------------------------------------------------------------------------------- /docs/public/using/behavior/run-single-named-story.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Running a single named story successfully 3 | --- 4 | 5 | 6 | 7 | How a story runs when it is successful - i.e. when no exception 8 | is raised during its run. 9 | 10 | 11 | # Code Example 12 | 13 | 14 | 15 | example.story: 16 | 17 | ```yaml 18 | Create files: 19 | steps: 20 | - Create file 21 | - Create file: step2.txt 22 | - Create file: 23 | file name: step3.txt 24 | content: third step 25 | ``` 26 | engine.py: 27 | 28 | ```python 29 | from hitchstory import BaseEngine 30 | 31 | 32 | class Engine(BaseEngine): 33 | def create_file(self, file_name="step1.txt", content="example"): 34 | with open(file_name, 'w') as handle: 35 | handle.write(content) 36 | 37 | def on_success(self): 38 | print("splines reticulated") 39 | 40 | with open("ranstory.txt", 'w') as handle: 41 | handle.write(self.story.name) 42 | ``` 43 | 44 | With code: 45 | 46 | ```python 47 | from hitchstory import StoryCollection 48 | from pathlib import Path 49 | from engine import Engine 50 | 51 | ``` 52 | 53 | 54 | 55 | 56 | 57 | 58 | ```python 59 | StoryCollection(Path(".").glob("*.story"), Engine()).named("Create files").play() 60 | 61 | ``` 62 | 63 | Will output: 64 | ``` 65 | RUNNING Create files in /path/to/working/example.story ... splines reticulated 66 | SUCCESS in 0.1 seconds. 67 | ``` 68 | 69 | 70 | 71 | 72 | File step1.txt should now contain: 73 | 74 | ``` 75 | example 76 | ``` 77 | 78 | File step2.txt should now contain: 79 | 80 | ``` 81 | example 82 | ``` 83 | 84 | File step3.txt should now contain: 85 | 86 | ``` 87 | third step 88 | ``` 89 | 90 | File ranstory.txt should now contain: 91 | 92 | ``` 93 | Create files 94 | ``` 95 | 96 | 97 | 98 | 99 | 100 | 101 | !!! note "Executable specification" 102 | 103 | Documentation automatically generated from 104 | success.story 105 | storytests. 106 | 107 | -------------------------------------------------------------------------------- /examples/website/hitch/docstory.yml: -------------------------------------------------------------------------------- 1 | # These jinja2 templates translate stories 2 | # and story metadata into markdown 3 | 4 | story: | 5 | # {{ name }} 6 | 7 | {{ about }} 8 | 9 | {% if "jiras" in info %}* Fake related JIRAS: {{ info["jiras"].documentation() }}{% endif %} 10 | 11 | ## Video 12 | 13 | 17 | 18 | ## Steps 19 | 20 | {% for step in steps %} 21 | {{ step.documentation() }} 22 | {% endfor %} 23 | 24 | {% if "context" in info %} 25 | {{ info["context"].documentation() }} 26 | {% endif %} 27 | 28 | ## Autogenerated 29 | 30 | This markdown page was automatically generated from [this story](https://github.com/hitchdev/hitchstory/blob/master/examples/website/story/{{ filename.name }}) [with this template](https://github.com/hitchdev/hitchstory/blob/master/examples/website/tests/docstory.yml). 31 | 32 | The screenshots and video recordings were autogenerated via playwright playing the story with this [story engine](https://github.com/hitchdev/hitchstory/blob/master/examples/website/tests/test_integration.py). 33 | info: 34 | context: | 35 | ## Background context 36 | 37 | {{ context }} 38 | jiras: | 39 | {% for jira in jiras -%} 40 | [{{ jira }}](https://myproject.jira.com/{{ jira }}) 41 | {%- if not loop.last %}, {% endif %} 42 | {%- endfor %} 43 | steps: 44 | load website: | 45 | * When the website is loaded 46 | 47 | 51 | 52 | enter: | 53 | * Enter text `{{ text }}` on `{{ on }}`. 54 | 55 | click: | 56 | * Click on `{{ on }}`. 57 | 58 | should appear: | 59 | {% if which is not none -%} 60 | * Then text `{{ text }}` should appear on {{ ordinal(which) }} `{{ on }}`. 61 | {% else %} 62 | * Then text `{{ text }}` should appear on `{{ on }}`. 63 | {% endif %} 64 | 65 | 69 | -------------------------------------------------------------------------------- /examples/restapi/app/api.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, jsonify, send_from_directory 2 | from textblob import TextBlob 3 | from datetime import datetime 4 | import time 5 | import json 6 | import uuid 7 | 8 | app = Flask(__name__) 9 | 10 | 11 | def load_data(): 12 | try: 13 | with open("data.json", "r") as f: 14 | data = json.load(f) 15 | except FileNotFoundError: 16 | data = [] 17 | return data 18 | 19 | 20 | def save_data(data): 21 | with open("data.json", "w") as f: 22 | json.dump(data, f) 23 | 24 | 25 | def correct_spelling(text): 26 | blob = TextBlob(text) 27 | corrected = str(blob.correct()) 28 | if corrected != text: 29 | return suggest_spelling(text) 30 | else: 31 | return corrected 32 | 33 | 34 | def suggest_spelling(text): 35 | blob = TextBlob(text) 36 | suggestion = str(blob.correct()) 37 | return suggestion 38 | 39 | 40 | @app.route("/todo", methods=["GET"]) 41 | def get_todo(): 42 | data = load_data() 43 | return jsonify(data) 44 | 45 | 46 | @app.route("/todo", methods=["POST"]) 47 | def add_todo(): 48 | data = load_data() 49 | item = request.json["item"] 50 | corrected_item = correct_spelling(item) 51 | if corrected_item != item: 52 | return jsonify({"message": corrected_item}), 400 53 | data.append(item) 54 | new_id = uuid.uuid4() 55 | save_data(data) 56 | return ( 57 | jsonify( 58 | { 59 | "message": "Item added successfully", 60 | "data": { 61 | "id": new_id, 62 | "timestamp": int(datetime.now().timestamp()), 63 | }, 64 | } 65 | ), 66 | 201, 67 | ) 68 | 69 | 70 | @app.route("/todo/", methods=["DELETE"]) 71 | def delete_todo(index): 72 | data = load_data() 73 | try: 74 | data.pop(index - 1) 75 | save_data(data) 76 | return jsonify({"message": "Item removed successfully"}), 200 77 | except IndexError: 78 | return jsonify({"message": "Item not found"}), 404 79 | 80 | 81 | if __name__ == "__main__": 82 | app.run(debug=True) 83 | -------------------------------------------------------------------------------- /hitch/story/matching-json.story: -------------------------------------------------------------------------------- 1 | Match two JSON snippets: 2 | docs: engine/match-json 3 | based on: handling failing tests 4 | about: | 5 | While you could use `assert json.loads(expected) == json.loads(actual)` to match 6 | two JSON in a story step, if you use `json_match(expected, actual)` 7 | instead then when it fails: 8 | 9 | * It will warn you if it failed because it wasn't valid JSON. 10 | * It will show cleanly formatted actual JSON, expected JSON and a diff. 11 | * It will raise a Failure exception and avoid polluting the error message with the full stacktrace. 12 | 13 | given: 14 | files: 15 | example.story: | 16 | Failing story: 17 | steps: 18 | - Pass because json matches 19 | - Fail because strings don't match 20 | engine.py: | 21 | from hitchstory import BaseEngine, json_match 22 | 23 | class Engine(BaseEngine): 24 | def pass_because_json_matches(self): 25 | json_match( 26 | """{"a": 1, "b": 2}""", 27 | """{"b": 2, "a": 1}""" 28 | ) 29 | 30 | def fail_because_strings_dont_match(self): 31 | json_match( 32 | """{"a": 1, "b": 2}""", 33 | """{"b": 2, "a": 1, "d": 3}""" 34 | ) 35 | 36 | steps: 37 | - Run: 38 | code: story_collection.one().play() 39 | will output: |- 40 | RUNNING Failing story in /path/to/working/example.story ... FAILED in 0.1 seconds. 41 | 42 | steps: 43 | - Pass because json matches 44 | - Fail because strings don't match 45 | 46 | 47 | hitchstory.exceptions.Failure 48 | 49 | Test failed. 50 | 51 | ACTUAL: 52 | { 53 | "a": 1, 54 | "b": 2, 55 | "d": 3 56 | } 57 | 58 | EXPECTED: 59 | { 60 | "a": 1, 61 | "b": 2 62 | } 63 | 64 | DIFF: 65 | { 66 | "a": 1, 67 | - "b": 2 68 | + "b": 2, 69 | ? + 70 | + "d": 3 71 | } 72 | -------------------------------------------------------------------------------- /hitch/story/gradual-typing.story: -------------------------------------------------------------------------------- 1 | Gradual typing of story steps: 2 | docs: engine/gradual-typing 3 | about: | 4 | In order to speed up prototyping and development 5 | of a story suite, the structure of your YAML data 6 | specified in preconditions, parameters and step 7 | arguments need not be strictly defined in advance. 8 | 9 | All story data that is parsed without a validator 10 | is parsed either as a dict, list or string. 11 | 12 | It is nonetheless still recommended that you 13 | apply validators as soon as possible. 14 | [See more about that here](../strong-typing). 15 | given: 16 | files: 17 | example.story: | 18 | Create files: 19 | given: 20 | files created: 21 | preconditionfile.txt: 22 | some text 23 | steps: 24 | - Create file: 25 | details: 26 | file name: step1.txt 27 | content: some other text 28 | engine.py: | 29 | from hitchstory import BaseEngine, GivenDefinition, GivenProperty 30 | 31 | 32 | class Engine(BaseEngine): 33 | given_definition = GivenDefinition( 34 | files_created=GivenProperty(), 35 | ) 36 | 37 | def set_up(self): 38 | for filename, contents in self.given['files_created'].items(): 39 | with open(filename, 'w') as handle: 40 | handle.write(contents) 41 | 42 | def create_file(self, details): 43 | with open(details['file name'], 'w') as handle: 44 | handle.write(details['content']) 45 | setup: | 46 | from hitchstory import StoryCollection 47 | from pathlib import Path 48 | from engine import Engine 49 | steps: 50 | - Run: 51 | code: | 52 | StoryCollection(Path(".").glob("*.story"), Engine()).named("Create files").play() 53 | will output: RUNNING Create files in /path/to/working/example.story ... SUCCESS 54 | in 0.1 seconds. 55 | - File was created with: 56 | filename: preconditionfile.txt 57 | contents: some text 58 | - File was created with: 59 | filename: step1.txt 60 | contents: some other text 61 | -------------------------------------------------------------------------------- /examples/commandline/app/main.py: -------------------------------------------------------------------------------- 1 | import json 2 | from textblob import TextBlob 3 | 4 | 5 | def load_data(): 6 | try: 7 | with open("data.json", "r") as f: 8 | data = json.load(f) 9 | except FileNotFoundError: 10 | data = [] 11 | return data 12 | 13 | 14 | def save_data(data): 15 | with open("data.json", "w") as f: 16 | json.dump(data, f) 17 | 18 | 19 | def add_item(): 20 | item = input("Enter a to-do item: ") 21 | corrected_item = correct_spelling(item) 22 | if corrected_item != item: 23 | print(f'Did you mean "{corrected_item}"?') 24 | choice = input("Enter Y to confirm, or any other key to re-enter: ") 25 | if choice.lower() == "y": 26 | item = corrected_item 27 | data.append(item) 28 | save_data(data) 29 | 30 | 31 | def remove_item(): 32 | print("Current to-do list:") 33 | for i, item in enumerate(data): 34 | print(f"{i + 1}. {item}") 35 | try: 36 | index = int(input("Enter the number of the item to remove: ")) 37 | data.pop(index - 1) 38 | save_data(data) 39 | except (ValueError, IndexError): 40 | print("Invalid input. Please enter a valid number.") 41 | 42 | 43 | def correct_spelling(text): 44 | blob = TextBlob(text) 45 | corrected = str(blob.correct()) 46 | if corrected != text: 47 | return suggest_spelling(text) 48 | else: 49 | return corrected 50 | 51 | 52 | def suggest_spelling(text): 53 | blob = TextBlob(text) 54 | suggestion = str(blob.correct()) 55 | return suggestion 56 | 57 | 58 | data = load_data() 59 | 60 | while True: 61 | print("To-do list:") 62 | for i, item in enumerate(data): 63 | print(f"{i + 1}. {item}") 64 | 65 | print("Options:") 66 | print("1. Add item") 67 | print("2. Remove item") 68 | print("3. Quit") 69 | 70 | try: 71 | choice = int(input("Enter your choice: ")) 72 | except ValueError: 73 | print("Invalid input. Please enter a valid number.") 74 | continue 75 | 76 | if choice == 1: 77 | add_item() 78 | elif choice == 2: 79 | remove_item() 80 | elif choice == 3: 81 | break 82 | else: 83 | print("Invalid input. Please enter a valid number.") 84 | -------------------------------------------------------------------------------- /docs/public/using/runner/shortcut-lookup.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Shortcut lookup for story names 3 | --- 4 | 5 | 6 | 7 | Hunting for and specifying particular story to run can be a pain. 8 | 9 | Using the 'shortcut' function you can select a specific story 10 | to run just by specifying one or more key words that appear in 11 | the story title. The case is ignored, as are special characters. 12 | 13 | If you specify key words that match no stories or more than one 14 | story, an error is raised. 15 | 16 | 17 | # Code Example 18 | 19 | 20 | 21 | example1.story: 22 | 23 | ```yaml 24 | Create file: 25 | steps: 26 | - Create file 27 | Create file again: 28 | steps: 29 | - Create file 30 | ``` 31 | example2.story: 32 | 33 | ```yaml 34 | Create files: 35 | steps: 36 | - Create file 37 | ``` 38 | 39 | With code: 40 | 41 | ```python 42 | from hitchstory import StoryCollection, BaseEngine 43 | from ensure import Ensure 44 | from pathlib import Path 45 | 46 | class Engine(BaseEngine): 47 | def create_file(self, filename="step1.txt", content="example"): 48 | with open(filename, 'w') as handle: 49 | handle.write(content) 50 | 51 | story_collection = StoryCollection(Path(".").glob("*.story"), Engine()) 52 | 53 | ``` 54 | 55 | 56 | 57 | 58 | ## Story found and run 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | ```python 67 | story_collection.shortcut("file", "again").play() 68 | 69 | ``` 70 | 71 | Will output: 72 | ``` 73 | RUNNING Create file again in /path/to/working/example1.story ... SUCCESS in 0.1 seconds. 74 | ``` 75 | 76 | 77 | 78 | 79 | 80 | ## Story not found 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | ```python 89 | story_collection.shortcut("toast").play() 90 | ``` 91 | 92 | 93 | 94 | 95 | ## More than one story found 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | ```python 104 | story_collection.shortcut("file").play() 105 | ``` 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | !!! note "Executable specification" 116 | 117 | Documentation automatically generated from 118 | shortcut.story 119 | storytests. 120 | 121 | -------------------------------------------------------------------------------- /docs/src/why/interesting-to-the-business.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Why does hitchstory not have an opinion on what counts as interesting to "the business"? 3 | --- 4 | 5 | When Cucumber first gained some popularity, there was something of a backlash 6 | from the original developers that the users "[weren't using it correctly](https://cucumber.io/blog/collaboration/the-worlds-most-misunderstood-collaboration-tool/)": 7 | 8 | The specific complaint was that it was being used as an integration testing 9 | framework rather than a tool for "communicating with the business" and that everything 10 | except the high level "business requirements" should be abstracted away. 11 | 12 | Hitchstory takes a different approach and recommends using it as an integration testing framework *primarily* and use [documentation generation tools](../../using/documentation/generate) to produce documentation from that, where necessary, that is of an appropriate level of detail for stakeholders. 13 | 14 | This is because: 15 | 16 | - "The business", from a BDD perspective, is a hypothetical entity which does not really exist. There are different kinds of stakeholders - lots of different people who have varying levels of interest in the different parts of the specification. 17 | 18 | - Stakeholders rarely have any interest in implementation details and only have an interest in some aspects of the specification. 19 | 20 | 3. Stakeholder interest in the details of your software's behavior will vary wildly. Sometimes they will want only vague details, other times they will be interested in obscure business logic, and other times they will be interested in specific user UI actions. 21 | 22 | 4. Interest varies from stakeholder to stakeholder - UX designers, UI designers, translators, product managers and the CEO will all have varying levels of interest in the specifics of the behavior of your software. 23 | 24 | 5. The language that is most appropriate for specifying code is not necessarily going to be English or English-like. 25 | 26 | Nonetheless, despite all of this, the Cucumber people were on to something - there is a tight relationship between documentation and specification. 27 | 28 | HitchStory takes the view that all behavior should be specified by a user story, along with any appropriate metadata and that documentation should be generated. 29 | -------------------------------------------------------------------------------- /docs/public/why/interesting-to-the-business.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Why does hitchstory not have an opinion on what counts as interesting to "the business"? 3 | --- 4 | 5 | When Cucumber first gained some popularity, there was something of a backlash 6 | from the original developers that the users "[weren't using it correctly](https://cucumber.io/blog/collaboration/the-worlds-most-misunderstood-collaboration-tool/)": 7 | 8 | The specific complaint was that it was being used as an integration testing 9 | framework rather than a tool for "communicating with the business" and that everything 10 | except the high level "business requirements" should be abstracted away. 11 | 12 | Hitchstory takes a different approach and recommends using it as an integration testing framework *primarily* and use [documentation generation tools](../../using/documentation/generate) to produce documentation from that, where necessary, that is of an appropriate level of detail for stakeholders. 13 | 14 | This is because: 15 | 16 | - "The business", from a BDD perspective, is a hypothetical entity which does not really exist. There are different kinds of stakeholders - lots of different people who have varying levels of interest in the different parts of the specification. 17 | 18 | - Stakeholders rarely have any interest in implementation details and only have an interest in some aspects of the specification. 19 | 20 | 3. Stakeholder interest in the details of your software's behavior will vary wildly. Sometimes they will want only vague details, other times they will be interested in obscure business logic, and other times they will be interested in specific user UI actions. 21 | 22 | 4. Interest varies from stakeholder to stakeholder - UX designers, UI designers, translators, product managers and the CEO will all have varying levels of interest in the specifics of the behavior of your software. 23 | 24 | 5. The language that is most appropriate for specifying code is not necessarily going to be English or English-like. 25 | 26 | Nonetheless, despite all of this, the Cucumber people were on to something - there is a tight relationship between documentation and specification. 27 | 28 | HitchStory takes the view that all behavior should be specified by a user story, along with any appropriate metadata and that documentation should be generated. 29 | -------------------------------------------------------------------------------- /hitchstory/story_list.py: -------------------------------------------------------------------------------- 1 | from hitchstory.result import ResultList 2 | from hitchstory.story import Story 3 | from hitchstory import exceptions 4 | from types import ModuleType 5 | from copy import copy 6 | 7 | 8 | class StoryList(object): 9 | """ 10 | A sequence of stories ready to be played in order. 11 | """ 12 | 13 | def __init__(self, stories): 14 | for story in stories: 15 | assert type(story) is Story 16 | self._stories = stories 17 | self._continue_on_failure = False 18 | 19 | def continue_on_failure(self): 20 | new_story_list = copy(self) 21 | new_story_list._continue_on_failure = True 22 | return new_story_list 23 | 24 | def play(self): 25 | results = ResultList() 26 | if len(self._stories) > 0: 27 | for story in self._stories: 28 | result = story.play() 29 | results.append(result) 30 | 31 | if hasattr(story.engine, "_aborted") and story.engine._aborted: 32 | break 33 | 34 | if not result.passed and not self._continue_on_failure: 35 | break 36 | else: 37 | print("No stories found") 38 | return results 39 | 40 | def add_pytests_to(self, module): 41 | if not isinstance(module, ModuleType): 42 | raise exceptions.HitchStoryException( 43 | "add_pytests_to must be used with a module." 44 | ) 45 | 46 | if len(self._stories) > 0: 47 | for story in self._stories: 48 | pytest_name = "test_{}".format(story.slug.replace("-", "_")) 49 | 50 | def hitchstory(story=story): 51 | story.play() 52 | 53 | setattr( 54 | module, 55 | pytest_name, 56 | hitchstory, 57 | ) 58 | else: 59 | raise exceptions.HitchStoryException( 60 | ( 61 | "The StoryList you are trying to turn into pytest tests " 62 | "has zero stories in it." 63 | ) 64 | ) 65 | 66 | def __len__(self): 67 | return len(self._stories) 68 | 69 | def __getitem__(self, index): 70 | return self._stories[index] 71 | -------------------------------------------------------------------------------- /hitch/story/shortcut.story: -------------------------------------------------------------------------------- 1 | Shortcut lookup for story names: 2 | docs: runner/shortcut-lookup 3 | about: | 4 | Hunting for and specifying particular story to run can be a pain. 5 | 6 | Using the 'shortcut' function you can select a specific story 7 | to run just by specifying one or more key words that appear in 8 | the story title. The case is ignored, as are special characters. 9 | 10 | If you specify key words that match no stories or more than one 11 | story, an error is raised. 12 | given: 13 | files: 14 | example1.story: | 15 | Create file: 16 | steps: 17 | - Create file 18 | Create file again: 19 | steps: 20 | - Create file 21 | example2.story: | 22 | Create files: 23 | steps: 24 | - Create file 25 | setup: | 26 | from hitchstory import StoryCollection, BaseEngine 27 | from ensure import Ensure 28 | from pathlib import Path 29 | 30 | class Engine(BaseEngine): 31 | def create_file(self, filename="step1.txt", content="example"): 32 | with open(filename, 'w') as handle: 33 | handle.write(content) 34 | 35 | story_collection = StoryCollection(Path(".").glob("*.story"), Engine()) 36 | variations: 37 | Story found and run: 38 | steps: 39 | - Run: 40 | code: | 41 | story_collection.shortcut("file", "again").play() 42 | will output: |- 43 | RUNNING Create file again in /path/to/working/example1.story ... SUCCESS in 0.1 seconds. 44 | 45 | 46 | Story not found: 47 | steps: 48 | - Run: 49 | code: story_collection.shortcut("toast").play() 50 | raises: 51 | type: hitchstory.exceptions.StoryNotFound 52 | message: Story 'toast' not found. 53 | 54 | More than one story found: 55 | steps: 56 | - Run: 57 | code: story_collection.shortcut("file").play() 58 | raises: 59 | type: hitchstory.exceptions.MoreThanOneStory 60 | message: |- 61 | More than one matching story: 62 | Create file (in /path/to/working/example1.story) 63 | Create file again (in /path/to/working/example1.story) 64 | Create files (in /path/to/working/example2.story) 65 | -------------------------------------------------------------------------------- /examples/commandline/tests/test_integration.py: -------------------------------------------------------------------------------- 1 | from os import getenv 2 | from pathlib import Path 3 | 4 | from commandlib import Command 5 | from hitchstory import ( 6 | BaseEngine, 7 | Failure, 8 | GivenDefinition, 9 | GivenProperty, 10 | InfoDefinition, 11 | InfoProperty, 12 | StoryCollection, 13 | no_stacktrace_for, 14 | strings_match, 15 | ) 16 | from icommandlib import ICommand, IProcessTimeout 17 | from strictyaml import Str 18 | 19 | PROJECT_DIRECTORY = Path(__file__).absolute().parents[0].parent 20 | 21 | 22 | class Engine(BaseEngine): 23 | """Python engine for running tests.""" 24 | 25 | def __init__(self, rewrite=False): 26 | self._cmd = Command( 27 | "podman", "run", "-it", "-v", "/src/app:/app", "app" 28 | ).in_dir(PROJECT_DIRECTORY) 29 | self._rewrite = rewrite 30 | 31 | def set_up(self): 32 | self._iprocess = ICommand(self._cmd).run() 33 | 34 | def expect(self, text): 35 | self._iprocess.wait_until_output_contains(text) 36 | 37 | def display(self, expected_text): 38 | try: 39 | self._iprocess.wait_for_stripshot_to_match( 40 | expected_text, 41 | timeout=2, 42 | ) 43 | except IProcessTimeout as error: 44 | if self._rewrite: 45 | self.current_step.rewrite("expected_text").to(error.stripshot) 46 | else: 47 | strings_match(expected_text, error.stripshot) 48 | 49 | def enter_text(self, text): 50 | self._iprocess.send_keys(f"{text}\n") 51 | 52 | def exit_successfully(self): 53 | self._iprocess.wait_for_successful_exit() 54 | 55 | def pause(self): 56 | import IPython 57 | 58 | IPython.embed() 59 | 60 | def tear_down(self): 61 | Command("podman", "stop", "app", "--time", "1", "-i").run() 62 | 63 | def on_failure(self, result): 64 | pass 65 | 66 | def on_success(self): 67 | if self._rewrite: 68 | self.new_story.save() 69 | 70 | 71 | collection = StoryCollection( 72 | Path(__file__).parent.parent.joinpath("story").glob("*.story"), 73 | Engine(rewrite=getenv("STORYMODE", "") == "rewrite"), 74 | ) 75 | 76 | collection.with_external_test_runner().ordered_by_name().add_pytests_to( 77 | module=__import__(__name__) # This module 78 | ) 79 | -------------------------------------------------------------------------------- /docs/public/why/given-when-then.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Why does hitchstory mandate the use of given but not when and then? 3 | --- 4 | 5 | [Given-When-Then](https://en.wikipedia.org/wiki/Given-When-Then) is a structured 6 | way of writing test cases or executable specs. It was invented by Dan North as 7 | part of behavior driven development. 8 | 9 | ## Hoare logic 10 | 11 | This pattern is a structure that essentially all test cases should be written, 12 | separating the preconditions from the actions from the observable outcome. It 13 | follows from [Hoare logic](https://en.wikipedia.org/wiki/Given-When-Then) 14 | - a means of reasoning rigorously about the correctness of a computer program. 15 | 16 | Many BDD frameworks have *explicit* keywords for given, when and then. These 17 | keywords are intended to describe the structure of the story in a 'readable' 18 | way. 19 | 20 | ## But what does given, when and then actually do? 21 | 22 | Crucially, though, keywords *don't actually do anything*. In Cucumber, for example, 23 | there is no meaningful difference to putting "when" and "then" - they are 24 | essentially null operators. 25 | 26 | I view them as something akin to test case writing "training wheels" - they are 27 | useful for beginner testers to keep the tester/specifier on track, ensuring 28 | that the test cases created are meaningful and actually test. 29 | 30 | However, while training wheels are useful for beginners, they become cumbersome 31 | and get in the way once you no longer need them. 32 | 33 | ## Terseness and clarity 34 | 35 | Hitchstory recognizes that the pattern describes the structure of how *should* be 36 | written but, other than given, it neither requires nor encourages that the 37 | actual keywords be used. 38 | 39 | Terseness is a key principle of hitchstory - the idea that stories should be 40 | as short and non-repetitive as possible, provided it doesn't inhibit readability. 41 | 42 | As an example, in this case, "click" is the "when" step and "goes kaboom" is the 43 | then step. The story is still clear without them: 44 | 45 | ```yaml 46 | given: 47 | box: red 48 | steps: 49 | - click: red button 50 | - goes kaboom 51 | ``` 52 | 53 | ## Still optional 54 | 55 | Nonetheless, if you choose, you can create steps that start with when and then: 56 | 57 | ```yaml 58 | Given: 59 | I have: red box 60 | Steps: 61 | - When I click: the red button 62 | - Then it goes kaboom 63 | ``` 64 | -------------------------------------------------------------------------------- /docs/src/why/given-when-then.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Why does hitchstory mandate the use of given but not when and then? 3 | --- 4 | 5 | [Given-When-Then](https://en.wikipedia.org/wiki/Given-When-Then) is a structured 6 | way of writing test cases or executable specs. It was invented by Dan North as 7 | part of behavior driven development. 8 | 9 | ## Hoare logic 10 | 11 | This pattern is a structure that essentially all test cases should be written, 12 | separating the preconditions from the actions from the observable outcome. It 13 | follows from [Hoare logic](https://en.wikipedia.org/wiki/Given-When-Then) 14 | - a means of reasoning rigorously about the correctness of a computer program. 15 | 16 | Many BDD frameworks have *explicit* keywords for given, when and then. These 17 | keywords are intended to describe the structure of the story in a 'readable' 18 | way. 19 | 20 | ## But what does given, when and then actually do? 21 | 22 | Crucially, though, keywords *don't actually do anything*. In Cucumber, for example, 23 | there is no meaningful difference to putting "when" and "then" - they are 24 | essentially null operators. 25 | 26 | I view them as something akin to test case writing "training wheels" - they are 27 | useful for beginner testers to keep the tester/specifier on track, ensuring 28 | that the test cases created are meaningful and actually test. 29 | 30 | However, while training wheels are useful for beginners, they become cumbersome 31 | and get in the way once you no longer need them. 32 | 33 | ## Terseness and clarity 34 | 35 | Hitchstory recognizes that the pattern describes the structure of how *should* be 36 | written but, other than given, it neither requires nor encourages that the 37 | actual keywords be used. 38 | 39 | Terseness is a key principle of hitchstory - the idea that stories should be 40 | as short and non-repetitive as possible, provided it doesn't inhibit readability. 41 | 42 | As an example, in this case, "click" is the "when" step and "goes kaboom" is the 43 | then step. The story is still clear without them: 44 | 45 | ```yaml 46 | given: 47 | box: red 48 | steps: 49 | - click: red button 50 | - goes kaboom 51 | ``` 52 | 53 | ## Still optional 54 | 55 | Nonetheless, if you choose, you can create steps that start with when and then: 56 | 57 | ```yaml 58 | Given: 59 | I have: red box 60 | Steps: 61 | - When I click: the red button 62 | - Then it goes kaboom 63 | ``` 64 | -------------------------------------------------------------------------------- /examples/website/app/todos/templates/todos/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'todos/base.html' %} 2 | 3 | {% block title %} 4 | Todo list 5 | {% endblock %} 6 | 7 | {% block content %} 8 |
9 | 10 | 11 |
12 |
13 | 18 |
19 |
20 | 21 | 22 |
23 |
24 |
25 | {% csrf_token %} 26 |
27 |
28 | 29 |
30 |
31 | 34 |
35 | {% if error_message %} 36 | 39 | {% endif %} 40 |
41 |
42 |
43 |
44 |
45 | 46 | 47 |
48 |
49 |
50 | {% for todo in todo_list %} 51 |
52 |
53 | {% csrf_token %} 54 | 57 |
58 | {{ todo.title }} 59 | 60 | 61 | 62 |
63 | {% endfor %} 64 |
65 |
66 |
67 |
68 | 69 | {% endblock %} 70 | -------------------------------------------------------------------------------- /hitch/story/rewrite-subkey-of-argument.story: -------------------------------------------------------------------------------- 1 | Story that rewrites the sub key of an argument: 2 | docs: engine/rewrite-subkey-of-argument 3 | about: | 4 | This shows how to build a story that rewrites a sub-key 5 | of an argument. 6 | 7 | ``` 8 | self.current_step.rewrite("response", "content").to("new output") 9 | ``` 10 | 11 | given: 12 | files: 13 | example.story: | 14 | REST API: 15 | steps: 16 | - API call: 17 | request: 18 | path: /hello 19 | response: 20 | status code: 200 21 | content: | 22 | {"old": "response"} 23 | engine.py: | 24 | from hitchstory import BaseEngine 25 | 26 | class Engine(BaseEngine): 27 | def __init__(self, rewrite=True): 28 | self._rewrite = rewrite 29 | 30 | def run(self, command): 31 | pass 32 | 33 | def api_call(self, request, response): 34 | if self._rewrite: 35 | self.current_step.rewrite( 36 | "response", "content" 37 | ).to("""{"new": "output"}""") 38 | 39 | setup: | 40 | from hitchstory import StoryCollection 41 | from pathlib import Path 42 | from engine import Engine 43 | variations: 44 | Story is rewritten when rewrite=True is used: 45 | steps: 46 | - Run: 47 | code: | 48 | StoryCollection(Path(".").glob("*.story"), Engine(rewrite=True)).ordered_by_name().play() 49 | will output: |- 50 | RUNNING REST API in /path/to/working/example.story ... SUCCESS in 0.1 seconds. 51 | 52 | - File contents will be: 53 | filename: example.story 54 | contents: |- 55 | REST API: 56 | steps: 57 | - API call: 58 | request: 59 | path: /hello 60 | response: 61 | status code: 200 62 | content: |- 63 | {"new": "output"} 64 | 65 | Story remains unchanged when rewrite=False is used instead: 66 | steps: 67 | - Run: 68 | code: | 69 | StoryCollection(Path(".").glob("*.story"), Engine(rewrite=False)).ordered_by_name().play() 70 | will output: |- 71 | RUNNING REST API in /path/to/working/example.story ... SUCCESS in 0.1 seconds. 72 | - Example story unchanged 73 | -------------------------------------------------------------------------------- /docs/public/using/engine/match-json.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Match two JSON snippets 3 | --- 4 | 5 | 6 | 7 | While you could use `assert json.loads(expected) == json.loads(actual)` to match 8 | two JSON in a story step, if you use `json_match(expected, actual)` 9 | instead then when it fails: 10 | 11 | * It will warn you if it failed because it wasn't valid JSON. 12 | * It will show cleanly formatted actual JSON, expected JSON and a diff. 13 | * It will raise a Failure exception and avoid polluting the error message with the full stacktrace. 14 | 15 | 16 | # Code Example 17 | 18 | 19 | 20 | example.story: 21 | 22 | ```yaml 23 | Failing story: 24 | steps: 25 | - Pass because json matches 26 | - Fail because strings don't match 27 | ``` 28 | engine.py: 29 | 30 | ```python 31 | from hitchstory import BaseEngine, json_match 32 | 33 | class Engine(BaseEngine): 34 | def pass_because_json_matches(self): 35 | json_match( 36 | """{"a": 1, "b": 2}""", 37 | """{"b": 2, "a": 1}""" 38 | ) 39 | 40 | def fail_because_strings_dont_match(self): 41 | json_match( 42 | """{"a": 1, "b": 2}""", 43 | """{"b": 2, "a": 1, "d": 3}""" 44 | ) 45 | ``` 46 | 47 | With code: 48 | 49 | ```python 50 | from hitchstory import StoryCollection 51 | from engine import Engine 52 | from pathlib import Path 53 | 54 | story_collection = StoryCollection(Path(".").glob("*.story"), Engine()) 55 | 56 | ``` 57 | 58 | 59 | 60 | 61 | 62 | 63 | ```python 64 | story_collection.one().play() 65 | ``` 66 | 67 | Will output: 68 | ``` 69 | RUNNING Failing story in /path/to/working/example.story ... FAILED in 0.1 seconds. 70 | 71 | steps: 72 | - Pass because json matches 73 | - Fail because strings don't match 74 | 75 | 76 | hitchstory.exceptions.Failure 77 | 78 | Test failed. 79 | 80 | ACTUAL: 81 | { 82 | "a": 1, 83 | "b": 2, 84 | "d": 3 85 | } 86 | 87 | EXPECTED: 88 | { 89 | "a": 1, 90 | "b": 2 91 | } 92 | 93 | DIFF: 94 | { 95 | "a": 1, 96 | - "b": 2 97 | + "b": 2, 98 | ? + 99 | + "d": 3 100 | } 101 | ``` 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | !!! note "Executable specification" 112 | 113 | Documentation automatically generated from 114 | matching-json.story 115 | storytests. 116 | 117 | -------------------------------------------------------------------------------- /docs/public/using/engine/gradual-typing.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Gradual typing of story steps 3 | --- 4 | 5 | 6 | 7 | In order to speed up prototyping and development 8 | of a story suite, the structure of your YAML data 9 | specified in preconditions, parameters and step 10 | arguments need not be strictly defined in advance. 11 | 12 | All story data that is parsed without a validator 13 | is parsed either as a dict, list or string. 14 | 15 | It is nonetheless still recommended that you 16 | apply validators as soon as possible. 17 | [See more about that here](../strong-typing). 18 | 19 | 20 | # Code Example 21 | 22 | 23 | 24 | example.story: 25 | 26 | ```yaml 27 | Create files: 28 | given: 29 | files created: 30 | preconditionfile.txt: 31 | some text 32 | steps: 33 | - Create file: 34 | details: 35 | file name: step1.txt 36 | content: some other text 37 | ``` 38 | engine.py: 39 | 40 | ```python 41 | from hitchstory import BaseEngine, GivenDefinition, GivenProperty 42 | 43 | 44 | class Engine(BaseEngine): 45 | given_definition = GivenDefinition( 46 | files_created=GivenProperty(), 47 | ) 48 | 49 | def set_up(self): 50 | for filename, contents in self.given['files_created'].items(): 51 | with open(filename, 'w') as handle: 52 | handle.write(contents) 53 | 54 | def create_file(self, details): 55 | with open(details['file name'], 'w') as handle: 56 | handle.write(details['content']) 57 | ``` 58 | 59 | With code: 60 | 61 | ```python 62 | from hitchstory import StoryCollection 63 | from pathlib import Path 64 | from engine import Engine 65 | 66 | ``` 67 | 68 | 69 | 70 | 71 | 72 | 73 | ```python 74 | StoryCollection(Path(".").glob("*.story"), Engine()).named("Create files").play() 75 | 76 | ``` 77 | 78 | Will output: 79 | ``` 80 | RUNNING Create files in /path/to/working/example.story ... SUCCESS in 0.1 seconds. 81 | ``` 82 | 83 | 84 | 85 | 86 | File preconditionfile.txt should now contain: 87 | 88 | ``` 89 | some text 90 | ``` 91 | 92 | File step1.txt should now contain: 93 | 94 | ``` 95 | some other text 96 | ``` 97 | 98 | 99 | 100 | 101 | 102 | 103 | !!! note "Executable specification" 104 | 105 | Documentation automatically generated from 106 | gradual-typing.story 107 | storytests. 108 | 109 | --------------------------------------------------------------------------------