├── .github └── workflows │ ├── build.yml │ └── check.yml ├── .gitignore ├── .readthedocs.yaml ├── .typos.toml ├── Dockerfile ├── README.md ├── _theme ├── css │ ├── admonition.css │ ├── behat-docs.css │ ├── button.css │ ├── code-samples.css │ ├── colours-brand.css │ ├── colours-dark.css │ ├── colours-light.css │ ├── font.css │ ├── page-colour-scheme.css │ └── wagtail-fixes │ │ ├── admonition-spacing.css │ │ ├── dark-theme-overrides.css │ │ ├── sticky-footer.css │ │ ├── table-spacing.css │ │ └── top-whitespace.css ├── python │ ├── button.py │ └── lightningcss.py ├── static │ ├── favicon.png │ └── img │ │ └── behat-b-2x-white.png └── templates │ ├── globaltoc.html │ ├── layout.html │ ├── pager.html │ └── searchbox.html ├── borg.json ├── community.rst ├── conf.py ├── cookbooks.rst ├── cookbooks ├── accessing_contexts_from_each_other.rst ├── creating_a_context_configuration_extension.rst └── custom_formatter.rst ├── docker-compose.yml ├── docker └── entrypoint.sh ├── guides.rst ├── images ├── formatter-pretty.png └── formatter-progress.png ├── index.rst ├── quick_start.rst ├── releases.rst ├── requirements.txt ├── useful_resources.rst └── user_guide ├── annotations.rst ├── command_line_tool.rst ├── command_line_tool ├── formatting.rst ├── identifying.rst └── informative_output.rst ├── configuration.rst ├── configuration ├── printing_paths.rst ├── suites.rst └── yaml_configuration.rst ├── context.rst ├── context ├── definitions.rst ├── hooks.rst └── transformations.rst ├── features_scenarios.rst ├── gherkin.rst ├── initialize.rst ├── integrations.rst ├── organizing.rst └── writing_scenarios.rst /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build Docs 2 | 3 | on: 4 | push: 5 | branches: [v3.0] 6 | pull_request: ~ 7 | 8 | jobs: 9 | build-docs: 10 | name: Build Docs 11 | runs-on: ubuntu-latest 12 | defaults: 13 | run: 14 | shell: bash 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Run build command 20 | run: docker compose run --rm read-the-docs-builder build 21 | 22 | - name: Check build 23 | run: cat _build/html/guides.html 24 | 25 | - name: save site as artifact 26 | uses: actions/upload-artifact@v4 27 | with: 28 | name: docs 29 | path: _build/html 30 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: Check Docs 2 | 3 | on: 4 | push: 5 | branches: [v3.0] 6 | pull_request: null 7 | 8 | jobs: 9 | sphinx: 10 | name: Sphinx reStructuredText validity 11 | runs-on: ubuntu-latest 12 | defaults: 13 | run: 14 | shell: bash 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Install sphinx-lint 18 | run: | 19 | pip install --user sphinx-lint 20 | - name: Check Sphinx document sources 21 | run: | 22 | git ls-files --cached -z -- '*.rst' \ 23 | | xargs --null -- python -m sphinxlint --enable all --max-line-length 120 24 | 25 | typos: 26 | name: Typos 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@v4 30 | - name: Search for misspellings 31 | uses: crate-ci/typos@master 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _build 2 | build 3 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file for Sphinx projects 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | # Required 5 | version: 2 6 | 7 | # Set the OS, Python version and other tools you might need 8 | build: 9 | os: ubuntu-22.04 10 | tools: 11 | # Note: Update the Dockerfile if the python version changes 12 | python: "3.12" 13 | 14 | # Build documentation in the "docs/" directory with Sphinx 15 | sphinx: 16 | configuration: conf.py 17 | # Fail on all warnings to avoid broken references 18 | fail_on_warning: true 19 | 20 | # Optional but recommended, declare the Python requirements required 21 | # to build your documentation 22 | # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 23 | python: 24 | install: 25 | - requirements: requirements.txt 26 | -------------------------------------------------------------------------------- /.typos.toml: -------------------------------------------------------------------------------- 1 | [files] 2 | extend-exclude = [ 3 | ".git/", 4 | ] 5 | ignore-hidden = false 6 | 7 | [default.extend-identifiers] 8 | # Typos 9 | "Github" = "GitHub" 10 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.12 2 | 3 | WORKDIR /workspace 4 | 5 | COPY requirements.txt . 6 | RUN python -m pip install --upgrade --no-cache-dir pip setuptools 7 | RUN python -m pip install --upgrade --no-cache-dir -r requirements.txt 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Documentation & web content for docs.behat.org 2 | 3 | This repo holds the content for https://docs.behat.org/, which is built and hosted on [Read the Docs Community](https://about.readthedocs.com/) (RtD). 4 | https://behat.org and https://www.behat.org both redirect to this site. 5 | 6 | At the moment, RtD does not feed build status back to GitHub for builds on a project's main branch(es). Instead, you 7 | can find build history for each version on RtD. 8 | 9 | | Version | Status | Docs URL | Build dashboard | 10 | |---------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------|---------------------------------------------------------------------------------------------------| 11 | | v2.5 | [![Documentation Status](https://readthedocs.org/projects/behat/badge/?version=v2.5&style=for-the-badge)](https://docs.behat.org/en/latest/?badge=v2.5) | https://docs.behat.org/en/v2.5/ | [v2.5 build history](https://app.readthedocs.org/projects/behat/builds/?version__slug=v2.5) | 12 | | v3.0 | [![Documentation Status](https://readthedocs.org/projects/behat/badge/?version=v3.0&style=for-the-badge)](https://docs.behat.org/en/latest/?badge=v3.0) | https://docs.behat.org/en/v3.0/ | [v3.0 build history](https://app.readthedocs.org/projects/behat/builds/?version__slug=v3.0) | 13 | | latest* | [![Documentation Status](https://readthedocs.org/projects/behat/badge/?version=latest&style=for-the-badge)](https://docs.behat.org/en/latest/?badge=v3.0&style=for-the-badge) | https://docs.behat.org/en/latest/ | ["latest" build history](https://app.readthedocs.org/projects/behat/builds/?version__slug=latest) | 14 | 15 | > \* the "latest" version is currently also based off the v3.0 branch, but is a separate build on RTD. 16 | 17 | ## Project structure 18 | 19 | The site is built using [sphinx](https://www.sphinx-doc.org/en/master/index.html), a python-based documentation 20 | generator based on reStructuredText (.rst) or MyST Markdown (.md) files. Most content is populated in these files. 21 | 22 | reStructuredText is similar to Markdown, with some differences. The following resources may be useful: 23 | 24 | * RtD's [Getting started with RST tutorial](https://sphinx-tutorial.readthedocs.io/step-1/) 25 | * The [Spinx reStructuredText primer](https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html) 26 | * @waldyrious' [browser-based RST playground](https://waldyrious.net/rst-playground/) 27 | 28 | Sphinx takes these .rst/.md source files and renders them into the final HTML site with the custom `borg` theme. You'll 29 | find the templates and resources for this under the `_themes/borg` directory. The theme provides the overall page 30 | layout, as well as features such as automatic contents pages and navigation. 31 | 32 | ## Previewing on GitHub 33 | 34 | The GitHub web interface natively supports rendering both reStructuredText and Markdown files. For simple changes, this 35 | is often the quickest way to check the formatting of your contribution. Custom sphinx tags & metadata (including some 36 | navigation tags & internal links) will not be rendered, but the main content should appear roughly as it will in the 37 | built version. 38 | 39 | ## Building locally 40 | 41 | For more significant changes, you may want to build the full docs site locally. For this, you will need python, sphinx 42 | and the relevant dependencies. 43 | 44 | The easiest solution may be to use a temporary docker container. In this repository you will find a `Dockerfile` 45 | and a `docker-compose.yml` file that will let you do that easily. 46 | 47 | You can run a one-time build: 48 | 49 | ```bash 50 | # Launch a docker container with the right dependencies and run the site build command 51 | # This will build the container if needed, using the Dockerfile 52 | docker compose run --rm read-the-docs-builder build 53 | 54 | # The docs will be generated into _build/html 55 | # Check the CLI output for any errors 56 | ``` 57 | 58 | Or you can serve the docs locally over HTTP and rebuild them automatically as you make changes: 59 | 60 | ```bash 61 | # By default, the docs will be served on port 8000 but you can specify a custom port 62 | SPHINX_PORT=8129 docker compose up --remove-orphans 63 | 64 | # Browse to http://localhost:8129 (or your specified SPHINX_PORT) to see the built docs. 65 | # They will re-build and refresh in the browser each time you save changes. 66 | 67 | # The docs will also be generated statically into _build/html 68 | ``` 69 | 70 | 71 | If you encounter problems, start by looking at the logs of the latest build on Read the Docs to see the commands that 72 | were executed. It's possible that this README has got out of date with later changes to the build process. 73 | -------------------------------------------------------------------------------- /_theme/css/admonition.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Apply custom colour scheme to admonitions 3 | * @todo could Wagtail support this as CSS variables directly? 4 | */ 5 | 6 | .admonition { 7 | /* fallback for old browsers (they already get a border and drop shadow) */ 8 | background-color: transparent !important; 9 | /* 10 | * the panel body should be a subdued version of the title background. This can be done 11 | * by mixing with transparent, which will lighten the colour in light mode, and darken 12 | * it in dark. 13 | */ 14 | background-color: color-mix(in oklab, var(--bh-admonition-bg) 15%, transparent) !important; 15 | 16 | > .admonition-title { 17 | background-color: var(--bh-admonition-bg) !important; 18 | color: var(--bh-admonition-title-color) !important; 19 | } 20 | 21 | &.hint, 22 | &.note { 23 | --bh-admonition-bg: var(--bh-adm-hint-bg); 24 | } 25 | 26 | &.important, 27 | &.seealso, 28 | &.tip { 29 | --bh-admonition-bg: var(--bh-adm-tip-bg); 30 | } 31 | 32 | &.caution, 33 | &.warning, 34 | &.attention { 35 | --bh-admonition-bg: var(--bh-adm-warning-bg); 36 | } 37 | 38 | &.danger, 39 | &.error { 40 | --bh-admonition-bg: var(--bh-adm-danger-bg); 41 | } 42 | 43 | 44 | } 45 | -------------------------------------------------------------------------------- /_theme/css/behat-docs.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Behat custom documentation CSS 3 | * These CSS files are compiled together and minified with lightningcss 4 | * Do not add individual styles in this file, group them together 5 | */ 6 | @import "colours-brand.css"; 7 | @import "colours-light.css"; 8 | @import "colours-dark.css"; 9 | @import "page-colour-scheme.css"; 10 | @import "font.css"; 11 | 12 | @import "admonition.css"; 13 | @import "button.css"; 14 | @import "code-samples.css"; 15 | 16 | @import "wagtail-fixes/admonition-spacing.css"; 17 | @import "wagtail-fixes/sticky-footer.css"; 18 | @import "wagtail-fixes/table-spacing.css"; 19 | @import "wagtail-fixes/top-whitespace.css"; 20 | @import "wagtail-fixes/dark-theme-overrides.css"; 21 | -------------------------------------------------------------------------------- /_theme/css/button.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Styles for bootstrap buttons 3 | */ 4 | 5 | .btn { 6 | /* reset the underline that wagtail adds to all `a` elements */ 7 | text-decoration: none; 8 | } 9 | 10 | 11 | .btn-primary { 12 | background-color: var(--bh-bg-primary) !important; 13 | color: white !important; 14 | /* Fallback for older browsers */ 15 | border-color: var(--bh-bg-primary) !important; 16 | /* Use color-mix to shade the border for modern browsers */ 17 | border-color: color-mix(in srgb, var(--bh-bg-primary) 90%, #000) !important; 18 | } 19 | 20 | .btn-primary:hover { 21 | /* These will fallback to the native theme hover colours, not pretty but it works */ 22 | background-color: color-mix(in srgb, var(--bh-bg-primary) 80%, #000) !important; 23 | border-color: color-mix(in srgb, var(--bh-bg-primary) 70%, #000) !important; 24 | color: white !important; 25 | } -------------------------------------------------------------------------------- /_theme/css/code-samples.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Customise the style of code blocks 3 | */ 4 | 5 | pre, .pre { 6 | /* 7 | * The default font stack in wagtail isn't that readable on some devices. 8 | */ 9 | font-family: 'JetBrains Mono', monospace; 10 | /* Bootstrap sizes `code` inline smaller than the text surrounding it, which looks 11 | odd when we use it for things like "Behat will set up a `features` directory". 12 | */ 13 | font-size: 1rem; 14 | } 15 | 16 | 17 | .highlight { 18 | pre { 19 | background-color: var(--bh-code-bg); 20 | color: var(--bh-code-text); 21 | /* big code blocks can be very slightly smaller in font to fit wider line width */ 22 | font-size: 0.9rem; 23 | } 24 | 25 | .c, /* Comment */ 26 | .cm, /* Comment.Multiline */ 27 | .cp, /* Comment.Preproc */ 28 | .c1, /* Comment.Single */ 29 | .cs /* Comment.Special */ 30 | { 31 | color: var(--bh-code-comment); 32 | } 33 | 34 | .k, /* Keyword */ 35 | .kc, /* Keyword.Constant */ 36 | .kd, /* Keyword.Declaration */ 37 | .kp, /* Keyword.Pseudo */ 38 | .kr, /* Keyword.Reserved */ 39 | .kt, /* Keyword.Type */ 40 | .no /* Name.Constant */ 41 | { 42 | color: var(--bh-code-keyword); 43 | } 44 | 45 | .l, /* Literal */ 46 | .il, /* Literal.Number.Integer.Long */ 47 | .m, /* Literal.Number */ 48 | .mf, /* Literal.Number.Float */ 49 | .mh, /* Literal.Number.Hex */ 50 | .mi, /* Literal.Number.Integer */ 51 | .mo, /* Literal.Number.Oct */ 52 | .se /* Literal.String.Escape */ 53 | { 54 | color: var(--bh-code-literal); 55 | } 56 | 57 | .n, /* Name */ 58 | .p, /* Punctuation */ 59 | .nb, /* Name.Builtin */ 60 | .ni, /* Name.Entity */ 61 | .nl, /* Name.Label */ 62 | .nn, /* Name.Namespace */ 63 | .py, /* Name.Property */ 64 | .w, /* Text.Whitespace */ 65 | .bp, /* Name.Builtin.Pseudo */ 66 | .vc, /* Name.Variable.Class */ 67 | .vg, /* Name.Variable.Global */ 68 | .vi /* Name.Variable.Instance */ 69 | { 70 | color: var(--bh-code-text); 71 | } 72 | 73 | .nv /* Name.Variable */ 74 | { 75 | color: var(--bh-code-var); 76 | } 77 | 78 | 79 | .o, /* Operator */ 80 | .kn, /* Keyword.Namespace */ 81 | .ow, /* Operator.Word */ 82 | .nt /* Name.Tag */ 83 | { 84 | color: var(--bh-code-operator); 85 | } 86 | 87 | .ld, /* Literal.Date */ 88 | .s, /* Literal.String */ 89 | .sb, /* Literal.String.Backtick */ 90 | .sc, /* Literal.String.Char */ 91 | .sd, /* Literal.String.Doc */ 92 | .s2, /* Literal.String.Double */ 93 | .sh, /* Literal.String.Heredoc */ 94 | .si, /* Literal.String.Interpol */ 95 | .sx, /* Literal.String.Other */ 96 | .sr, /* Literal.String.Regex */ 97 | .s1, /* Literal.String.Single */ 98 | .ss /* Literal.String.Symbol */ 99 | { 100 | color: var(--bh-code-string); 101 | } 102 | 103 | .na, /* Name.Attribute */ 104 | .nc, /* Name.Class */ 105 | .nd, /* Name.Decorator */ 106 | .ne, /* Name.Exception */ 107 | .nf, /* Name.Function */ 108 | .nx /* Name.Other */ 109 | { 110 | color: var(--bh-code-name); 111 | } 112 | } 113 | 114 | /* 115 | * Customise colours for Gherkin - the wagtail/pygments defaults don't feel quite logical 116 | */ 117 | .highlight-gherkin { 118 | /* Pygment "Function Name* in gherkin is the step feature / step text etc which is more like "text" in other langs */ 119 | --bh-code-name: var(--bh-code-text); 120 | /* Pygment "Variables" in gherkin is things like which are closer to step args */ 121 | --bh-code-var: var(--bh-code-string); 122 | } 123 | -------------------------------------------------------------------------------- /_theme/css/colours-brand.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Main behat colours - based on extraction from a picture of a cucumber 😂 and then 3 | * generating complementary etc colours. 4 | */ 5 | :root { 6 | /* 7 | * Tints and shades here can be computed from brand colours using color-mix for ease 8 | * of changing / clarity about where they came from. However, the color-mix functions 9 | * need to take hardcoded colours (not variables) to allow lightningcss to compile them 10 | * out for older browsers. 11 | */ 12 | --behat-green: #506A2A; 13 | --behat-green-dk1: color-mix(in oklab, #506A2A, black 20%); 14 | --behat-green-dk2: color-mix(in oklab, #506A2A, black 40%); 15 | --behat-green-dk3: color-mix(in oklab, #506A2A, black 60%); 16 | --behat-green-dk4: color-mix(in oklab, #506A2A, black 80%); 17 | 18 | /* Alternate green */ 19 | --behat-green-2: #DBDFBE; 20 | --behat-green-2-dk3: color-mix(in oklab, #DBDFBE, black 60%); 21 | --behat-green-2-lt2: color-mix(in oklab, #DBDFBE, white 40%); 22 | 23 | --behat-accent: #2a506a; 24 | } 25 | -------------------------------------------------------------------------------- /_theme/css/colours-dark.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Defines variables for the dark mode theme 3 | */ 4 | body.theme-dark { 5 | /* 6 | * General page colours 7 | */ 8 | background-color: var(--behat-green-dk4) !important; 9 | color: var(--behat-green-2-lt2) !important; 10 | 11 | --bh-bg-primary: var(--behat-green-dk1); 12 | --bh-link-underline: var(--behat-green-2-dk3); 13 | 14 | /* 15 | * Code samples 16 | */ 17 | --bh-code-bg: var(--behat-green-dk3); 18 | 19 | 20 | /* 21 | * Fixes for elements that are forced to hardcoded colours by wagtail 22 | */ 23 | 24 | 25 | 26 | 27 | 28 | } 29 | -------------------------------------------------------------------------------- /_theme/css/colours-light.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Defines variables for the default (light mode) theme 3 | */ 4 | body { 5 | /* 6 | * General page colours 7 | */ 8 | --bh-bg-primary: var(--behat-green); 9 | --bh-footer-bg: var(--behat-green-dk3); 10 | 11 | /* 12 | * Links 13 | */ 14 | --bh-link-color: var(--behat-green-dk1); 15 | --bh-link-underline: var(--behat-green-2); 16 | 17 | --bh-link-color-current: #333; 18 | --bh-link-color-current-underline: transparent; 19 | 20 | /* 21 | * Code samples 22 | */ 23 | --bh-code-bg: var(--behat-green-dk4); 24 | --bh-code-comment: #aaa; 25 | --bh-code-keyword: #66d9ef; 26 | --bh-code-literal: #ae81ff; 27 | --bh-code-text: #f8f8f2; 28 | --bh-code-var: #f8f8f2; 29 | --bh-code-operator: #f92672; 30 | --bh-code-string: #e6db74; 31 | --bh-code-name: #a6e22e; 32 | 33 | 34 | /* 35 | * Admonitions (tips, warnings, etc) 36 | */ 37 | --bh-admonition-title-color: #fff; 38 | --bh-adm-hint-bg: var(--behat-green); 39 | --bh-adm-tip-bg: var(--behat-accent); 40 | --bh-adm-warning-bg: #dba507; 41 | --bh-adm-danger-bg: #6a2a30; 42 | } 43 | -------------------------------------------------------------------------------- /_theme/css/font.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Use Behat (Comfortaa) font 3 | */ 4 | 5 | h1, 6 | h2, 7 | h3, 8 | h4, 9 | h5 { 10 | font-family: 'Comfortaa', var(--font-family-sans-serif); 11 | } 12 | 13 | header .navbar-brand { 14 | font-family: 'Comfortaa', var(--font-family-sans-serif); 15 | /* 16 | * font metrics are different to the font wagtail is using, their 1.5em is offcentre vertically with our font 17 | */ 18 | font-size: 1.25rem; 19 | } -------------------------------------------------------------------------------- /_theme/css/page-colour-scheme.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Apply the colour scheme to main page / simple elements. 3 | * More complex components should get their own CSS files. 4 | */ 5 | .bg-primary { 6 | background-color: var(--bh-bg-primary) !important; 7 | } 8 | 9 | footer.bg-primary { 10 | background-color: var(--bh-footer-bg) !important; 11 | } 12 | 13 | a { 14 | color: var(--bh-link-color); 15 | text-decoration-color: var(--bh-link-underline); 16 | } 17 | 18 | .toc .current > a { 19 | color: var(--bh-link-color-current); 20 | text-decoration-color: var(--bh-link-color-current-underline); 21 | } 22 | -------------------------------------------------------------------------------- /_theme/css/wagtail-fixes/admonition-spacing.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Remove redundant whitespace at the bottom of an admonition 3 | * @todo: could be contributed back to wagtail? 4 | */ 5 | .admonition > p:last-child { 6 | margin-bottom: 0; 7 | } 8 | -------------------------------------------------------------------------------- /_theme/css/wagtail-fixes/dark-theme-overrides.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Forces style application to the elements wagtail override in their _darkmode.scss 3 | * 4 | * @todo: some of these could be tidied by applying them consistently with CSS variables 5 | */ 6 | body.theme-dark { 7 | 8 | hr, 9 | .border, 10 | .border-top, 11 | .border-right, 12 | .border-bottom, 13 | .border-left, 14 | .sidebar-container, 15 | .search li { 16 | /* The borders between layout elements */ 17 | border-color: var(--behat-green-dk3) !important; 18 | } 19 | 20 | .toc .toctree-expand { 21 | /* The + drilldowns at the side of the nav */ 22 | background-color: var(--bh-bg-primary) !important; 23 | } 24 | 25 | .btn-light { 26 | /* The Light mode / edit on github buttons */ 27 | color: #eee !important; 28 | background-color: var(--bh-bg-primary) !important; 29 | border-color: color-mix(in oklab, var(--bh-bg-primary) 80%, transparent) !important; 30 | 31 | &:hover, 32 | &:focus, 33 | &:focus-within, 34 | &:not(:disabled):not(.disabled):active, 35 | &:not(:disabled):not(.disabled).active { 36 | color: #eee !important; 37 | background-color: var(--behat-green) !important; 38 | } 39 | } 40 | 41 | .searchbox { 42 | > .input-group { 43 | color: #eee !important; 44 | background-color: var(--bh-bg-primary) !important; 45 | 46 | .input-group-text { 47 | color: inherit !important; 48 | background-color: transparent !important; 49 | border-color: #ffffff22 !important; 50 | } 51 | 52 | .form-control { 53 | color: inherit !important; 54 | background-color: transparent !important; 55 | border-color: #ffffff22 !important; 56 | 57 | &::placeholder { 58 | color: inherit !important; 59 | } 60 | } 61 | } 62 | } 63 | 64 | table.docutils:not(.field-list) { 65 | th { 66 | background-color: var(--behat-green-dk2) !important; 67 | border-color: #ffffff22 !important; 68 | } 69 | 70 | td { 71 | background-color: var(--behat-green-dk3) !important; 72 | border-color: #ffffff22 !important; 73 | } 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /_theme/css/wagtail-fixes/sticky-footer.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Ensure that the footer is pulled to the bottom on short pages 3 | * @todo: this could be contributed back to the theme 4 | */ 5 | body { 6 | display: flex; 7 | flex-direction: column; 8 | min-height: 100vh; 9 | 10 | > div.container-fluid { 11 | flex-grow: 1; 12 | } 13 | } 14 | 15 | -------------------------------------------------------------------------------- /_theme/css/wagtail-fixes/table-spacing.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Remove redundant whitespace at the bottom of table cells 3 | * Because sphinx renders the cell content as `

` tags and bootstrap always puts a bottom 4 | * margin on them. 5 | * @todo: could be contributed back to wagtail? 6 | */ 7 | td, th { 8 | > p:last-child { 9 | margin-bottom: 0; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /_theme/css/wagtail-fixes/top-whitespace.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Improve vertical spacing and alignment at the tops of pages 3 | * @todo this would be cleaner to implement if contributed back to wagtail 4 | */ 5 | 6 | .page-toc { 7 | /* Remove the extra whitespace above the page nav heading (is 2.4rem in the theme, plus the 1.5rem below the HR */ 8 | padding-top: 0; 9 | } 10 | 11 | .rst-content > main > section:first-child { 12 | /* Remove the extra whitespace above the main article heading (is 2.5rem in the theme, plus the 1.5rem below the HR */ 13 | margin-top: 0; 14 | } 15 | 16 | .row :has(>.rst-content) { 17 | /* Get the article title & "Page contents" headings to sit on the same baseline */ 18 | align-items: baseline; 19 | /* If extra whitespace desired above the headings, would be better to add a padding-top here to push the article 20 | and nav content down by the same amount 21 | */ 22 | } 23 | -------------------------------------------------------------------------------- /_theme/python/button.py: -------------------------------------------------------------------------------- 1 | # Basic sphinx plugin to support button-style links from RST 2 | # Borrowed from https://stackoverflow.com/questions/25088113/make-a-css-button-a-link-in-sphinx 3 | # and https://github.com/conda/conda-docs/blob/04613488d57688d77b0bc20618b9a4d5b56947ed/web/source/conf.py 4 | # with some alterations to allow customisation of the button CSS class and to convert `/` urls to be relative 5 | # to the html_baseurl for support on ReadTheDocs etc and fixes for newer Sphinx versions. 6 | from __future__ import absolute_import 7 | from docutils import nodes 8 | import jinja2 9 | from docutils.parsers.rst.directives import unchanged 10 | from sphinx.util.docutils import SphinxDirective 11 | 12 | BUTTON_TEMPLATE = jinja2.Template(u""" 13 | 14 | {{ text }} 15 | 16 | """) 17 | 18 | # placeholder node for document graph 19 | class button_node(nodes.General, nodes.Element): 20 | pass 21 | 22 | class ButtonDirective(SphinxDirective): 23 | required_arguments = 0 24 | 25 | option_spec = { 26 | 'class': unchanged, 27 | 'text': unchanged, 28 | 'link': unchanged, 29 | } 30 | 31 | # this will execute when your directive is encountered 32 | # it will insert a button_node into the document that will 33 | # get visisted during the build phase 34 | def run(self): 35 | # Absolute local URLs should be made relative to the html_baseurl for the site 36 | # Otherwise they will not work on ReadTheDocs 37 | url = self.options['link'] 38 | if url.startswith('/'): 39 | url = self.config.html_baseurl + url.lstrip('/') 40 | 41 | if 'class' in self.options: 42 | btn_class = self.options['class'] 43 | else: 44 | btn_class = 'btn btn-primary' 45 | 46 | node = button_node() 47 | node['text'] = self.options['text'] 48 | node['btn_class'] = btn_class 49 | node['link'] = url 50 | return [node] 51 | 52 | # build phase visitor emits HTML to append to output 53 | def html_visit_button_node(self, node): 54 | html = BUTTON_TEMPLATE.render(text=node['text'], btn_class=node['btn_class'], link=node['link']) 55 | self.body.append(html) 56 | raise nodes.SkipNode 57 | 58 | # note, not defining visitors for text or manpage etc format as we only output HTML for Behat 59 | 60 | def setup(app): 61 | app.add_node(button_node, 62 | html=(html_visit_button_node, None)) 63 | app.add_directive('button', ButtonDirective) -------------------------------------------------------------------------------- /_theme/python/lightningcss.py: -------------------------------------------------------------------------------- 1 | # This sphinx extension downloads and installs lightningcss and then uses it to bundle our 2 | # theme CSS files. We're using binaries built by lightningcss but they only distribute them 3 | # through npm - so we have to deal with platform detection and then download the .tar from 4 | # npm and extract it ourselves. 5 | # 6 | # @todo This could be extracted to a standalone sphinx extension and (I think) use PyPi to 7 | # mirror the npm lightningcss binaries as python platform-specific wheels. 8 | from urllib.request import urlretrieve 9 | from sphinx.util import logging 10 | import platform 11 | import os 12 | import shutil 13 | import subprocess 14 | import tarfile 15 | from tempfile import gettempdir 16 | 17 | def detect_lightningcss_platform(): 18 | os = platform.system() 19 | lib, _ =platform.libc_ver() 20 | arch = platform.machine() 21 | 22 | # @todo there are lightningcss binaries for many platforms, but the names don't map directly to 23 | # the values python provides and it would need a bit of testing to be confident I was 24 | # getting them correct for other environments. For now, just check it's the expected 25 | # platform for our local Docker and for ReadTheDocs 26 | 27 | if os != 'Linux': 28 | raise Exception(f"Installation on '{os}' is not yet supported") 29 | 30 | if lib != 'glibc': 31 | raise Exception(f"Installation with '{lib}' c compiler is not yet supported") 32 | 33 | if arch != 'x86_64': 34 | raise Exception(f"Installation on '{arch}' architecture is not yet supported") 35 | 36 | return 'linux-x64-gnu' 37 | 38 | def install_lightningcss(version): 39 | platform = detect_lightningcss_platform() 40 | 41 | # Place the binary in the working directory - it should survive across docker restarts when working locally 42 | filename = f"{os.getcwd()}/lightningcss-{platform}-{version}" 43 | if os.path.isfile(filename): 44 | return filename 45 | 46 | # Download from the nodejs release (these packages just contain a standalone binary for each platform) 47 | download_url = f"https://registry.npmjs.org/lightningcss-cli-linux-x64-gnu/-/lightningcss-cli-{platform}-{version}.tgz" 48 | 49 | logger = logging.getLogger(__name__) 50 | logger.info(f"Downloading lightningcss from {download_url}") 51 | lightning_tar , _ = urlretrieve(download_url) 52 | 53 | # Unpack the lightningcss binary from the archive to the tmpdir 54 | tmpdir = gettempdir() 55 | tar = tarfile.open(lightning_tar) 56 | tar.extract('package/lightningcss', path=tmpdir, filter='data') 57 | 58 | # Move the executable to the working dir 59 | shutil.move(f"{tmpdir}/package/lightningcss", filename) 60 | logger.info(f"Unpacked lightningcss to {filename}") 61 | 62 | return filename 63 | 64 | def compile_css(app,exc): 65 | if app.builder.format != 'html': 66 | return 67 | if exc: 68 | # there was an exception 69 | return 70 | 71 | lightningcss_bin = install_lightningcss('1.29.3') 72 | logger = logging.getLogger(__name__) 73 | 74 | # Run it 75 | input_css = '_theme/css/behat-docs.css' 76 | output_css = app.outdir / '_static/css/behat-docs.css' 77 | logger.info(f"Compiling {input_css} to {output_css} with lightningcss") 78 | subprocess.run([ 79 | lightningcss_bin, 80 | '--bundle', 81 | '--minify', 82 | '--targets', 83 | '>=0.25%', 84 | input_css, 85 | '-o', 86 | output_css 87 | ]) 88 | 89 | def setup(app): 90 | app.connect('build-finished', compile_css) 91 | -------------------------------------------------------------------------------- /_theme/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Behat/docs/ac2b628bcbe3972c9dca6fa67e49462a7f15dfa8/_theme/static/favicon.png -------------------------------------------------------------------------------- /_theme/static/img/behat-b-2x-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Behat/docs/ac2b628bcbe3972c9dca6fa67e49462a7f15dfa8/_theme/static/img/behat-b-2x-white.png -------------------------------------------------------------------------------- /_theme/templates/globaltoc.html: -------------------------------------------------------------------------------- 1 | {# 2 | The only customisation in this template is to change `includeHidden=true` to include 3 | our toctree from the guides page (which is hidden on that page). 4 | @TODO: Other sphinx themes provide this as a theme config option, this could be contributed 5 | back to sphinx wagtail theme to allow setting this in conf.py rather than overriding 6 | the template. 7 | #} 8 |

9 | 12 | 17 |
18 | -------------------------------------------------------------------------------- /_theme/templates/layout.html: -------------------------------------------------------------------------------- 1 | {# 2 | Wagtail includes a javascript search that we're not using, they render a bunch of 3 | JS and meta links etc into the markup for this. 4 | We extend their template to prevent rendering that content. 5 | @todo this would ideally be a config option - I'm not sure where the "search" document 6 | actually comes from. 7 | #} 8 | {% extends "sphinx_wagtail_theme/layout.html" %} 9 | {% block linktags %} 10 | {# suppress the wagtail-generated link rel=search meta tag #} 11 | 12 | {%- if parents %} 13 | 14 | {%- endif %} 15 | {%- if next %} 16 | 17 | {%- endif %} 18 | {%- if prev %} 19 | 20 | {%- endif %} 21 | {% endblock %} 22 | {% block search %} 23 | {# suppress the wagtail search javascript #} 24 | {% endblock %} 25 | -------------------------------------------------------------------------------- /_theme/templates/pager.html: -------------------------------------------------------------------------------- 1 | {% include "sphinx_wagtail_theme/pager.html" %} 2 | 3 |
4 | -------------------------------------------------------------------------------- /_theme/templates/searchbox.html: -------------------------------------------------------------------------------- 1 | {# 2 | This custom searchbox triggers the ReadTheDocs search instead of a local search 3 | #} 4 | {%- if builder != "singlehtml" %} 5 | 18 | 24 | {%- endif %} 25 | -------------------------------------------------------------------------------- /borg.json: -------------------------------------------------------------------------------- 1 | { 2 | "for-package": "behat/behat" 3 | } 4 | -------------------------------------------------------------------------------- /community.rst: -------------------------------------------------------------------------------- 1 | Community 2 | ========= 3 | 4 | Behat has an amazing community around it. 5 | 6 | The Behat community is friendly and welcoming. All questions and comments are 7 | valuable, so please come join the discussion! 8 | 9 | There are a number of places to connect with community members at all experience levels. 10 | 11 | - Have a question? Join us in our `Gitter channel`_ 12 | - Want to submit an issue or contribute a feature? Read about contributing in our `contribution guide`_ 13 | - Want latest news from around community? Follow us on Twitter_ 14 | 15 | .. _`Gitter channel`: https://gitter.im/Behat/Behat 16 | .. _`contribution guide`: https://github.com/Behat/Behat/blob/master/CONTRIBUTING.md 17 | .. _Twitter: https://twitter.com/behatphp 18 | -------------------------------------------------------------------------------- /conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import sys, os 4 | from sphinx.highlighting import lexers 5 | from pygments.lexers.web import PhpLexer 6 | 7 | lexers['php'] = PhpLexer(startinline=True) 8 | 9 | # Add our custom python path to the system search path so that sphinx can find extensions there 10 | sys.path.insert(0, os.path.abspath('./_theme/python')) 11 | 12 | extensions = [ 13 | 'notfound.extension', 14 | # Enables automatic linking to headings within the document within Sphinx 15 | 'sphinx.ext.autosectionlabel', 16 | 'sphinx_wagtail_theme', 17 | # Enables a .. button:: directive to render a link as a button e.g. on homepage. Use sparingly. 18 | 'button', 19 | # Installs and runs lightningcss to bundle and minify our custom CSS 20 | 'lightningcss', 21 | ] 22 | 23 | source_suffix = '.rst' 24 | source_encoding = 'utf-8' 25 | master_doc = 'index' 26 | 27 | project = u'Behat' 28 | copyright = u'2016 - %Y, Konstantin Kudryashov (@everzet)' 29 | 30 | language = 'php' 31 | highlight_language = 'php' 32 | 33 | exclude_trees = [] 34 | exclude_patterns = [] 35 | 36 | htmlhelp_basename = 'behat' 37 | 38 | # Files in these paths are copied to the _static output directory e.g. theme/static/img/file.png 39 | # will become "/_static/img/file.png". They will overwrite any file from the theme with the same 40 | # name. 41 | html_static_path = ['_theme/static'] 42 | 43 | # CSS files here are relative to html_static_path (or absolute urls) 44 | html_css_files = [ 45 | 'https://fonts.googleapis.com/css?family=Comfortaa&family=JetBrains+Mono:ital,wght@0,100..800;1,100..800', 46 | 'css/behat-docs.css', 47 | ] 48 | 49 | # Files in these paths override templates provided by the theme 50 | templates_path = ['_theme/templates'] 51 | 52 | html_baseurl = os.environ.get("READTHEDOCS_CANONICAL_URL", "/") 53 | html_theme='sphinx_wagtail_theme' 54 | html_theme_options = dict( 55 | # Navbar branding 56 | project_name = "Behat", 57 | logo = 'img/behat-b-2x-white.png', 58 | logo_alt = 'Behat', 59 | logo_url = '/', 60 | logo_width = 36, 61 | logo_height = 50, 62 | 63 | # Base path for "Edit on GitHub" links 64 | github_url="https://github.com/Behat/docs/blob/v3.0/", 65 | 66 | # Global header / footer links as text|target 67 | header_links=', '.join([ 68 | f"Releases|{html_baseurl}releases.html", 69 | 'GitHub|https://github.com/Behat/Behat' 70 | ]), 71 | footer_links=', '.join([ 72 | ]) 73 | ) 74 | 75 | html_favicon = '_theme/static/favicon.png' 76 | 77 | # If false, no index is generated. 78 | # Since we are implementing search with Algolia DocSearch through ReadTheDocs, 79 | # we do not need Sphinx to generate its own index. It might not hurt to keep 80 | # the Sphinx index, but speeds up the build process. 81 | html_use_index = False 82 | 83 | # Don't show separate "view source" links on generated pages 84 | html_show_sourcelink = False 85 | -------------------------------------------------------------------------------- /cookbooks.rst: -------------------------------------------------------------------------------- 1 | Cookbooks 2 | ========= 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | cookbooks/accessing_contexts_from_each_other 8 | cookbooks/creating_a_context_configuration_extension 9 | cookbooks/custom_formatter 10 | -------------------------------------------------------------------------------- /cookbooks/accessing_contexts_from_each_other.rst: -------------------------------------------------------------------------------- 1 | Accessing Contexts from each other 2 | ================================== 3 | 4 | When splitting the definitions in multiple contexts, it might be useful to 5 | access a context from another one. This is particularly useful when migrating 6 | from Behat 2.x to replace subcontexts. 7 | 8 | Behat allows to access the environment in 9 | :doc:`hooks `, 10 | so other contexts can be retrieved using a ``BeforeScenario`` hook: 11 | 12 | .. code-block:: php 13 | 14 | use Behat\Behat\Context\Context; 15 | use Behat\Behat\Hook\Scope\BeforeScenarioScope; 16 | use Behat\Hook\BeforeScenario; 17 | 18 | class FeatureContext implements Context 19 | { 20 | /** @var \Behat\MinkExtension\Context\MinkContext */ 21 | private $minkContext; 22 | 23 | #[BeforeScenario] 24 | public function gatherContexts(BeforeScenarioScope $scope) 25 | { 26 | $environment = $scope->getEnvironment(); 27 | 28 | $this->minkContext = $environment->getContext('Behat\MinkExtension\Context\MinkContext'); 29 | } 30 | } 31 | 32 | .. caution:: 33 | 34 | Circular references in context objects would prevent the PHP reference 35 | counting from collecting contexts at the end of each scenarios, forcing 36 | to wait for the garbage collector to run. This would increase the memory 37 | usage of your Behat run. To prevent that, it is better to avoid storing 38 | the environment itself in your context classes. It is also better to 39 | avoid creating circular references between different contexts. 40 | -------------------------------------------------------------------------------- /cookbooks/creating_a_context_configuration_extension.rst: -------------------------------------------------------------------------------- 1 | Creating a Behat extension to provide custom configuration for Contexts 2 | ======================================================================= 3 | 4 | Extensions are particularly useful when configuration becomes a necessity. 5 | 6 | In this cookbook, we will create a simple extension named ``HelloWorld`` that will display some text and cover: 7 | 8 | #. Setting up the context 9 | #. Creating the extension 10 | #. Initializing the context 11 | #. Using the extension 12 | 13 | Setting Up the Context 14 | ---------------------- 15 | 16 | First, we create a ``Context`` class that will throw a ``PendingException`` with a configurable text. 17 | The configuration will also control whether the behaviour is enabled or not. 18 | 19 | The example directory structure and file locations in this cookbook assume you have configured 20 | composer to autoload the ``HelloWorld`` namespace from the ``src`` directory. 21 | 22 | .. code-block:: 23 | 24 | src/ 25 | Context/ 26 | HelloWorldContext.php # This is where we'll implement our step 27 | 28 | .. code-block:: php 29 | 30 | enable = $enable; 49 | $this->text = $text; 50 | } 51 | 52 | #[Given('I say Hello World')] 53 | public function helloWorld() 54 | { 55 | if ($this->enable) { 56 | throw new PendingException($this->text); 57 | } 58 | } 59 | } 60 | 61 | Creating the Extension 62 | ---------------------- 63 | 64 | Next, we need to create the entry point for our Hello World, the extension itself. 65 | 66 | .. code-block:: 67 | 68 | src/ 69 | Context/ 70 | HelloWorldContext.php 71 | ServiceContainer/ 72 | HelloWorldExtension.php # This is where we'll define our extension 73 | 74 | The ``getConfigKey`` method below is used to identify our extension in the configuration. 75 | The ``configure`` method is used to define the configuration tree. 76 | 77 | .. code-block:: php 78 | 79 | addDefaultsIfNotSet() 111 | ->children() 112 | ->booleanNode('enable')->defaultFalse()->end() 113 | ->scalarNode('text')->defaultValue('Hello World!')->end() 114 | ->end() 115 | ->end(); 116 | } 117 | 118 | public function load(ContainerBuilder $container, array $config) 119 | { 120 | // ... we'll load our configuration here 121 | } 122 | 123 | // needed as Extension interface implements CompilerPassInterface 124 | public function process(ContainerBuilder $container) 125 | { 126 | } 127 | } 128 | 129 | .. note:: 130 | 131 | The ``initialize`` and ``process`` methods are empty in our case but are 132 | useful when you need to interact with other extensions or process the 133 | container after it has been compiled. 134 | 135 | Initializing the Context 136 | ------------------------ 137 | 138 | To pass configuration values to our ``HelloWorldContext``, we need to create an initializer. 139 | 140 | .. code-block:: 141 | 142 | src/ 143 | Context/ 144 | Initializer/ 145 | HelloWorldInitializer.php # This will handle context initialization 146 | HelloWorldContext.php 147 | ServiceContainer/ 148 | HelloWorldExtension.php 149 | 150 | The code for ``HelloWorldInitializer.php``: 151 | 152 | .. code-block:: php 153 | 154 | text = $text; 170 | $this->enable = $enable; 171 | } 172 | 173 | public function initializeContext(Context $context) 174 | { 175 | /* 176 | * At the start of every scenario, behat will create a new instance of every `Context` 177 | * registered in your project. It will then call this method with each new `Context` in 178 | * turn. If you want to initialise multiple contexts, you can of course give them an 179 | * interface and check for that here. 180 | */ 181 | if (!$context instanceof HelloWorldContext) { 182 | return; 183 | } 184 | 185 | $context->initializeConfig($this->enable, $this->text); 186 | } 187 | } 188 | 189 | We need to register the initializer definition within the Behat container 190 | through the ``HelloWorldExtension``, ensuring it gets loaded: 191 | 192 | .. code-block:: php 193 | 194 | addTag(ContextExtension::INITIALIZER_TAG); 212 | $container->setDefinition('helloworld_extension.context_initializer', $definition); 213 | } 214 | 215 | // ... 216 | } 217 | 218 | Finally, we can define a custom configuration object for use in a project's ``behat.php``. 219 | This step is optional - users can also register your extension with the generic 220 | ``Behat\Config\Extension`` class. However, providing a custom class will make it easier 221 | for users to discover and configure the correct settings for your extension. 222 | 223 | .. code-block:: php 224 | 225 | settings; 251 | } 252 | 253 | // The following methods provide the mechanism for users to customise your 254 | // extension's configuration. 255 | // We recommend using setters rather than constructor properties, for 256 | // consistency with Behat's own config objects. 257 | 258 | public function enable(): self 259 | { 260 | $this->settings['enable'] = true; 261 | 262 | return $this; 263 | } 264 | 265 | public function disable(): self 266 | { 267 | $this->settings['enable'] = false; 268 | 269 | return $this; 270 | } 271 | 272 | public function withText(string $text): self 273 | { 274 | $this->settings['text'] = $text; 275 | 276 | return $this; 277 | } 278 | } 279 | 280 | 281 | Using the extension 282 | ------------------- 283 | 284 | Now that the extension is ready and will inject values into context, we just 285 | need to configure it into a project. 286 | 287 | We'll set up an instance of our custom ``HelloWorldExtensionConfig`` configuration 288 | object, and use the ``withExtension()`` method of a profile (``default`` in our 289 | case) to register it. 290 | 291 | Finally, we need to load the ``HelloWorld\Context\HelloWorldContext`` into our suite. 292 | 293 | Here's the ``behat.php``: 294 | 295 | .. code-block:: php 296 | 297 | withProfile(new Profile('default') 309 | ->withExtension(new HelloWorldExtensionConfig() 310 | ->withText('Hi there!') 311 | ->enable() 312 | ) 313 | ->withSuite(new Suite('default') 314 | ->withContexts( 315 | FeatureContext::class, 316 | HelloWorldContext::class 317 | ))); 318 | 319 | And now a scenario like this one: 320 | 321 | .. code-block:: 322 | 323 | Feature: Test 324 | 325 | Scenario: Test 326 | Given I say Hello World 327 | 328 | Will display our text ``Hi there!`` as a pending exception. 329 | 330 | Conclusion 331 | ---------- 332 | 333 | Congratulations! You have just created a simple Behat extension. 334 | This extension demonstrates three of the common steps to building a Behat 335 | extension: defining an extension, creating an initializer, and configuring contexts. 336 | 337 | Feel free to experiment with this extension and expand its functionality. 338 | 339 | Happy testing! 340 | -------------------------------------------------------------------------------- /cookbooks/custom_formatter.rst: -------------------------------------------------------------------------------- 1 | Writing a custom Behat formatter 2 | ================================ 3 | 4 | How to write a custom formatter for Behat? 5 | 6 | Introduction 7 | ------------ 8 | 9 | Why a custom formatter? 10 | ~~~~~~~~~~~~~~~~~~~~~~~~ 11 | 12 | Behat has three native formatters: 13 | 14 | - **pretty**: the default formatter, which prints every line in green (if a test passes) or red (if it fails), 15 | - **progress**: print a "dot" for each test, and a recap of all failing tests at the end, 16 | - **junit**: outputs a `junit`_ compatible XML file. 17 | 18 | Those are nice, and worked for most of the cases. You can use the "progress" one for the CI, and the "pretty" for 19 | development for example. 20 | 21 | But you might want to handle differently the output that Behat renders. 22 | In this cookbook, we will see how to implement a custom formatter for `reviewdog`_, 23 | a global review tool that takes input of linters or testers, and that can send "checks" on github, 24 | bitbucket or gitlab PR. 25 | 26 | Reviewdog can handle `two types of input`_: 27 | 28 | - any stdin, coupled with an "errorformat" 29 | (a Vim inspired format that can convert text string to machine-readable errors), 30 | - a `"Reviewdog Diagnostic Format"`_: a JSON with error data that reviewdog can parse. 31 | 32 | But parsing Behat stdout with errorformat is not that easy, as Behat's output is multi-line, add dots, errorformat can 33 | be tricky and might not handle every case (behat has different possible outputs, etc.). 34 | So We will create a custom formatter for Behat. 35 | 36 | This way, we will still 37 | have Behat's human-readable stdout, and a JSON file written that reviewdog can understand. 38 | 39 | 40 | .. _`junit`: https://junit.org/ 41 | .. _`reviewdog`: https://github.com/reviewdog/reviewdog 42 | .. _`two types of input`: https://github.com/reviewdog/reviewdog#input-format 43 | .. _`"Reviewdog Diagnostic Format"`: https://github.com/reviewdog/reviewdog/tree/48b25a0aafb8494e751387e16f729faee9522c46/proto/rdf 44 | 45 | Let's dive 46 | ---------- 47 | 48 | Behat allows us to load "extensions", that can add features to the language. In fact, it is a core functionality to 49 | implement PHP functions behind gherkin texts. 50 | Those extensions are just classes that are loaded by Behat to register configuration and features. 51 | 52 | Behat is powered by Symfony: if you know it, you will already know the concepts under the hood, if you don't, 53 | that's not a problem and not required to create your extension. 54 | 55 | Anatomy of a formatter extension 56 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 57 | 58 | A formatter extension requires three things to work: 59 | 60 | - a class that "defines" the extension, to make your extension work with Behat, 61 | - a "formatter", that can listen to Behat events, and converts Behat's tests result to anything you want, 62 | - an "output printer", that writes the converted data anywhere you want (mainly the stdout, a file or a directory). 63 | 64 | Create the extension 65 | ~~~~~~~~~~~~~~~~~~~~ 66 | 67 | Any Behat extensions must implement ``Behat\Testwork\ServiceContainer\Extension``. 68 | It is a way to inject anything you want into Behat's kernel. 69 | 70 | In our case, we need to load the "formatter" in Behat's kernel, and tag it as an output formatter. 71 | This way Behat will allow our extension to be configured as a formatter. You can register multiple formatters with the 72 | same extension if you like. 73 | 74 | .. code:: php 75 | 76 | register(ReviewdogOutputPrinter::class); 102 | 103 | // add some arguments. In this case, it will use Behat's current working directory to write the output file, if not override 104 | $outputPrinterDefinition->addArgument('%paths.base%'); 105 | 106 | // register the "ReviewdogFormatter" class in Behat's kernel 107 | $formatterDefinition = $container->register(ReviewdogFormatter::class); 108 | 109 | // add some arguments that will be called in the constructor. 110 | // This isn't required, but in our case we will inject Behat's base path (to remove it from the absolute file path later) and the printer. 111 | $formatterDefinition->addArgument('%paths.base%'); 112 | $formatterDefinition->addArgument($outputPrinterDefinition); 113 | 114 | // tag the formatter as an "output.formatter", this way Behat will add it to its formatter list. 115 | $formatterDefinition->addTag(OutputExtension::FORMATTER_TAG, ['priority' => 100]); 116 | } 117 | 118 | public function configure(ArrayNodeDefinition $builder): void { } 119 | 120 | public function initialize(ExtensionManager $extensionManager): void { } 121 | 122 | public function process(ContainerBuilder $container): void { } 123 | } 124 | 125 | Create the formatter 126 | ~~~~~~~~~~~~~~~~~~~~ 127 | 128 | The formatter will listen to Behat's events, and create output data depending on the type of event, the current state, 129 | etc. 130 | 131 | .. code:: php 132 | 133 | outputPrinter->setFileName($value); 166 | break; 167 | default: 168 | throw new \Exception('Unknown parameter ' . $name); 169 | } 170 | } 171 | 172 | /** 173 | * We do not call this, so no need to define an implementation 174 | */ 175 | public function getParameter($name) { } 176 | 177 | /** 178 | * Our formatter is a Symfony EventSubscriber. 179 | * This method tells Behat where we want to "hook" in the process. 180 | * Here we want to be called: 181 | * - at start, when the test is launched with the `BeforeExerciseCompleted::BEFORE` event, 182 | * - when a step has ended with the `StepTested::AFTER` event. 183 | * 184 | * There are a lot of other events that can be found here in the Behat\Testwork\EventDispatcher\Event class 185 | */ 186 | public static function getSubscribedEvents(): array 187 | { 188 | return [ 189 | // call the `onBeforeExercise` method on startup 190 | BeforeExerciseCompleted::BEFORE => 'onBeforeExercise', 191 | // call the `onAfterStepTested` method after each step 192 | StepTested::AFTER => 'onAfterStepTested', 193 | ]; 194 | } 195 | 196 | /** 197 | * This is the name of the formatter, that will be used in the behat.yml file 198 | */ 199 | public function getName(): string 200 | { 201 | return 'reviewdog'; 202 | } 203 | 204 | public function getDescription(): string 205 | { 206 | return 'Reviewdog formatter'; 207 | } 208 | 209 | public function getOutputPrinter(): OutputPrinter 210 | { 211 | return $this->outputPrinter; 212 | } 213 | 214 | /** 215 | * When we launch a test, let's inform the printer that we want a fresh new file 216 | */ 217 | public function onBeforeExercise(BeforeExerciseCompleted $event):void 218 | { 219 | $this->outputPrinter->removeOldFile(); 220 | } 221 | 222 | public function onAfterStepTested(AfterStepTested $event):void 223 | { 224 | $testResult = $event->getTestResult(); 225 | $step = $event->getStep(); 226 | 227 | // In the reviewdog formatter, we just want to print errors, so ignore all steps that are not a failure executed test 228 | // but you might want to handle things differently here ! 229 | if ($testResult->isPassed() || !$testResult instanceof ExecutedStepResult) { 230 | return; 231 | } 232 | 233 | // get the relative path 234 | $path = str_replace($this->pathsBase . '/', '', $event->getFeature()->getFile() ?? ''); 235 | 236 | // prepare the data that we will send to the printer… 237 | $line = [ 238 | 'message' => $testResult->getException()?->getMessage() ?? 'Failed step', 239 | 'location' => [ 240 | 'path' => $path, 241 | 'range' => [ 242 | 'start' => [ 243 | 'line' => $step->getLine(), 244 | 'column' => 0, 245 | ], 246 | ], 247 | ], 248 | 'severity' => 'ERROR', 249 | 'source' => [ 250 | 'name' => 'behat', 251 | ], 252 | ]; 253 | 254 | $json = json_encode($line, \JSON_THROW_ON_ERROR); 255 | 256 | // …and send it 257 | $this->getOutputPrinter()->writeln($json); 258 | } 259 | 260 | } 261 | 262 | Create the output printer 263 | ~~~~~~~~~~~~~~~~~~~~~~~~~ 264 | 265 | The last file that we need to implement is the printer. In our case we need a single class that can write lines to a 266 | file. 267 | 268 | .. code:: php 269 | 270 | fileName = $fileName; 294 | } 295 | 296 | /** 297 | * outputPath is a special parameter that you can give to any Behat formatter under the key `output_path` 298 | */ 299 | public function setOutputPath($path): void 300 | { 301 | $this->outputPath = $path; 302 | } 303 | 304 | /** 305 | * The output path, defaults to Behat's base path 306 | */ 307 | public function getOutputPath(): string 308 | { 309 | return $this->outputPath ?? $this->pathBase; 310 | } 311 | 312 | /** Sets output styles. */ 313 | public function setOutputStyles(array $styles): void { } 314 | 315 | /** @deprecated */ 316 | public function getOutputStyles() 317 | { 318 | return []; 319 | } 320 | 321 | /** Forces output to be decorated. */ 322 | public function setOutputDecorated($decorated): void 323 | { 324 | $this->isOutputDecorated = (bool) $decorated; 325 | } 326 | 327 | /** @deprecated */ 328 | public function isOutputDecorated() 329 | { 330 | return $this->isOutputDecorated; 331 | } 332 | 333 | /** 334 | * Behat can have multiple verbosity levels, you may want to handle this to display more information. 335 | * These use the Symfony\Component\Console\Output\OutputInterface::VERBOSITY_ constants. 336 | * For reviewdog, we do not need that. 337 | */ 338 | public function setOutputVerbosity($level): void { } 339 | 340 | /** @deprecated */ 341 | public function getOutputVerbosity() 342 | { 343 | return 0; 344 | } 345 | 346 | /** 347 | * Writes message(s) to output stream. 348 | * 349 | * @param string|array $messages 350 | */ 351 | public function write($messages): void 352 | { 353 | if (!is_array($messages)) { 354 | $messages = [$messages]; 355 | } 356 | 357 | $this->doWrite($messages, false); 358 | } 359 | 360 | /** 361 | * Writes newlined message(s) to output stream. 362 | * 363 | * @param string|array $messages 364 | */ 365 | 366 | public function writeln($messages = ''): void 367 | { 368 | if (!is_array($messages)) { 369 | $messages = [$messages]; 370 | } 371 | 372 | $this->doWrite($messages, true); 373 | } 374 | 375 | /** 376 | * Clear output stream, so on next write formatter will need to init (create) it again. 377 | * Not needed in my case. 378 | */ 379 | public function flush(): void 380 | { 381 | } 382 | 383 | /** 384 | * Called by the formatter when test starts 385 | */ 386 | public function removeOldFile(): void 387 | { 388 | $filePath = $this->getFilePath(); 389 | 390 | if (file_exists($filePath)) { 391 | unlink($filePath); 392 | } 393 | } 394 | 395 | /** 396 | * @param array $messages 397 | */ 398 | private function doWrite(array $messages, bool $append): void 399 | { 400 | // create the output path if if does not exists. 401 | if (!is_dir($this->getOutputPath())) { 402 | mkdir($this->getOutputPath(), 0777, true); 403 | } 404 | 405 | // write data to the file 406 | file_put_contents($this->getFilePath(), implode("\n", $messages) . "\n", $append ? \FILE_APPEND : 0); 407 | } 408 | 409 | private function getFilePath(): string 410 | { 411 | return $this->getOutputPath() . '/' . $this->fileName; 412 | } 413 | } 414 | 415 | Integration in your project 416 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 417 | 418 | You need to add the extension in your Behat configuration file (default is ``behat.php``) 419 | and configure it to use the formatter: 420 | 421 | .. code:: php 422 | 423 | withProfile(new Profile('default') 434 | ->withExtension(new Extension(ReviewdogFormatterExtension::class))); 435 | ->withFormatter(new PrettyFormatter()) 436 | // "reviewdog" here is the "name" given in our formatter 437 | ->withFormatter(new Formatter('reviewdog', [ 438 | // 'file_name' is optional and a custom parameter that we inject into the printer 439 | 'file_name' => 'reviewdog-behat.json', 440 | ]) 441 | // outputPath is optional and handled directly by Behat 442 | ->withOutputPath('build/logs/behat')) 443 | 444 | .. note:: 445 | 446 | Remember, this formatter only produces output if scenarios fail - if your features all 447 | pass, the output file will not be created. 448 | 449 | Different output per profile 450 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 451 | 452 | You can activate the extension only when you specify a profile in your command (ex: ``--profile=ci``) 453 | 454 | For example if you want the pretty formatter by default, but both progress and reviewdog on your CI, 455 | you can configure it like this: 456 | 457 | .. code:: php 458 | 459 | withProfile(new Profile('default') 471 | ->withExtension(new Extension(ReviewdogFormatterExtension::class))) 472 | ->withFormatter(new PrettyFormatter()) 473 | ->withProfile(new Profile('ci') 474 | ->disableFormatter('pretty') 475 | ->withFormatter(new ProgressFormatter()) 476 | ->withFormatter(new Formatter('reviewdog', [ 477 | 'file_name' => 'reviewdog-behat.json', 478 | ]) 479 | ->withOutputPath('build/logs/behat'))); 480 | 481 | Enjoy! 482 | ------- 483 | 484 | That's how you can write a basic custom Behat formatter! 485 | 486 | If you have much more complex logic, and you need the formatter to be more dynamic, Behat provides a 487 | FormatterFactory interface. 488 | You can see usage examples directly in `Behat's codebase`_, 489 | but in a lot of cases, something like this example should work. 490 | 491 | .. _`Behat's codebase`: https://github.com/Behat/Behat/tree/2a3832d9cb853a794af3a576f9e524ae460f3340/src/Behat/Behat/Output/ServiceContainer/Formatter 492 | 493 | Want to use reviewdog and the custom formatter yourself? 494 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 495 | 496 | If you want to use the reviewdog custom formatter, you can find it on github: `jdeniau/behat-reviewdog-formatter`_ 497 | 498 | There are other Behat custom formatters in the wild, especially `BehatHtmlFormatterPlugin`_. 499 | Reading this formatter might help you understand how the Behat formatter system works, and it can output an HTML 500 | file that can help you understand why your CI is failing. 501 | 502 | 503 | .. _`jdeniau/behat-reviewdog-formatter`: https://github.com/jdeniau/behat-reviewdog-formatter 504 | .. _`BehatHtmlFormatterPlugin`: https://github.com/dutchiexl/BehatHtmlFormatterPlugin 505 | 506 | About the author 507 | ~~~~~~~~~~~~~~~~ 508 | 509 | Written by `Julien Deniau `__, 510 | originally posted as a blog post `on his blog `__. 511 | 512 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | read-the-docs-builder: 3 | build: 4 | context: . 5 | volumes: 6 | - ./:/workspace 7 | ports: 8 | # Technically speaking, we don't need to override the sphinx-autobuild port inside the container. 9 | # However, sphinx-autobuild prints the URL / port it is serving on after every build and it will 10 | # be confusing if this does not match the forwarded port on the host. 11 | - ${SPHINX_PORT:-8000}:${SPHINX_PORT:-8000} 12 | environment: 13 | SPHINX_PORT: ${SPHINX_PORT:-8000} 14 | entrypoint: /workspace/docker/entrypoint.sh 15 | command: serve 16 | -------------------------------------------------------------------------------- /docker/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -o errexit 3 | set -o nounset 4 | 5 | command="${1:-build}" 6 | 7 | if [ "$command" == 'build' ] ; then 8 | echo "Building docs" 9 | exec python -m sphinx -T -W --keep-going -b html -d _build/doctrees -D language=en . _build/html 10 | 11 | elif [ "$command" == 'serve' ] ; then 12 | echo "Serving live docs on http://localhost:$SPHINX_PORT" 13 | # Note the --write-all option (= -a) which forces a full rather than incremental build. 14 | # This is because sphinx does not properly update theme and similar files on incremental builds 15 | # See https://github.com/sphinx-doc/sphinx-autobuild?tab=readme-ov-file#relevant-sphinx-bugs 16 | # 17 | # Also note the host 0.0.0.0 here is from inside the docker container. 18 | exec sphinx-autobuild --write-all --host 0.0.0.0 --port "$SPHINX_PORT" . _build/html 19 | 20 | else 21 | echo "Unknown command $command" 22 | exit 1; 23 | fi 24 | -------------------------------------------------------------------------------- /guides.rst: -------------------------------------------------------------------------------- 1 | Behat Documentation 2 | =================== 3 | 4 | Behat is an open source Behavior Driven Development framework for PHP 5.3+. 5 | What's *behavior driven development*, you ask? It's a way to develop software 6 | through a constant communication with stakeholders in form of examples; 7 | examples of how this software should help them, and you, to achieve your goals. 8 | 9 | For example, imagine you're about to create the famous UNIX ``ls`` command. 10 | Before you begin, you will have a conversation with your stakeholders (UNIX 11 | users) and they might say that even though they like UNIX a lot, they need a 12 | way to see all the files in the current working directory. You then have 13 | a back-and-forth chat with them about how they see this feature 14 | working and you come up with your first scenario (an alternative name for example 15 | in BDD methodology): 16 | 17 | .. code-block:: gherkin 18 | 19 | Feature: Listing command 20 | In order to change the structure of the folder I am currently in 21 | As a UNIX user 22 | I need to be able to see the currently available files and folders there 23 | 24 | Scenario: Listing two files in a directory 25 | Given I am in a directory "test" 26 | And I have a file named "foo" 27 | And I have a file named "bar" 28 | When I run "ls" 29 | Then I should get: 30 | """ 31 | bar 32 | foo 33 | """ 34 | 35 | If you are a stakeholder, this is your proof that developers understand 36 | exactly how you want this feature to work. If you are a developer, this is your 37 | proof that the stakeholder expects you to implement this feature exactly in the 38 | way you're planning to implement it. 39 | 40 | So, as a developer your work is done as soon as you've made the ``ls`` 41 | command, and made it behave as described in the "Listing command" scenario. 42 | 43 | You've probably heard about this modern development practice called TDD, where 44 | you write tests for your code before, not after, the code. Well, BDD is like 45 | that, except that you don't need to come up with a test - your *scenarios* are 46 | your tests. That's exactly what Behat does! As you'll see, Behat is easy to 47 | learn, quick to use, and will put the fun back into your testing. 48 | 49 | Behaviour Driven Development 50 | ---------------------------- 51 | 52 | Once you're up and running with Behat, you can learn more about behaviour 53 | driven development via the following links. Though both tutorials are specific 54 | to Cucumber, Behat shares a lot with Cucumber and the philosophies are one 55 | and the same. 56 | 57 | * `Dan North's "What's in a Story?"`_ 58 | * `Cucumber's "Backgrounder"`_ 59 | 60 | .. note:: 61 | 62 | Behat was heavily inspired by Ruby's `Cucumber`_ project. Since v3.0, 63 | Behat is considered an official Cucumber implementation in PHP and is part 64 | of one big family of BDD tools. 65 | 66 | Documentation Contents 67 | ---------------------- 68 | 69 | .. toctree:: 70 | :maxdepth: 1 71 | 72 | user_guide/gherkin 73 | user_guide/features_scenarios 74 | user_guide/initialize 75 | user_guide/writing_scenarios 76 | user_guide/organizing 77 | user_guide/context 78 | user_guide/annotations 79 | user_guide/command_line_tool 80 | user_guide/configuration 81 | user_guide/integrations 82 | 83 | .. _`Cucumber`: https://cucumber.io/ 84 | .. _Dan North's "What's in a Story?": http://dannorth.net/whats-in-a-story 85 | .. _Cucumber's "Backgrounder": https://github.com/cucumber/cucumber/wiki/Cucumber-Backgrounder 86 | -------------------------------------------------------------------------------- /images/formatter-pretty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Behat/docs/ac2b628bcbe3972c9dca6fa67e49462a7f15dfa8/images/formatter-pretty.png -------------------------------------------------------------------------------- /images/formatter-progress.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Behat/docs/ac2b628bcbe3972c9dca6fa67e49462a7f15dfa8/images/formatter-progress.png -------------------------------------------------------------------------------- /index.rst: -------------------------------------------------------------------------------- 1 | .. 2 | The :orphan: tag tells sphinx not to emit a warning that the index page is not referenced from 3 | any automatic table-of-contents. 4 | 5 | :orphan: 6 | 7 | Behat - a collaborative specification tool 8 | ========================================== 9 | 10 | 11 | .. code-block:: gherkin 12 | 13 | Feature: Prove that software meets my business expectations 14 | In order to know when I have correctly implemented a feature 15 | As a PHP developer 16 | I want a tool to execute tests based on the agreed specifications 17 | 18 | Background: 19 | Given I have discussed a feature with the product team 20 | And we have agreed a set of requirements in plain language 21 | 22 | Scenario: Work is complete 23 | Given I have correctly implemented the feature 24 | When I run Behat with our specifications 25 | Then I should see that the software behaves as expected 26 | 27 | 28 | Behat is an open source `Behaviour Driven Development`_ tool for building software that 29 | meets your business expectations. 30 | 31 | It is the PHP implementation of `Cucumber`_, to execute specifications in **plain language** 32 | that **anyone on your team** can read. This improves **communication**, **collaboration** 33 | and **trust**, giving you the confidence to build the right system from the start and to 34 | maintain it over time. 35 | 36 | .. button:: 37 | :text: Read the quick start guide 38 | :link: /quick_start.html 39 | :class: btn btn-lg btn-primary btn-block mb-1 40 | 41 | .. button:: 42 | :text: Explore the documentation 43 | :link: /guides.html 44 | :class: btn btn-lg btn-primary btn-block mb-1 45 | 46 | 47 | Cover your whole application 48 | ---------------------------- 49 | 50 | Behat helps you to focus on business expectations and user-facing behaviour. This is 51 | very different to many test runners, where tests become tied to implementations and 52 | to individual layers of your stack. 53 | 54 | With Behat, your feature files can describe the whole behaviour of your application. 55 | You then have total freedom to decide how to implement the code that proves 56 | the application works as expected. 57 | 58 | This allows you to "mix and match" different testing approaches and technologies. 59 | Browser automation; HTTP API calls; running shell commands; communicating directly 60 | with your database, filesystem or PHP code: anything is possible. 61 | 62 | You can even use profiles, tags and suites to test the same features in different ways. 63 | For example, a quick regression test that directly calls your API handlers in PHP, 64 | or a more thorough test over HTTP when you've made bigger changes. It's all up to you! 65 | 66 | Built for PHP, for any framework 67 | -------------------------------- 68 | 69 | Behat is a well structured PHP library designed for use with any framework (or none). 70 | Internally, the codebase uses Symfony components, but you will find it easy to use in 71 | any PHP project. 72 | 73 | Behat provides flexible and developer-friendly interfaces for your tests, follows 74 | modern coding standards and scores high ratings in static analysis tools. 75 | 76 | And of course, all of `Behat's features are specified in plain language`_ and tested with Behat! 77 | 78 | 79 | Extensible to the core 80 | ---------------------- 81 | 82 | Behat was built from the ground up to be extensible. Almost every part of Behat's functionality 83 | can be enhanced or replaced through the extension system. 84 | 85 | There are a wide range of `extensions`_ already available. These include integrations with 86 | common PHP application frameworks, browser automation, test result reporters, data fixtures 87 | and many more. 88 | 89 | 90 | .. toctree:: 91 | :hidden: 92 | :maxdepth: 2 93 | 94 | quick_start 95 | guides 96 | cookbooks 97 | releases 98 | useful_resources 99 | community 100 | 101 | .. _`Behaviour Driven Development`: https://en.wikipedia.org/wiki/Behavior-driven_development 102 | .. _`Cucumber`: https://cucumber.io/ 103 | .. _`Behat's features are specified in plain language`: https://github.com/Behat/Behat/tree/master/features 104 | .. _`extensions`: https://github.com/search?o=desc&q=behat+extension+in%3Aname%2Cdescription&ref=searchresults&s=stars&type=Repositories&utf8=%E2%9C%93 105 | -------------------------------------------------------------------------------- /releases.rst: -------------------------------------------------------------------------------- 1 | Releases & version support 2 | ========================== 3 | 4 | Behat follows `Semantic Versioning`_ - breaking changes will only be made in a major release. 5 | 6 | Supported versions 7 | ------------------ 8 | 9 | ======= ========== ========== ============ ======================================================================= 10 | Major Released Bugfix EOL Security EOL 11 | ======= ========== ========== ============ ======================================================================= 12 | `v3.x`_ April 2014 See below See below `Changelog `__ 13 | ======= ========== ========== ============ ======================================================================= 14 | 15 | As a minimum, a major version series will receive: 16 | 17 | * Bugfixes for 12 months after the release of the next major. 18 | * Security fixes for 24 months after the release of the next major. 19 | 20 | Each time a new major version is released, the Behat maintainers will set End-of-Life dates for the previous version 21 | series. This will be based on the scale of the breaking changes, the complexity of supporting the older version, and the 22 | likely effort required for users and third-party extensions to upgrade. 23 | 24 | Bugfixes will usually only be applied to the most recent minor of each supported major version, unless they are 25 | particularly severe or have security implications. This will impact 26 | :ref:`support for End-of-Life PHP & Symfony versions`. 27 | 28 | Release timescales 29 | ------------------ 30 | 31 | There is no fixed schedule for releasing new major versions - but we will try to keep them to a frequency that is 32 | manageable for users. 33 | 34 | Minor versions 35 | ~~~~~~~~~~~~~~ 36 | 37 | Minor & patch versions will be released whenever there is something to release. These releases do not come with any 38 | specific support timescale, and we expect that users will upgrade to the next minor when it becomes available. 39 | 40 | Please bear in mind that this is free software, maintained by volunteers as a gift to users, and the license 41 | specifically explains that it is provided without warranty of any kind. 42 | 43 | 44 | Support for PHP and dependency versions 45 | --------------------------------------- 46 | 47 | Behat only supports current versions of PHP and third-party dependency packages (e.g. Symfony components). 48 | 49 | By "current", we mean: 50 | 51 | * PHP versions that are listed as receiving active support or security fixes 52 | on the `official php.net version support page`_. 53 | * Symfony versions that are listed as maintained or receiving security fixes on the `official Symfony releases page`_. 54 | 55 | Once a PHP or Symfony version reaches End of Life we will remove it from our composer.json and CI flows. 56 | 57 | .. note:: 58 | When we drop support for a PHP / dependency version we will highlight this in the CHANGELOG, but we will treat 59 | it as a minor release. Composer will automatically protect users from upgrading to a version that does not support 60 | their environment. Users running Behat as a ``.phar`` should review the release notes before downloading 61 | a new version. 62 | 63 | We will not ship bugfix releases for unsupported PHP / dependency versions, unless: 64 | 65 | * It fixes a security vulnerability within the security support period for a Behat major version. 66 | * An external contributor wishes to take on the work of backporting, including any changes required 67 | to get a green build in CI. 68 | 69 | End-of-Life versions 70 | -------------------- 71 | 72 | These behat series are no longer maintained and will not receive any further releases. We strongly recommend that users 73 | upgrade to a supported version as soon as possible. 74 | 75 | ======= ========== ============ ============ ===================================================================== 76 | Major Released Bugfix EOL Security EOL 77 | ======= ========== ============ ============ ===================================================================== 78 | `v2.x`_ July 2011 June 2015 June 2015 `Changelog `__ 79 | ======= ========== ============ ============ ===================================================================== 80 | 81 | 82 | .. _`Semantic Versioning`: http://semver.org/ 83 | .. _`official php.net version support page`: https://www.php.net/supported-versions.php 84 | .. _`official Symfony releases page`: https://symfony.com/releases 85 | .. _`v2.x`: https://github.com/Behat/Behat/releases?q=v2 86 | .. _`v3.x`: https://github.com/Behat/Behat/releases?q=v3 87 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | alabaster==1.0.0 2 | anyio==4.9.0 3 | babel==2.16.0 4 | certifi==2024.8.30 5 | charset-normalizer==3.4.0 6 | click==8.1.8 7 | colorama==0.4.6 8 | docutils==0.21.2 9 | h11==0.16.0 10 | idna==3.10 11 | imagesize==1.4.1 12 | Jinja2==3.1.6 13 | MarkupSafe==3.0.2 14 | packaging==24.1 15 | Pygments==2.18.0 16 | requests==2.32.3 17 | setuptools==78.1.1 18 | sniffio==1.3.1 19 | snowballstemmer==2.2.0 20 | Sphinx==8.1.3 21 | sphinx-autobuild==2024.10.3 22 | sphinx-copybutton==0.5.2 23 | sphinx-notfound-page==1.0.4 24 | sphinx_wagtail_theme==6.5.0 25 | sphinxcontrib-applehelp==2.0.0 26 | sphinxcontrib-devhelp==2.0.0 27 | sphinxcontrib-htmlhelp==2.1.0 28 | sphinxcontrib-jsmath==1.0.1 29 | sphinxcontrib-qthelp==2.0.0 30 | sphinxcontrib-serializinghtml==2.0.0 31 | starlette==0.46.1 32 | typing_extensions==4.13.1 33 | urllib3==2.2.3 34 | uvicorn==0.34.0 35 | watchfiles==1.0.4 36 | websockets==15.0.1 37 | wheel==0.44.0 38 | -------------------------------------------------------------------------------- /useful_resources.rst: -------------------------------------------------------------------------------- 1 | Useful Resources & Extensions 2 | ============================= 3 | 4 | Behat Extensions 5 | ---------------- 6 | 7 | There are a wide range of extensions already available. These include integrations with 8 | common PHP application frameworks, browser automation, test result reporters, data fixtures 9 | and many more. 10 | 11 | We do not currently maintain an official list of extensions - 12 | `most extensions can be found on GitHub`_. 13 | 14 | 15 | Integrating Behat with PHPStorm 16 | ------------------------------- 17 | 18 | More information on integrating Behat with PHPStorm can be found in this 19 | `blog post`_. 20 | 21 | Behat cheat sheet 22 | ----------------- 23 | 24 | An interesting `Behat and Mink cheat sheet`_ developed by `Jean-François Lépine`_ 25 | 26 | .. _`most extensions can be found on GitHub`: https://github.com/search?o=desc&q=behat+extension+in%3Aname%2Cdescription&ref=searchresults&s=stars&type=Repositories&utf8=%E2%9C%93 27 | .. _`blog post`: http://blog.jetbrains.com/phpstorm/2014/07/using-behat-in-phpstorm/ 28 | .. _`Behat and Mink cheat sheet`: http://blog.lepine.pro/images/2012-04-behat-cheat-sheet1.pdf 29 | .. _`Jean-François Lépine`: http://blog.lepine.pro 30 | -------------------------------------------------------------------------------- /user_guide/annotations.rst: -------------------------------------------------------------------------------- 1 | Annotations 2 | =========== 3 | 4 | Historically Behat used doc-block annotations instead of attributes to define steps, hooks and 5 | transformations in PHP contexts. These annotations are still available for now, and you can use them instead 6 | of PHP attributes in your projects - however they will likely be deprecated and removed in the future. 7 | 8 | Step Annotations 9 | ---------------- 10 | 11 | Here is an example of how you can define your steps using annotations: 12 | 13 | .. code-block:: php 14 | 15 | // features/bootstrap/FeatureContext.php 16 | 17 | use Behat\Behat\Context\Context; 18 | 19 | class FeatureContext implements Context 20 | { 21 | /* 22 | * @Given we have some context 23 | */ 24 | public function prepareContext() 25 | { 26 | // do something 27 | } 28 | 29 | /* 30 | * @When an :event occurs 31 | */ 32 | public function onEvent(string $event) 33 | { 34 | // do something 35 | } 36 | 37 | /* 38 | * @Then something should be done 39 | */ 40 | public function checkOutcomes() 41 | { 42 | // do something 43 | } 44 | } 45 | 46 | The pattern that you would include as an argument to the step attribute should be listed here 47 | after the annotation, separated by a space 48 | 49 | Hook Annotations 50 | ---------------- 51 | 52 | Here is an example of how you can define your hooks using annotations: 53 | 54 | .. code-block:: php 55 | 56 | // features/bootstrap/FeatureContext.php 57 | 58 | use Behat\Behat\Context\Context; 59 | use Behat\Testwork\Hook\Scope\BeforeSuiteScope; 60 | use Behat\Behat\Hook\Scope\AfterScenarioScope; 61 | 62 | class FeatureContext implements Context 63 | { 64 | /* 65 | * @BeforeSuite 66 | */ 67 | public static function prepare(BeforeSuiteScope $scope) 68 | { 69 | } 70 | 71 | /* 72 | * @AfterScenario @database 73 | */ 74 | public function cleanDB(AfterScenarioScope $scope) 75 | { 76 | } 77 | } 78 | 79 | Transformation Annotations 80 | -------------------------- 81 | 82 | Here is an example of how you can define your transformations using annotations: 83 | 84 | .. code-block:: php 85 | 86 | // features/bootstrap/FeatureContext.php 87 | 88 | use Behat\Behat\Context\Context; 89 | 90 | class FeatureContext implements Context 91 | { 92 | /* 93 | * @Transform /^(\d+)$/ 94 | */ 95 | public function castStringToNumber($string) 96 | { 97 | return intval($string); 98 | } 99 | } 100 | 101 | Existing code 102 | ------------- 103 | 104 | Even though annotations are still available, they will probably be deprecated and eventually removed in the future. 105 | Therefore, we do not recommend using annotations for new projects. If your current project uses annotations, we 106 | recommend that you refactor your code to use PHP attributes instead. This can be done manually but you should be able 107 | to use the search and replace capabilities of your IDE to do this in a more automated way. 108 | 109 | Alternatively you may want to use a tool like `Rector`_ which can do automated refactoring of your code. Rector 2.0 110 | includes a rule that allows you to automatically convert any doc-block annotations to the corresponding attributes. 111 | To use it for your Behat contexts, add the following option to your Rector configuration: 112 | 113 | .. code-block:: php 114 | 115 | ->withAttributesSets(behat: true) 116 | 117 | .. _`Rector`: https://github.com/rectorphp/rector 118 | 119 | -------------------------------------------------------------------------------- /user_guide/command_line_tool.rst: -------------------------------------------------------------------------------- 1 | Command Line Tool 2 | ================= 3 | 4 | This is a summary of the usage of Behat in the command line: 5 | 6 | .. code-block:: console 7 | 8 | Usage: 9 | behat [options] [--] [] 10 | 11 | Arguments: 12 | paths 13 | Optional path(s) to execute. Could be: 14 | - a dir (features/) 15 | - a feature (*.feature) 16 | - a scenario at specific line (*.feature:10). 17 | - all scenarios at or after a specific line 18 | (*.feature:10-*). 19 | - all scenarios at a line within a specific range 20 | (*.feature:10-20). 21 | - a scenarios list file (*.scenarios). 22 | 23 | Options: 24 | -s, --suite=SUITE 25 | Only execute a specific suite. 26 | -f, --format=FORMAT 27 | How to format tests output. pretty is default. 28 | Available formats are: 29 | - pretty: Prints the feature as is. 30 | - progress: Prints one character per step. 31 | - junit: Outputs the failures in JUnit compatible 32 | files. 33 | You can use multiple formats at the same time. 34 | (multiple values allowed) 35 | -o, --out=OUT 36 | Write format output to a file/directory instead of 37 | STDOUT (output_path). You can also provide different 38 | outputs to multiple formats. This option is mandatory 39 | for the junit formatter. (multiple values allowed) 40 | --format-settings=FORMAT-SETTINGS 41 | Set formatters parameters using json object. 42 | Keys are parameter names, values are values. (multiple 43 | values allowed) 44 | --print-absolute-paths 45 | Print absolute paths in output 46 | --editor-url=EDITOR-URL 47 | URL template for opening files in an editor 48 | --init 49 | Initialize all registered test suites. 50 | --lang=LANG 51 | Print output in particular language. 52 | --name=NAME 53 | Only execute the feature elements which match part 54 | of the given name or regex. (multiple values allowed) 55 | --tags=TAGS 56 | Only execute the features or scenarios with tags 57 | matching tag filter expression. (multiple values 58 | allowed) 59 | --role=ROLE 60 | Only execute the features with actor role matching 61 | a wildcard. 62 | --narrative=NARRATIVE 63 | Only execute the features with actor description 64 | matching a regex. 65 | --story-syntax 66 | Print *.feature example. 67 | Use --lang to see specific language. 68 | -d, --definitions=DEFINITIONS 69 | Print all available step definitions: 70 | - use --definitions l to just list definition 71 | expressions. 72 | - use --definitions i to show definitions with extended 73 | info. 74 | - use --definitions 'needle' to find specific 75 | definitions. 76 | Use --lang to see definitions in specific language. 77 | --snippets-for[=SNIPPETS-FOR] 78 | Specifies which context class to generate snippets for. 79 | --snippets-type=SNIPPETS-TYPE 80 | Specifies which type of snippets (turnip, regex) to 81 | generate. 82 | --append-snippets 83 | Appends snippets for undefined steps into main context. 84 | --no-snippets 85 | Do not print snippets for undefined steps after stats. 86 | --strict 87 | Passes only if all tests are explicitly passing. 88 | --print-unused-definitions 89 | Reports definitions that were never used. 90 | --order=ORDER 91 | Set an order in which to execute the specifications 92 | (this will result in slower feedback). 93 | --rerun 94 | Re-run scenarios that failed during last execution, 95 | or run everything if there were no failures. 96 | --rerun-only 97 | Re-run scenarios that failed during last execution, 98 | or exit if there were no failures. 99 | --stop-on-failure 100 | Stop processing on first failed scenario. 101 | --dry-run 102 | Invokes formatters without executing the tests and 103 | hooks. 104 | --allow-no-tests 105 | Will not fail if no specifications are found. 106 | -p, --profile=PROFILE 107 | Specify config profile to use. 108 | -c, --config=CONFIG 109 | Specify config file to use. 110 | -v, --verbose[=VERBOSE] 111 | Increase verbosity of exceptions. 112 | Use -vv or --verbose=2 to display backtraces in 113 | addition to exceptions. 114 | -h, --help 115 | Display this help message. 116 | --convert-config 117 | Convert the configuration to the PHP format. 118 | --config-reference 119 | Display the configuration reference. 120 | --debug 121 | Provide debugging information about current 122 | environment. 123 | -V, --version 124 | Display version. 125 | -n, --no-interaction 126 | Do not ask any interactive question. 127 | --colors 128 | Force ANSI color in the output. By default color 129 | support is guessed based on your platform and the 130 | output if not specified. 131 | --no-colors 132 | Force no ANSI color in the output. 133 | --xdebug 134 | Allow Xdebug to run. 135 | 136 | .. toctree:: 137 | :maxdepth: 2 138 | 139 | command_line_tool/identifying 140 | command_line_tool/formatting 141 | command_line_tool/informative_output 142 | 143 | -------------------------------------------------------------------------------- /user_guide/command_line_tool/formatting.rst: -------------------------------------------------------------------------------- 1 | Output Formatters 2 | ================= 3 | 4 | Behat supports different ways of printing output information. Output printers 5 | in Behat are called *formats* or *formatters*. You can tell Behat to 6 | run with a specific formatter by providing the ``--format`` option: 7 | 8 | .. code-block:: bash 9 | 10 | $ behat --format progress 11 | 12 | .. note:: 13 | 14 | The default formatter is ``pretty``. 15 | 16 | Behat supports 3 formatters out of the box: 17 | 18 | * ``pretty`` - prints the feature as is, with the full text of each step. 19 | 20 | .. image:: /images/formatter-pretty.png 21 | :align: center 22 | 23 | * ``progress`` - prints one character per step: 24 | 25 | .. image:: /images/formatter-progress.png 26 | :align: center 27 | 28 | * ``junit`` - prints the output to xml files in the standard junit.xml format 29 | 30 | If you don't want to print output to the console, you can tell Behat 31 | to print output to a file instead of ``STDOUT`` with the ``--out`` option: 32 | 33 | .. code-block:: bash 34 | 35 | $ behat --format pretty --out report.txt 36 | 37 | .. note:: 38 | 39 | Some formatters, like ``junit``, always require the ``--out`` option to be 40 | specified. The ``junit`` formatter generates ``*.xml`` files for every 41 | suite, so it needs a destination directory to put these XML files into. 42 | 43 | Also, you can specify multiple formats to be used by Behat using multiple --format options: 44 | 45 | .. code-block:: bash 46 | 47 | $ behat --format pretty --format progress 48 | 49 | In this case, default output will be used as output for both formatters. But if you want 50 | them to use different ones - specify them with ``--out``: 51 | 52 | .. code-block:: bash 53 | 54 | $ behat -f pretty -o ~/pretty.out -f progress -o std 55 | -f junit -o xml 56 | 57 | In this case, output of pretty formatter will be written to ``~/pretty.out`` file, output of junit 58 | formatter will be written to ``xml`` folder and progress formatter will just print to console. 59 | 60 | Behat tries hard to identify if your terminal supports colors or not, but 61 | sometimes it still fails. In such cases, you can force Behat to 62 | use colors (or not) with the options ``--colors`` or ``--no-colors``, 63 | respectively: 64 | 65 | .. code-block:: bash 66 | 67 | $ behat --no-colors 68 | 69 | Format Options 70 | -------------- 71 | 72 | The formatters can be configured with some options. The following options are available for 73 | all formatters: 74 | 75 | * ``output_verbosity`` indicates the level of detail of the output. Use one of the ``OutputFactory::*`` constants 76 | * ``output_path`` indicates the path where the output should be saved. Equivalent to the ``--out`` command 77 | line option. Should be a file or folder, depending on the formatter. 78 | * ``output_decorate`` determines whether the output generated by Behat is "decorated" with formatting, 79 | such as colors, bold text, or other visual enhancements. Should be a boolean, defaults to true. 80 | * ``output_styles`` can be used to override the default styles used by Behat to display the different output 81 | elements. It should be an array where the key is the style that needs to be overridden and which points to an array of 82 | three values. The first one is the foreground color, the second one the background color and the third one an array of 83 | optional styles. 84 | 85 | The styles available for redefinition are: 86 | 87 | * ``keyword`` style of Gherkin keywords 88 | * ``stdout`` style of stdout output 89 | * ``exception`` style of exceptions 90 | * ``undefined`` style of undefined steps 91 | * ``pending`` style of pending steps 92 | * ``pending_param`` style of pending step params 93 | * ``failed`` style of failed steps 94 | * ``failed_param`` style of failed step params 95 | * ``passed`` style of passed steps 96 | * ``passed_param`` style of passed steo params 97 | * ``skipped`` style of skipped steps 98 | * ``skipped_param`` style of skipped step params 99 | * ``comment`` style of comments 100 | * ``tag`` style of scenario/feature tags 101 | 102 | Available colors for first two arguments (``fg`` and ``bg``) are: ``black``, ``red``, ``green``, ``yellow``, 103 | ``blue``, ``magenta``, ``cyan`` and ``white``. 104 | 105 | Available optional styles are: ``bold``, ``underscore``, ``blink``, ``reverse`` and ``conceal`` 106 | 107 | Pretty formatter 108 | ^^^^^^^^^^^^^^^^ 109 | 110 | The following options are specific to the Pretty formatter: 111 | 112 | * ``timer`` show time and memory usage at the end of the test run. Boolean, defaults to true. 113 | * ``expand`` print each example of a scenario outline separately. Boolean, defaults to false. 114 | * ``paths`` display the file path and line number for each scenario and the context file and method for each step. 115 | Boolean, defaults to true. 116 | * ``multiline`` print out PyStrings and TableNodes in full. Boolean, defaults to true. 117 | * ``showOutput`` show the test stdout output as part of the formatter output. Should be one of the 118 | ``ShowOutputOption`` enum values, defaults to ``ShowOutputOption::Yes``. 119 | 120 | Progress formatter 121 | ^^^^^^^^^^^^^^^^^^ 122 | 123 | The following options are specific to the Progress formatter: 124 | 125 | * ``timer`` show time and memory usage at the end of the test run. Boolean, defaults to true. 126 | * ``showOutput`` show the test stdout output as part of the formatter output. Should be one of the 127 | ``ShowOutputOption`` enum values, defaults to ``ShowOutputOption::InSummary``. 128 | 129 | Setting format options 130 | ^^^^^^^^^^^^^^^^^^^^^^ 131 | 132 | Format options can be set using the ``withFormatter()`` function of the ``Profile`` PHP config class. For example: 133 | 134 | .. code-block:: php 135 | 136 | use Behat\Config\Config; 137 | use Behat\Config\Profile; 138 | use Behat\Config\Formatter\PrettyFormatter; 139 | 140 | $profile = (new Profile('default')) 141 | ->withFormatter((new PrettyFormatter(paths: false)) 142 | ->withOutputStyles([ 143 | 'comment' => [ 144 | 'black', 'white', 145 | ['underscore', 'bold'] 146 | ] 147 | ]) 148 | ) 149 | ; 150 | 151 | return (new Config())->withProfile($profile); 152 | 153 | These options can also be set on the command line by using the 154 | ``--format-setting`` option which accepts a json object with this configuration. For example: 155 | 156 | .. code-block:: bash 157 | 158 | $ behat --format-settings='{\"paths\": false}' 159 | 160 | Disabling a formatter 161 | ^^^^^^^^^^^^^^^^^^^^^ 162 | 163 | You can disable a formatter so that it won't be available by using the ``disableFormatter()`` function of the 164 | ``Profile`` PHP config class. For example: 165 | 166 | .. code-block:: php 167 | 168 | use Behat\Config\Config; 169 | use Behat\Config\Profile; 170 | use Behat\Config\Formatter\PrettyFormatter; 171 | 172 | $profile = (new Profile('default')) 173 | ->disableFormatter(PrettyFormatter::NAME) 174 | ; 175 | 176 | return (new Config())->withProfile($profile); 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | -------------------------------------------------------------------------------- /user_guide/command_line_tool/identifying.rst: -------------------------------------------------------------------------------- 1 | Identifying Tests 2 | ================= 3 | 4 | .. _user-guide--command-line-tool--identifying-tests--by-suite: 5 | 6 | By Suite 7 | -------- 8 | 9 | By default, when you run Behat it will execute all registered suites 10 | one-by-one. If you want to run a single suite instead, use the ``--suite`` 11 | option: 12 | 13 | .. code-block:: bash 14 | 15 | $ vendor/bin/behat --suite=web_features 16 | -------------------------------------------------------------------------------- /user_guide/command_line_tool/informative_output.rst: -------------------------------------------------------------------------------- 1 | Informative Output 2 | ================== 3 | 4 | .. _user-guide--command-line-tool--informative-output--print-definitions: 5 | 6 | Print Definitions 7 | ----------------- 8 | 9 | As your set of features will grow, there's a good chance that the amount of 10 | different steps that you'll have at your disposal to describe new scenarios will also grow. 11 | 12 | Behat provides a command line option ``--definitions`` or simply ``-d`` to easily browse definitions 13 | in order to reuse them or adapt them (introducing new placeholders for example). 14 | 15 | For example, when using the Mink context provided by the Mink extension, you'll have access to its 16 | step dictionary by running: 17 | 18 | .. code-block:: console 19 | 20 | $ behat -di 21 | web_features | Given /^(?:|I )am on (?:|the )homepage$/ 22 | | Opens homepage. 23 | | at `Behat\MinkExtension\Context\MinkContext::iAmOnHomepage()` 24 | 25 | web_features | When /^(?:|I )go to (?:|the )homepage$/ 26 | | Opens homepage. 27 | | at `Behat\MinkExtension\Context\MinkContext::iAmOnHomepage()` 28 | 29 | web_features | Given /^(?:|I )am on "(?P[^"]+)"$/ 30 | | Opens specified page. 31 | | at `Behat\MinkExtension\Context\MinkContext::visit()` 32 | 33 | # ... 34 | 35 | or, for a shorter output: 36 | 37 | .. code-block:: console 38 | 39 | $ behat -dl 40 | web_features | Given /^(?:|I )am on (?:|the )homepage$/ 41 | web_features | When /^(?:|I )go to (?:|the )homepage$/ 42 | web_features | Given /^(?:|I )am on "(?P[^"]+)"$/ 43 | web_features | When /^(?:|I )go to "(?P[^"]+)"$/ 44 | web_features | When /^(?:|I )reload the page$/ 45 | web_features | When /^(?:|I )move backward one page$/ 46 | web_features | When /^(?:|I )move forward one page$/ 47 | # ... 48 | 49 | You can also search for a specific pattern by running: 50 | 51 | .. code-block:: console 52 | 53 | $ behat --definitions="field" (or simply behat -dfield) 54 | web_features | When /^(?:|I )fill in "(?P(?:[^"]|\\")*)" with "(?P(?:[^"]|\\")*)"$/ 55 | | Fills in form field with specified id|name|label|value. 56 | | at `Behat\MinkExtension\Context\MinkContext::fillField()` 57 | 58 | web_features | When /^(?:|I )fill in "(?P(?:[^"]|\\")*)" with:$/ 59 | | Fills in form field with specified id|name|label|value. 60 | | at `Behat\MinkExtension\Context\MinkContext::fillField()` 61 | 62 | #... 63 | 64 | That's it, you can now search and browse your whole step dictionary. 65 | 66 | Print Unused Definitions 67 | ------------------------ 68 | 69 | If your project is large, you may end up with definitions which are no longer used. If you want to print a list of 70 | these unused definitions you can use the ``--print-unused-definitions`` command line flag. With this flag you will see 71 | output similar to this: 72 | 73 | .. code-block:: console 74 | 75 | $ behat --print-unused-definitions 76 | ... 77 | --- 1 unused definition: 78 | 79 | [Then|*] I call a step not used in any feature 80 | `FeatureContext::stepNotUsedInAnyFeature()` 81 | 82 | This can also be set for each profile using the PHP configuration: 83 | 84 | .. code-block:: php 85 | 86 | use Behat\Config\Config; 87 | use Behat\Config\Profile; 88 | 89 | return (new Config()) 90 | ->withProfile( 91 | (new Profile('default')) 92 | ->withPrintUnusedDefinitions() 93 | ) 94 | ; 95 | 96 | 97 | -------------------------------------------------------------------------------- /user_guide/configuration.rst: -------------------------------------------------------------------------------- 1 | Configuration 2 | ============= 3 | 4 | Behat has a very powerful configuration system based on ``PHP`` configuration files and 5 | profiles. 6 | 7 | .. toctree:: 8 | :maxdepth: 2 9 | 10 | configuration/suites.rst 11 | configuration/printing_paths.rst 12 | configuration/yaml_configuration.rst 13 | 14 | ``behat.php`` 15 | ------------- 16 | 17 | All configuration happens inside a single configuration file in the ``PHP`` or the ``YAML`` 18 | format. By default, Behat loads the configuration from the first file matching: 19 | 20 | #. ``behat.yaml`` or ``behat.yml`` 21 | #. ``behat.yaml.dist`` or ``behat.yml.dist`` 22 | #. ``behat.dist.yaml`` or ``behat.dist.yml`` 23 | #. ``behat.php`` 24 | #. ``behat.dist.php`` 25 | #. ``config/behat.yaml`` or ``config/behat.yml`` 26 | #. ``config/behat.yaml.dist`` or ``config/behat.yml.dist`` 27 | #. ``config/behat.dist.yaml`` or ``config/behat.dist.yml`` 28 | #. ``config/behat.php`` 29 | #. ``config/behat.dist.php`` 30 | 31 | You can also tell Behat where your config file is with the ``--config`` option: 32 | 33 | .. code-block:: bash 34 | 35 | $ behat --config custom-config.php 36 | 37 | All configuration parameters in that file are defined under a profile name root 38 | (``default:`` for example). A profile is just a custom name you can use to 39 | quickly switch testing configuration by using the ``--profile`` option when 40 | executing your feature suite. 41 | 42 | The default profile is always ``default``. All other profiles inherit 43 | parameters from the ``default`` profile. If you only need one profile, define 44 | all of your parameters under the ``default`` profile: 45 | 46 | .. code-block:: php 47 | 48 | withProfile( 55 | new Profile('default') 56 | //... 57 | ) 58 | ; 59 | 60 | Overriding ``default`` params 61 | ----------------------------- 62 | 63 | Each profile is an extension of the ``default`` profile. This means you can 64 | define a new profile that overrides configuration parameters defined in the 65 | ``default`` profile. 66 | 67 | Let's assume we have a ``default`` profile as such: 68 | 69 | .. code-block:: php 70 | 71 | withFilter(new TagFilter('@runthisonlyondefault')) 80 | ; 81 | 82 | return new Config() 83 | ->withProfile( 84 | new Profile('default') 85 | ->withSuite($defaultSuite) 86 | ) 87 | ; 88 | 89 | Now we want a profile that changes the tag which is to be run in the default 90 | suite. We can add the profile and just override: 91 | 92 | .. code-block:: php 93 | 94 | withFilter(new TagFilter('@runthisonlyondefault')) 103 | ; 104 | 105 | $profile1DefaultSuite = new Suite('default') 106 | ->withFilter(new TagFilter('@runthisonlyonprofile1')) 107 | ; 108 | 109 | return new Config() 110 | ->withProfile( 111 | new Profile('default') 112 | ->withSuite($defaultSuite) 113 | ) 114 | ->withProfile( 115 | new Profile('profile1') 116 | ->withSuite($profile1DefaultSuite) 117 | ) 118 | ; 119 | 120 | Or maybe we want to unset the tag filter for a profile: 121 | 122 | .. code-block:: php 123 | 124 | withFilter(new TagFilter('@runthisonlyondefault')) 133 | ; 134 | 135 | $profile1DefaultSuite = new Suite('default', ['filters' => null]); 136 | 137 | return new Config() 138 | ->withProfile( 139 | new Profile('default') 140 | ->withSuite($defaultSuite) 141 | ) 142 | ->withProfile( 143 | new Profile('profile1') 144 | ->withSuite($profile1DefaultSuite) 145 | ) 146 | ; 147 | 148 | Importing Config 149 | ---------------- 150 | 151 | The ``import`` methods can be used to merge multiple configuration files in to 152 | one loaded config in Behat, using the following syntax: 153 | 154 | .. code-block:: php 155 | 156 | import([ 162 | 'config/base.behat.php', 163 | 'config/ci.behat.php', 164 | ]) 165 | ; 166 | 167 | All files from the ``import`` method will be loaded by Behat and merged, in 168 | the listed order, into your ``behat.php`` config. This is especially useful 169 | when you want to tweak configuration slightly between local development and 170 | on Continuous Integration environments by using partial configuration files. 171 | 172 | This allows configuration files listed in the ``import`` method to override 173 | configuration values for previously listed files. 174 | 175 | Global profile configuration 176 | ---------------------------- 177 | 178 | You can set some global configuration in your profile configuration: 179 | 180 | .. code-block:: php 181 | 182 | withProfile( 188 | new Profile('default', [ 189 | 'testers' => [ 190 | // these are the default values 191 | 'stop_on_failure' => false, 192 | 'strict' => false, 193 | ], 194 | ]) 195 | ) 196 | ; 197 | 198 | Combining the fact that you can override the default profile, you can change the configuration per profile: 199 | 200 | .. code-block:: php 201 | 202 | withProfile( 208 | new Profile('default', [ 209 | 'testers' => [ 210 | 'stop_on_failure' => true, 211 | 'strict' => false, 212 | ], 213 | ]) 214 | ) 215 | ->withProfile( 216 | new Profile('ci', [ 217 | 'testers' => [ 218 | 'stop_on_failure' => false, 219 | 'strict' => true, 220 | ], 221 | ]) 222 | ) 223 | ; 224 | 225 | This way, with the default profile behat will stop on failure and won't be 226 | strict, but will not stop and will be strict if the CI profile is selected. 227 | 228 | You can force ``--stop-on-failure`` and ``--strict`` via CLI options to override 229 | configuration values. 230 | 231 | Environment Variable - BEHAT_PARAMS 232 | ----------------------------------- 233 | 234 | If you want to set up configurable Behat settings, use the ``BEHAT_PARAMS`` 235 | environment variable: 236 | 237 | .. code-block:: bash 238 | 239 | export BEHAT_PARAMS='{"extensions" : {"Behat\\MinkExtension" : {"base_url" : "https://www.example.com/"}}}' 240 | 241 | You can set any value for any option that is available in a ``behat.php`` file. 242 | Just provide options in *JSON* format. Behat will use those options as defaults. 243 | You can always override them with the settings in the project ``behat.php`` 244 | file (it has higher priority). 245 | 246 | .. tip:: 247 | 248 | You can convert the PHP configuration to JSON using the ``toArray`` method. 249 | 250 | .. code-block:: php 251 | 252 | withProfile( 260 | new Profile('default') 261 | ->withFilter(new TagFilter('~@wip')) 262 | ) 263 | ; 264 | 265 | var_dump(json_encode($config->toArray())); 266 | 267 | .. code-block:: json 268 | 269 | {"default":{"gherkin":{"filters":{"tags":"~@wip"}}}} 270 | 271 | .. tip:: 272 | 273 | In order to specify a parameter in an environment variable, the value 274 | *must not* exist in your ``behat.php`` 275 | 276 | .. tip:: 277 | 278 | NOTE: In Behat 2.x this variable was in *URL* format. It has been changed 279 | to use *JSON* format. 280 | 281 | Global Filters 282 | -------------- 283 | 284 | While it is possible to specify filters as part of suite configuration, 285 | sometimes you will want to exclude certain scenarios across the suite, 286 | with the option to override the filters at the command line. 287 | 288 | This is achieved by specifying the filter in the ``gherkin`` configuration: 289 | 290 | .. code-block:: php 291 | 292 | withProfile( 300 | new Profile('default') 301 | ->withFilter(new TagFilter('~@wip')) 302 | ) 303 | ; 304 | 305 | In this instance, scenarios tagged as ``@wip`` will be ignored unless the CLI command is run with a custom filter, e.g.: 306 | 307 | .. code-block:: bash 308 | 309 | vendor/bin/behat --tags=wip 310 | 311 | Custom Autoloading 312 | ------------------ 313 | 314 | Sometimes you will need to place your ``features`` folder somewhere other than the 315 | default location (e.g. ``app/features``). All you need to do is specify the path 316 | you want to autoload via ``behat.php``: 317 | 318 | .. code-block:: php 319 | 320 | withProfile( 327 | new Profile('default', [ 328 | 'autoload' => [ 329 | '' => '%paths.base%/app/features/bootstrap', 330 | ], 331 | ]) 332 | ) 333 | ; 334 | 335 | If you wish to namespace your features (for example: to be PSR-1 compliant) 336 | you will need to add the namespace to the classes and also tell behat where 337 | to load them. Here ``contexts`` is an array of classes: 338 | 339 | .. code-block:: php 340 | 341 | [ 349 | '' => '%paths.base%/app/features/bootstrap', 350 | ], 351 | ]); 352 | 353 | $defaultProfile->withSuite( 354 | new Suite('default') 355 | ->withContexts('My\Application\Namespace\Bootstrap\FeatureContext') 356 | ); 357 | 358 | return new Config() 359 | ->withProfile($defaultProfile) 360 | ; 361 | 362 | Using ``behat.php`` to autoload will only allow for ``PSR-0``. 363 | You can also use ``composer.json`` to autoload, which will also allow for ``PSR-4``: 364 | 365 | .. code-block:: json 366 | 367 | { 368 | "autoload-dev": { 369 | "psr-4": { 370 | "My\\Application\\Namespace\\Bootstrap\\": "app/features/bootstrap" 371 | } 372 | } 373 | } 374 | 375 | If you add this to your ``composer.json`` file, then you won't need to specify autoloading in 376 | your ``behat.php`` file: 377 | 378 | .. code-block:: php 379 | 380 | withSuite( 388 | new Suite('default') 389 | ->withContexts('My\Application\Namespace\Bootstrap\FeatureContext') 390 | ) 391 | ; 392 | 393 | return new Config() 394 | ->withProfile($defaultProfile) 395 | ; 396 | 397 | Formatters 398 | ---------- 399 | 400 | Default formatters can be enabled by specifying them in the profile. 401 | 402 | .. code-block:: php 403 | 404 | withProfile( 412 | new Profile('default') 413 | ->withFormatter( 414 | new PrettyFormatter() 415 | ) 416 | ) 417 | ; 418 | 419 | Extensions 420 | ---------- 421 | 422 | Extensions can be configured like this: 423 | 424 | .. code-block:: php 425 | 426 | withProfile( 434 | new Profile('default') 435 | >withExtension( 436 | new Extension('Behat\MinkExtension', [ 437 | 'base_url' => 'http://www.example.com', 438 | 'selenium2' => null, 439 | ]) 440 | ) 441 | ) 442 | ; 443 | -------------------------------------------------------------------------------- /user_guide/configuration/printing_paths.rst: -------------------------------------------------------------------------------- 1 | Printing paths 2 | ============== 3 | 4 | When showing test results or failures Behat will print the path to the related files. By default 5 | the path will be relative to the current directory. This behaviour can be modified through 6 | configuration or on the command line: 7 | 8 | ``Print Absolute Paths``: if this is set to true, Behat will print the full absolute path instead. 9 | 10 | ``Editor URL``: if this is set, Behat will surround each path with a link that, when clicked, will open 11 | the file in your IDE. This parameter is a template which can include placeholders that Behat will replace 12 | at run time. The available placeholders are: 13 | 14 | - ``{relPath}``: Relative path to the file 15 | - ``{absPath}``: Absolute path to the file 16 | - ``{line}}``: Line number (note that this is optional and may not be available in some cases) 17 | 18 | The specific value that you need to use will depend on your IDE and other factors (for example, it may 19 | need to be different if you are running your code within a Docker container). Some examples could be: 20 | 21 | - For PhpStorm: ``phpstorm://open?file={relPath}&line={line}`` 22 | - For VS Code: ``vscode://file/{absPath}:{line}`` 23 | 24 | .. note:: 25 | 26 | The path style can be different for the visible path and the one used in the editor URL. For example you 27 | might have a visible relative path and use an absolute path in the URL. 28 | 29 | These options can be set using the ``withPathOptions()`` function of the ``Profile`` config object, for example: 30 | 31 | .. code-block:: php 32 | 33 | withProfile((new Profile('default')) 41 | ->withPathOptions( 42 | printAbsolutePaths: true, 43 | editorUrl: 'phpstorm://open?file={relPath}&line={line}' 44 | )); 45 | 46 | They can also be set as command line options (notice that the editor URL will usually need to be quoted): 47 | 48 | .. code-block:: bash 49 | 50 | behat --print-absolute-paths --editor-url="phpstorm://open?file={relPath}&line={line}" 51 | -------------------------------------------------------------------------------- /user_guide/configuration/suites.rst: -------------------------------------------------------------------------------- 1 | Configuring Test Suites 2 | ======================= 3 | 4 | We already talked about configuring multiple contexts for a single test 5 | suite in a :doc:`previous chapter `. Now it is 6 | time to talk about test suites themselves. A test suite represents a group of 7 | concrete features together with the information on how to test them. 8 | 9 | With suites you can configure Behat to test different kinds of features 10 | using different kinds of contexts and doing so in one run. Test suites are 11 | really powerful and ``behat.yml`` makes them that much more powerful: 12 | 13 | .. code-block:: php 14 | 15 | withSuite( 28 | new Suite('core_features') 29 | ->withPaths('%paths.base%/features/core') 30 | ->withContexts(CoreDomainContext::class) 31 | ) 32 | ->withSuite( 33 | new Suite('user_features') 34 | ->withFilter(new RoleFilter('user')) 35 | ->withPaths('%paths.base%/features/web') 36 | ->withContexts(UserContext::class) 37 | ) 38 | ->withSuite( 39 | new Suite('admin_features') 40 | ->withFilter(new RoleFilter('admin')) 41 | ->withPaths('%paths.base%/features/web') 42 | ->withContexts(AdminContext::class) 43 | ) 44 | ; 45 | 46 | return new Config() 47 | ->withProfile($profile) 48 | ; 49 | 50 | .. note:: 51 | 52 | On PHP < 8.4, you need to wrap new invocations with parentheses before calling config object methods. 53 | 54 | .. code-block:: php 55 | 56 | // ... 57 | // $profile = new Profile('default') 58 | $profile = (new Profile('default')) 59 | ->withSuite( 60 | // new Suite('core_features') 61 | (new Suite('core_features')) 62 | ->withPaths('%paths.base%/features/core') 63 | ->withContexts(CoreDomainContext::class) 64 | ) 65 | // ... 66 | 67 | Suite Paths 68 | ----------- 69 | 70 | One of the most obvious settings of the suites is the ``paths`` 71 | configuration: 72 | 73 | .. code-block:: php 74 | 75 | withSuite( 84 | new Suite('core_features') 85 | ->withPaths( 86 | '%paths.base%/features', 87 | '%paths.base%/test/features', 88 | '%paths.base%/vendor/.../features', 89 | ) 90 | ) 91 | ; 92 | 93 | return (new Config()) 94 | ->withProfile($profile) 95 | ; 96 | 97 | As you might imagine, this option tells Behat where to search for test features. 98 | You could, for example, tell Behat to look into the 99 | ``features/web`` folder for features and test them with ``WebContext``: 100 | 101 | .. code-block:: php 102 | 103 | withSuite( 113 | new Suite('web_features') 114 | ->withPaths('%paths.base%/features/web') 115 | ->withContexts(WebContext::class) 116 | ) 117 | ; 118 | 119 | return new Config() 120 | ->withProfile($profile) 121 | ; 122 | 123 | You then might want to also describe some API-specific features in 124 | ``features/api`` and test them with an API-specific ``ApiContext``. Easy: 125 | 126 | .. code-block:: php 127 | 128 | withSuite( 139 | new Suite('web_features') 140 | ->withPaths('%paths.base%/features/web') 141 | ->withContexts(WebContext::class) 142 | ) 143 | ->withSuite( 144 | new Suite('api_features') 145 | ->withPaths('%paths.base%/features/api') 146 | ->withContexts(ApiContext::class) 147 | ) 148 | ; 149 | 150 | return new Config() 151 | ->withProfile($profile) 152 | ; 153 | 154 | This will cause Behat to: 155 | 156 | #. Find all features inside ``features/web`` and test them using your 157 | ``WebContext``. 158 | 159 | #. Find all features inside ``features/api`` and test them using your 160 | ``ApiContext``. 161 | 162 | .. note:: 163 | 164 | ``%paths.base%`` is a special variable in ``behat.yml`` that refers 165 | to the folder in which ``behat.yml`` is stored. When using it, or 166 | any other percent-encased variable, it has to be put in quotes. 167 | 168 | Path-based suites are an easy way to test highly-modular applications 169 | where features are delivered by highly decoupled components. With suites 170 | you can test all of them together. 171 | 172 | Suite Filters 173 | ------------- 174 | 175 | In addition to being able to run features from different directories, 176 | we can run scenarios from the same directory, but filtered by specific 177 | criteria. The Gherkin parser comes bundled with a set of cool filters 178 | such as *tags* and *name* filters. You can use these filters to run 179 | features with specific tag (or name) in specific contexts: 180 | 181 | .. code-block:: php 182 | 183 | withSuite( 195 | new Suite('web_features') 196 | ->withFilter(new TagFilter('@web')) 197 | ->withPaths('%paths.base%/features') 198 | ->withContexts(WebContext::class) 199 | ) 200 | ->withSuite( 201 | new Suite('api_features') 202 | ->withFilter(new TagFilter('@api')) 203 | ->withPaths('%paths.base%/features') 204 | ->withContexts(ApiContext::class) 205 | ) 206 | ; 207 | 208 | return new Config() 209 | ->withProfile($profile) 210 | ; 211 | 212 | This configuration will tell Behat to run features and scenarios 213 | tagged as ``@web`` in ``WebContext`` and features and scenarios 214 | tagged as ``@api`` in ``ApiContext``. Even if they all are stored 215 | in the same folder. How cool is that? But it gets even better, 216 | because Gherkin 4+ (used in Behat 3+) added a very special *role* 217 | filter. That means, you can now have nice actor-based suites: 218 | 219 | .. code-block:: php 220 | 221 | withSuite( 233 | new Suite('user_features') 234 | ->withFilter(new RoleFilter('user')) 235 | ->withPaths('%paths.base%/features') 236 | ->withContexts(UserContext::class) 237 | ) 238 | ->withSuite( 239 | new Suite('api_features') 240 | ->withFilter(new RoleFilter('admin')) 241 | ->withPaths('%paths.base%/features') 242 | ->withContexts(AdminContext::class) 243 | ) 244 | ; 245 | 246 | return (new Config()) 247 | ->withProfile($profile) 248 | ; 249 | 250 | A Role filter takes a look into the feature description block: 251 | 252 | .. code-block:: gherkin 253 | 254 | Feature: Registering users 255 | In order to help more people use our system 256 | As an admin 257 | I need to be able to register more users 258 | 259 | It looks for a ``As a ...`` or ``As an ...`` pattern and guesses its 260 | actor from it. It then filters features that do not have the expected 261 | actor from the set. In the case of our example, it basically means that 262 | features described from the perspective of the *user* actor will 263 | be tested in ``UserContext`` and features described from the 264 | perspective of the *admin* actor will be tested in ``AdminContext``. 265 | Even if they are in the same folder. 266 | 267 | While it is possible to specify filters as part of suite configuration, 268 | sometimes you will want to exclude certain scenarios across the suite, with the 269 | option to override the filters at the command line. 270 | 271 | This is achieved by specifying the filter in the gherkin configuration: 272 | 273 | .. code-block:: php 274 | 275 | withFilter(new TagFilter('~@wip')) 284 | ; 285 | 286 | return new Config() 287 | ->withProfile($profile) 288 | ; 289 | 290 | In this instance, scenarios tagged as @wip will be ignored unless the CLI 291 | command is run with a custom filter, e.g.: 292 | 293 | .. code-block:: bash 294 | 295 | vendor/bin/behat --tags=wip 296 | 297 | .. tip:: 298 | 299 | More details on identifying tests can be found in the chapter 300 | :doc:`/user_guide/command_line_tool/identifying`. 301 | 302 | Suite Contexts 303 | -------------- 304 | 305 | Being able to specify a set of features with a set of contexts for 306 | these features inside the suite has a very interesting side-effect. 307 | You can specify the same features in two different suites being tested 308 | against different contexts *or* the same contexts configured differently. 309 | This basically means that you can use the same subset of features to 310 | develop different layers of your application with Behat: 311 | 312 | .. code-block:: php 313 | 314 | withSuite( 326 | new Suite('domain_features') 327 | ->withPaths('%paths.base%/features') 328 | ->withContexts(DomainContext::class) 329 | ) 330 | ->withSuite( 331 | new Suite('web_features') 332 | ->withFilter(new TagFilter('@web')) 333 | ->withPaths('%paths.base%/features') 334 | ->withContexts(WebContext::class) 335 | ) 336 | ; 337 | 338 | return new Config() 339 | ->withProfile($profile) 340 | ; 341 | 342 | In this case, Behat will first run all the features from the ``features/`` 343 | folder in ``DomainContext`` and then only those tagged with ``@web`` in 344 | ``WebContext``. 345 | 346 | .. tip:: 347 | 348 | It might be worth reading how to :ref:`execute a specific 349 | suite` or 350 | :ref:`initialize a new 351 | suite` 352 | -------------------------------------------------------------------------------- /user_guide/configuration/yaml_configuration.rst: -------------------------------------------------------------------------------- 1 | Yaml configuration 2 | ================== 3 | 4 | Currently the preferred way to define the configuration for your Behat project 5 | is by using one or more files with PHP configuration. 6 | 7 | But historically this configuration could also be expressed in files in the Yaml 8 | format. This possibility is still available for now, and you can use it instead 9 | of PHP configuration in your projects - however it will likely be deprecated and 10 | removed in the future. 11 | 12 | Here is an example of how some configuration could look using a Yaml file: 13 | 14 | .. code-block:: yaml 15 | 16 | imports: 17 | - imported.yaml 18 | 19 | default: 20 | formatters: 21 | progress: ~ 22 | junit: false 23 | suites: 24 | my_suite: 25 | contexts: 26 | - MyContext 27 | paths: 28 | - "one.feature" 29 | filters: 30 | tags: "@run" 31 | extensions: 32 | MyCustomExtension: ~ 33 | 34 | Converting your configuration 35 | ----------------------------- 36 | 37 | If your project uses the legacy Yaml format for your configuration, you can easily 38 | convert them to the PHP format by using the built in config converter. 39 | 40 | To use it, just run: 41 | 42 | .. code-block:: bash 43 | 44 | vendor/bin/behat --convert-config 45 | 46 | This will load the default config file loaded by your project and convert it from the 47 | Yaml format to the PHP format. It will also convert any config files that are imported 48 | by this main file. It will then remove the old Yaml files. 49 | 50 | We recommend carefully reviewing the generated config, particularly if you are using extensions, 51 | custom formatters, or have complex configuration files. 52 | 53 | If you want to convert any other config file which is not your default config file (for 54 | example a config file used in the CI environment), just load it with the ``-c`` 55 | (or ``--config``) option like this: 56 | 57 | .. code-block:: bash 58 | 59 | vendor/bin/behat --convert-config -c ci-behat.yml 60 | 61 | This will load the ``ci-behat.yml`` config file and convert it to the PHP format. 62 | 63 | .. note:: 64 | 65 | Behat needs to be able to load this config file before converting it, so it must be 66 | a valid Behat config file. 67 | -------------------------------------------------------------------------------- /user_guide/context.rst: -------------------------------------------------------------------------------- 1 | Testing Features 2 | ================ 3 | 4 | We've already used this strange ``FeatureContext`` class as a home for our 5 | :doc:`step definitions ` 6 | and :ref:`user-guide--testing-features--hooking-into-the-test-process--hooks`, 7 | but we haven't done much to explain what it actually is. 8 | 9 | Context classes are a keystone of testing environment in Behat. The context 10 | class is a simple POPO (Plain Old PHP Object) that tells Behat how to test 11 | your features. If ``*.feature`` files are all about describing *how* your 12 | application behaves, then the context class is all about how to test it. 13 | 14 | .. code-block:: php 15 | 16 | // features/bootstrap/FeatureContext.php 17 | 18 | use Behat\Behat\Context\Context; 19 | use Behat\Hook\BeforeFeature; 20 | use Behat\Step\Given; 21 | use Behat\Step\Then; 22 | use Behat\Step\When; 23 | 24 | class FeatureContext implements Context 25 | { 26 | public function __construct($parameter) 27 | { 28 | // instantiate context 29 | } 30 | 31 | #[BeforeFeature] 32 | public static function prepareForTheFeature() 33 | { 34 | // clean database or do other preparation stuff 35 | } 36 | 37 | #[Given('we have some context')] 38 | public function prepareContext() 39 | { 40 | // do something 41 | } 42 | 43 | #[When('event occurs')] 44 | public function doSomeAction() 45 | { 46 | // do something 47 | } 48 | 49 | #[Then('something should be done')] 50 | public function checkOutcomes() 51 | { 52 | // do something 53 | } 54 | } 55 | 56 | A simple mnemonic for context classes is: "testing features *in a context*". 57 | Feature descriptions tend to be very high level. It means there's not much 58 | technical detail exposed in them, so the way you will test those 59 | features pretty much depends on the context you test them in. That's what 60 | context classes are. 61 | 62 | .. tip:: 63 | 64 | Behat can automatically generate this class by using the 65 | :doc:`Behat command line tool` with the 66 | ``--init`` option from your project's directory. Behat has several built-in 67 | tools that can help you when creating a new project. Learn more about 68 | ":doc:`/user_guide/initialize`". 69 | 70 | 71 | .. toctree:: 72 | :maxdepth: 2 73 | 74 | context/hooks 75 | context/definitions 76 | context/transformations 77 | 78 | 79 | Context Class Requirements 80 | -------------------------- 81 | 82 | In order to be used by Behat, your context class should follow these rules: 83 | 84 | #. The context class should implement the ``Behat\Behat\Context\Context`` interface. 85 | 86 | #. The context class should be called ``FeatureContext``. It's a simple convention 87 | inside the Behat infrastructure. ``FeatureContext`` is the name of the 88 | context class for the default suite. This can easily be changed through 89 | suite configuration inside ``behat.php``. 90 | 91 | #. The context class should be discoverable and loadable by Behat. That means you 92 | should somehow tell Behat about your class file. Behat comes with a PSR-0 93 | autoloader out of the box and the default autoloading directory is 94 | ``features/bootstrap``. That's why the default ``FeatureContext`` is loaded so 95 | easily by Behat. You can place your own classes under ``features/bootstrap`` 96 | by following the PSR-0 convention or you can even define your own custom 97 | autoloading folder via ``behat.php``. 98 | 99 | .. note:: 100 | 101 | ``Behat\Behat\Context\SnippetAcceptingContext`` and 102 | ``Behat\Behat\Context\CustomSnippetAcceptingContext`` are special 103 | versions of the ``Behat\Behat\Context\Context`` interface that tell 104 | Behat this context expects snippets to be generated for it. 105 | 106 | .. tip:: 107 | 108 | The :doc:`Behat command line tool` 109 | has an ``--init`` option that will initialize a new Behat project in your 110 | directory. Learn more about 111 | :doc:`/user_guide/initialize`. 112 | 113 | Contexts Lifetime 114 | ----------------- 115 | 116 | Your context class is initialized before each scenario is run, and every scenario 117 | has its very own context instance. This means 2 things: 118 | 119 | #. Every scenario is isolated from each other scenario's context. You can do 120 | almost anything inside your scenario context instance without the fear of 121 | affecting other scenarios - every scenario gets its own context instance. 122 | 123 | #. Every step in a single scenario is executed inside a common context 124 | instance. This means you can set ``private`` instance variables inside 125 | your ``Given`` steps and you'll be able to read their new values inside 126 | your ``When`` and ``Then`` steps. 127 | 128 | Multiple Contexts 129 | ----------------- 130 | 131 | At some point, it could become very hard to maintain all your 132 | :doc:`step definitions ` 133 | and :ref:`user-guide--testing-features--hooking-into-the-test-process--hooks` 134 | inside a single class. You could use class inheritance and split definitions 135 | into multiple classes, but doing so could cause your code to become more 136 | difficult to follow and use. 137 | 138 | In light of these issues, Behat provides a more flexible way of helping make 139 | your code more structured by allowing you to use multiple contexts in a single test 140 | suite. 141 | 142 | In order to customise the list of contexts your test suite requires, you need 143 | to fine-tune the suite configuration inside ``behat.php``: 144 | 145 | .. code-block:: php 146 | 147 | withSuite( 159 | new Suite('default') 160 | ->withContexts( 161 | FeatureContext::class, 162 | SecondContext::class, 163 | ThirdContext::class, 164 | ) 165 | ) 166 | ; 167 | 168 | return new Config() 169 | ->withProfile($profile) 170 | ; 171 | 172 | The first ``default`` in this configuration is a name of the profile. We 173 | will discuss profiles a little bit later. Under 174 | the specific profile, we have a special ``suites`` section, which 175 | configures suites inside this profile. We will talk about test suites in more 176 | detail in the :doc:`next chapter `, for now just keep in mind 177 | that a suite is a way to tell Behat where to find your features and 178 | how to test them. The interesting part for us now is the ``contexts`` 179 | section - this is an array of context class names. Behat will use the classes 180 | specified there as your feature contexts. This means that every time 181 | Behat sees a scenario in your test suite, it will: 182 | 183 | #. Get list of all context classes from this ``contexts`` option. 184 | 185 | #. Will try to initialize all these context classes into objects. 186 | 187 | #. Will search for :doc:`step definitions ` and 188 | :ref:`user-guide--testing-features--hooking-into-the-test-process--hooks` in all of them. 189 | 190 | .. note:: 191 | 192 | Do not forget that each of these context classes should follow all 193 | context class requirements. Specifically - they all should implement 194 | ``Behat\Behat\Context\Context`` interface and be autoloadable by 195 | Behat. 196 | 197 | Basically, all contexts under the ``contexts`` section of your ``behat.php`` 198 | are the same for Behat. It will find and use the methods in them the same way 199 | it does in the default ``FeatureContext``. And if you're happy with a single 200 | context class, but you don't like the name ``FeatureContext``, here's 201 | how you change it: 202 | 203 | .. code-block:: php 204 | 205 | withSuite( 215 | new Suite('default') 216 | ->withContexts( 217 | MyAwesomeContext::class, 218 | ) 219 | ) 220 | ; 221 | 222 | return new Config() 223 | ->withProfile($profile) 224 | ; 225 | 226 | This configuration will tell Behat to look for ``MyAwesomeContext`` 227 | instead of the default ``FeatureContext``. 228 | 229 | .. note:: 230 | 231 | Unlike profiles, Behat will not inherit any configuration of your 232 | ``default`` suite. The name ``default`` is only used for demonstration 233 | purpose in this guide. If you have multiple suites that all should use the 234 | same context, you will have to define that specific context for every 235 | specific suite: 236 | 237 | .. code-block:: php 238 | 239 | withSuite( 250 | new Suite('default') 251 | ->withContexts( 252 | MyAwesomeContext::class, 253 | MyWickedContext::class, 254 | ) 255 | ) 256 | ->withSuite( 257 | new Suite('suite_a') 258 | ->withContexts( 259 | MyAwesomeContext::class, 260 | MyWickedContext::class, 261 | ) 262 | ) 263 | ->withSuite( 264 | new Suite('suite_b') 265 | ->withContexts( 266 | MyAwesomeContext::class, 267 | MyWickedContext::class, 268 | ) 269 | ) 270 | ; 271 | 272 | return new Config() 273 | ->withProfile($profile) 274 | ; 275 | 276 | This configuration will tell Behat to look for ``MyAwesomeContext`` and 277 | ``MyWickedContext`` when testing ``suite_a`` and ``MyAwesomeContext`` when 278 | testing ``suite_b``. In this example, ``suite_b`` will not be able to call 279 | steps that are defined in the ``MyWickedContext``. As you can see, even if 280 | you are using the name ``default`` as the name of the suite, Behat will not 281 | inherit any configuration from this suite. 282 | 283 | Context Parameters 284 | ------------------ 285 | 286 | Context classes can be very flexible depending on how far you want 287 | to go in making them dynamic. Most of us will want to make our contexts 288 | environment-independent; where should we put temporary files, which URLs 289 | will be used to access the application? These are 290 | context configuration options highly dependent on the environment you 291 | will test your features in. 292 | 293 | We already said that context classes are just plain old PHP classes. 294 | How would you incorporate environment-dependent parameters into your 295 | PHP classes? Use *constructor arguments*: 296 | 297 | .. code-block:: php 298 | 299 | // features/bootstrap/MyAwesomeContext.php 300 | 301 | use Behat\Behat\Context\Context; 302 | 303 | class MyAwesomeContext implements Context 304 | { 305 | public function __construct($baseUrl, $tempPath) 306 | { 307 | $this->baseUrl = $baseUrl; 308 | $this->tempPath = $tempPath; 309 | } 310 | } 311 | 312 | As a matter of fact, Behat gives you ability to do just that. You can 313 | specify arguments required to instantiate your context classes through 314 | same ``contexts`` setting inside your ``behat.php``: 315 | 316 | .. code-block:: php 317 | 318 | withSuite( 328 | new Suite('default') 329 | ->addContext(MyAwesomeContext::class, [ 330 | 'http://localhost:8080', 331 | '/var/tmp', 332 | ]) 333 | ) 334 | ; 335 | 336 | return new Config() 337 | ->withProfile($profile) 338 | ; 339 | 340 | Arguments would be passed to the ``MyAwesomeContext`` constructor in 341 | the order they were specified here. If you are not happy with the idea 342 | of keeping an order of arguments in your head, you can use argument 343 | names instead: 344 | 345 | .. code-block:: php 346 | 347 | withSuite( 357 | new Suite('default') 358 | ->addContext(MyAwesomeContext::class, [ 359 | 'baseUrl' => 'http://localhost:8080', 360 | 'tempPath' => '/var/tmp', 361 | ]) 362 | ) 363 | ; 364 | 365 | return new Config() 366 | ->withProfile($profile) 367 | ; 368 | 369 | As a matter of fact, if you do, the order in which you specify these 370 | arguments becomes irrelevant: 371 | 372 | .. code-block:: php 373 | 374 | withSuite( 384 | new Suite('default') 385 | ->addContext(MyAwesomeContext::class, [ 386 | 'tempPath' => '/var/tmp', 387 | 'baseUrl' => 'http://localhost:8080', 388 | ]) 389 | ) 390 | ; 391 | 392 | return new Config() 393 | ->withProfile($profile) 394 | ; 395 | 396 | Taking this a step further, if your context constructor arguments are 397 | optional: 398 | 399 | .. code-block:: php 400 | 401 | public function __construct($baseUrl = 'http://localhost', $tempPath = '/var/tmp') 402 | { 403 | $this->baseUrl = $baseUrl; 404 | $this->tempPath = $tempPath; 405 | } 406 | 407 | You then can specify only the parameter that you actually need to change: 408 | 409 | .. code-block:: php 410 | 411 | withSuite( 421 | new Suite('default') 422 | ->addContext(MyAwesomeContext::class, [ 423 | 'tempPath' => '/var/tmp', 424 | ]) 425 | ) 426 | ; 427 | 428 | return new Config() 429 | ->withProfile($profile) 430 | ; 431 | 432 | In this case, the default values would be used for other parameters. 433 | 434 | Context Traits 435 | -------------- 436 | 437 | PHP 5.4 have brought an interesting feature to the language - traits. 438 | Traits are a mechanism for code reuse in single inheritance languages 439 | like PHP. Traits are implemented as a compile-time copy-paste in PHP. 440 | That means if you put some step definitions or hooks inside a trait: 441 | 442 | .. code-block:: php 443 | 444 | // features/bootstrap/ProductsDictionary.php 445 | 446 | trait ProductsDictionary 447 | { 448 | #[Given('there is a(n) :product, which costs £:price')] 449 | public function thereIsAWhichCostsPs($product, $price) 450 | { 451 | throw new PendingException(); 452 | } 453 | } 454 | 455 | And then use it in your context: 456 | 457 | .. code-block:: php 458 | 459 | // features/bootstrap/MyAwesomeContext.php 460 | 461 | use Behat\Behat\Context\Context; 462 | 463 | class MyAwesomeContext implements Context 464 | { 465 | use ProductsDictionary; 466 | } 467 | 468 | It will just work as you expect it to. 469 | 470 | Context traits come in handy if you'd like to have separate contexts, 471 | but still need to use the very same step definition in both of them. Instead of 472 | having the same code in both context classes – and having to maintain it 473 | in both – you should create a single Trait that you would then ``use`` in 474 | both context classes. 475 | 476 | .. note:: 477 | 478 | Given that step definitions :doc:`cannot be duplicated within a Suite 479 | `, this will only work 480 | for contexts used in separate suites. 481 | 482 | In other words, if your Suite uses at least two different Contexts, and 483 | those context classes ``use`` the same Trait, this will result in a duplicate 484 | step definition and Behat will complain by throwing a ``Redundant`` exception. 485 | -------------------------------------------------------------------------------- /user_guide/context/definitions.rst: -------------------------------------------------------------------------------- 1 | Defining Reusable Actions 2 | ========================= 3 | 4 | :doc:`Gherkin language` provides a way to describe your 5 | application behavior in business understandable language. But how do you test 6 | that the described behavior is actually implemented? Or that the application 7 | satisfies your business expectations as described in the feature scenarios? 8 | Behat provides a way to map your scenario steps (actions) 1-to-1 with actual 9 | PHP code called step definitions: 10 | 11 | .. code-block:: php 12 | 13 | #[When('I do something with :argument')] 14 | public function iDoSomethingWith($argument) 15 | { 16 | // do something with $argument 17 | } 18 | 19 | .. note:: 20 | 21 | Step definitions are just normal PHP methods. They are instance methods in 22 | a special class called :doc:`FeatureContext`. 23 | 24 | Creating Your First Step Definition 25 | ----------------------------------- 26 | 27 | The main goal for a step definition is to be executed when Behat sees its matching 28 | step in executed scenario. However, just because a method exists within ``FeatureContext`` 29 | doesn't mean Behat can find it. Behat needs a way to check that a concrete class 30 | method is suitable for a concrete step in a scenario. Behat matches 31 | ``FeatureContext`` methods to step definitions using pattern matching. 32 | 33 | When Behat runs, it compares lines of Gherkin steps from each scenario to the 34 | patterns bound to each method in your ``FeatureContext``. If the line of Gherkin 35 | satisfies a bound pattern, its corresponding step definition is executed. It's 36 | that simple! 37 | 38 | Behat uses PHP attributes to bind patterns to ``FeatureContext`` methods: 39 | 40 | .. code-block:: php 41 | 42 | #[When('I do something with :methodArgument')] 43 | public function someMethod($methodArgument) {} 44 | 45 | Let's take a closer look at this code: 46 | 47 | #. ``When`` is a definition keyword. There are 3 supported keywords implemented as 48 | attributes: ``Given``/``When``/``Then``. These three definition keywords 49 | are actually equivalent, but all three are available so that your step 50 | definition remains readable. 51 | 52 | #. The argument to the attribute is the step text pattern (e.g. 53 | ``I do something with :methodArgument``). 54 | 55 | #. All token values of the pattern (e.g. ``:methodArgument``) will be captured 56 | and passed to the method argument with the same name (``$methodArgument``). 57 | 58 | As you have probably noticed, this pattern is quite general and its corresponding 59 | method will be called for steps that contain ``... I do something with ...``, 60 | including: 61 | 62 | .. code-block:: gherkin 63 | 64 | Given I do something with "string1" 65 | When I do something with 'some other string' 66 | Then I do something with 25 67 | 68 | The only real difference between those steps in the eyes of Behat is the 69 | captured token text. This text will be passed to the step's corresponding 70 | method as an argument value. In the example above, 71 | ``FeatureContext::someMethod()`` will be called three times, each time with 72 | a different argument: 73 | 74 | #. ``$context->someMethod($methodArgument = 'string1');``. 75 | 76 | #. ``$context->someMethod($methodArgument = 'some other string');``. 77 | 78 | #. ``$context->someMethod($methodArgument = '25');``. 79 | 80 | .. note:: 81 | 82 | A pattern can't automatically determine the datatype of its matches, so 83 | all method arguments coming from step definitions are passed as strings. 84 | Even if your pattern matches "500", which could be considered an integer, 85 | '500' will be passed as a string argument to the step definition's method. 86 | 87 | This is not a feature or limitation of Behat, but rather the inherent way 88 | string matching works. It is your responsibility to cast string arguments 89 | to integers, floats or booleans where applicable given the code you are 90 | testing. 91 | 92 | Casting arguments to specific types can be accomplished using 93 | :doc:`step argument transformations` 94 | 95 | .. note:: 96 | 97 | Behat does not differentiate between step keywords when matching patterns 98 | to methods. So a step defined with ``When`` could also be matched to 99 | ``Given ...``, ``Then ...``, ``And ...``, ``But ...``, etc. 100 | 101 | Your step definitions can also define multiple arguments to pass to its matching 102 | ``FeatureContext`` method: 103 | 104 | .. code-block:: php 105 | 106 | #[When('I do something with :stringArgument and with :numberArgument')] 107 | public function someMethod($stringArgument, $numberArgument) {} 108 | 109 | You can also specify alternative words and optional parts of words, like this: 110 | 111 | .. code-block:: php 112 | 113 | #[When('there is/are :count monster(s)')] 114 | public function thereAreMonsters($count) {} 115 | 116 | If you need to come up with a much more complicated matching algorithm, you can 117 | always use good old regular expressions: 118 | 119 | .. code-block:: php 120 | 121 | #[When('/^there (?:is|are) (\d+) monsters?$/i')] 122 | public function thereAreMonsters($count) {} 123 | 124 | And if you want to be able to say things in different ways that are not so 125 | easily written as a single regular expression, you can add multiple 126 | attributes for the one method: 127 | 128 | .. code-block:: php 129 | 130 | #[When('/^I create (\d+) monsters$/i')] 131 | #[Given('/^(\d+) monster(?:s|) (?:have|has) been created$/i')] 132 | public function thereAreMonsters($count) {} 133 | 134 | Behat will call the corresponding method if any of the patterns matches. 135 | 136 | .. note:: 137 | 138 | Behat uses the ``preg_match()`` function to match these regular expressions 139 | to arguments. Special care is needed to make sure that there is only one 140 | capturing group for each argument. A expression like ``(?P([\w\s]+))`` 141 | contains an inner capturing group (``([\w\s]+)``) and this may make some 142 | arguments not match correctly. The right expression to use would be 143 | ``(?P[\w\s]+)`` 144 | 145 | Definition Snippets 146 | ------------------- 147 | 148 | You now know how to write step definitions by hand, but writing all these 149 | method stubs, attributes and patterns by hand is tedious. Behat makes 150 | this routine task much easier and fun by generating definition snippets for 151 | you! Let's pretend that you have this feature: 152 | 153 | .. code-block:: gherkin 154 | 155 | Feature: 156 | Scenario: 157 | Given some step with "string" argument 158 | And number step with 23 159 | 160 | If your context class implements ``Behat\Behat\Context\SnippetAcceptingContext`` 161 | interface and you test a feature with missing steps in Behat: 162 | 163 | .. code-block:: bash 164 | 165 | $ vendor/bin/behat features/example.feature 166 | 167 | Behat will provide auto-generated snippets for your context class. 168 | 169 | It not only generates the proper definition attribute type (``Given``), but 170 | also a proper pattern with tokens capturing (``:arg1``, ``:arg2``), method 171 | name (``someStepWithArgument()``, ``numberStepWith()``) and arguments ( 172 | ``$arg1``, ``$arg2``), all based just on the text of the step. Isn't that cool? 173 | 174 | The only thing left for you to do is to copy these method snippets into your 175 | ``FeatureContext`` class and provide a useful body for them. Or even better, 176 | run behat with ``--append-snippets`` option: 177 | 178 | .. code-block:: bash 179 | 180 | $ vendor/bin/behat features/example.feature --dry-run --append-snippets 181 | 182 | ``--append-snippets`` tells Behat to automatically add snippets inside your 183 | context class. 184 | 185 | .. note:: 186 | 187 | Implementing the ``SnippetAcceptingContext`` interface tells Behat that 188 | your context is expecting snippets to be generated inside it. Behat will 189 | generate simple pattern snippets for you, but if regular expressions 190 | are your thing, Behat can generate them instead if you implement 191 | ``Behat\Behat\Context\CustomSnippetAcceptingContext`` interface instead 192 | and add ``getAcceptedSnippetType()`` method returning string ``"regex"``: 193 | 194 | .. code-block:: php 195 | 196 | public static function getAcceptedSnippetType() 197 | { 198 | return 'regex'; 199 | } 200 | 201 | Step Execution Result Types 202 | --------------------------- 203 | 204 | Now you know how to map actual code to PHP code that will be executed. But 205 | how can you tell what exactly "failed" or "passed" when executing a step? 206 | And how does Behat actually check that a step executed properly? 207 | 208 | For that, we have step execution types. Behat differentiates between seven 209 | types of step execution results: "`Successful Steps`_", "`Undefined Steps`_", 210 | "`Pending Steps`_", "`Failed Steps`_", "`Skipped Steps`_", "`Ambiguous Steps`_" 211 | and "`Redundant Step Definitions`_". 212 | 213 | Let's use our previously introduced feature for all the following examples: 214 | 215 | .. code-block:: gherkin 216 | 217 | # features/example.feature 218 | Feature: 219 | Scenario: 220 | Given some step with "string" argument 221 | And number step with 23 222 | 223 | Successful Steps 224 | ~~~~~~~~~~~~~~~~ 225 | 226 | When Behat finds a matching step definition it will execute it. If the 227 | definition method does **not** throw any ``Exception``, the step is marked 228 | as successful (green). What you return from a definition method has no 229 | effect on the passing or failing status of the definition itself. 230 | 231 | Let's pretend our context class contains the code below: 232 | 233 | .. code-block:: php 234 | 235 | // features/bootstrap/FeatureContext.php 236 | 237 | use Behat\Behat\Context\Context; 238 | use Behat\Step\Given; 239 | 240 | class FeatureContext implements Context 241 | { 242 | #[Given('some step with :argument1 argument')] 243 | public function someStepWithArgument($argument1) 244 | { 245 | } 246 | 247 | #[Given('number step with :argument1')] 248 | public function numberStepWith($argument1) 249 | { 250 | } 251 | } 252 | 253 | When you run your feature, you'll see all steps passed and are marked as 254 | green. That's simply because no exceptions were thrown during their 255 | execution. 256 | 257 | .. note:: 258 | 259 | Passed steps are always marked as **green** if colors are supported by 260 | your console. 261 | 262 | .. tip:: 263 | 264 | Enable the "posix" PHP extension in order to see colorful Behat output. 265 | Depending on your Linux, Mac OS or other Unix system it might be part 266 | of the default PHP installation or a separate ``posix`` package. 267 | 268 | Undefined Steps 269 | ~~~~~~~~~~~~~~~ 270 | 271 | When Behat cannot find a matching definition, the step is marked as 272 | **undefined**, and all subsequent steps in the scenarios are **skipped**. 273 | 274 | Let's pretend we have an empty context class: 275 | 276 | .. code-block:: php 277 | 278 | // features/bootstrap/FeatureContext.php 279 | 280 | use Behat\Behat\Context\Context; 281 | 282 | class FeatureContext implements Context 283 | { 284 | } 285 | 286 | When you run your feature, you'll get 2 undefined steps that are marked 287 | yellow. 288 | 289 | .. note:: 290 | 291 | Undefined steps are always marked as **yellow** if colors are supported by 292 | your console. 293 | 294 | .. note:: 295 | 296 | All steps following an undefined step are not executed, as the 297 | behavior following it is unpredictable. These steps are marked as 298 | **skipped** (cyan). 299 | 300 | .. tip:: 301 | 302 | If you use the ``--strict`` option with Behat, undefined steps will cause 303 | Behat to exit with ``1`` code. 304 | 305 | Pending Steps 306 | ~~~~~~~~~~~~~ 307 | 308 | When a definition method throws a 309 | ``Behat\Behat\Tester\Exception\PendingException`` exception, the step is 310 | marked as **pending**, reminding you that you have work to do. 311 | 312 | Let's pretend your ``FeatureContext`` looks like this: 313 | 314 | .. code-block:: php 315 | 316 | // features/bootstrap/FeatureContext.php 317 | 318 | use Behat\Behat\Context\Context; 319 | use Behat\Behat\Tester\Exception\PendingException; 320 | use Behat\Step\Given; 321 | 322 | class FeatureContext implements Context 323 | { 324 | #[Given('some step with :argument1 argument')] 325 | public function someStepWithArgument($argument1) 326 | { 327 | throw new PendingException('Do some string work'); 328 | } 329 | 330 | #[Given('number step with :argument1')] 331 | public function numberStepWith($argument1) 332 | { 333 | throw new PendingException('Do some number work'); 334 | } 335 | } 336 | 337 | When you run your feature, you'll get 1 pending step that is marked yellow and 338 | one step following it that is marked cyan. 339 | 340 | .. note:: 341 | 342 | Pending steps are always marked as **yellow** if colors are supported by 343 | your console, because they are logically similar to **undefined** steps. 344 | 345 | .. note:: 346 | 347 | All steps following a pending step are not executed, as the 348 | behavior following it is unpredictable. These steps are marked as 349 | **skipped**. 350 | 351 | .. tip:: 352 | 353 | If you use ``--strict`` option with Behat, pending steps will cause Behat 354 | to exit with ``1`` code. 355 | 356 | Failed Steps 357 | ~~~~~~~~~~~~ 358 | 359 | When a definition method throws any ``Exception`` (except ``PendingException``) 360 | during execution, the step is marked as **failed**. Again, what you return from a 361 | definition does not affect the passing or failing of the step. Returning ``null`` 362 | or ``false`` will not cause a step to fail. 363 | 364 | Let's pretend, that your ``FeatureContext`` has following code: 365 | 366 | .. code-block:: php 367 | 368 | // features/bootstrap/FeatureContext.php 369 | 370 | use Behat\Behat\Context\Context; 371 | use Behat\Step\Given; 372 | 373 | class FeatureContext implements Context 374 | { 375 | #[Given('some step with :argument1 argument')] 376 | public function someStepWithArgument($argument1) 377 | { 378 | throw new Exception('some exception'); 379 | } 380 | 381 | #[Given(number step with :argument1')] 382 | public function numberStepWith($argument1) 383 | { 384 | } 385 | } 386 | 387 | When you run your feature, you'll get 1 failing step that is marked red and 388 | it will be followed by 1 skipped step that is marked cyan. 389 | 390 | .. note:: 391 | 392 | Failed steps are always marked as **red** if colors are supported by 393 | your console. 394 | 395 | .. note:: 396 | 397 | All steps within a scenario following a failed step are not executed, as the 398 | behavior following it is unpredictable. These steps are marked as 399 | **skipped**. 400 | 401 | .. tip:: 402 | 403 | If Behat finds a failed step during suite execution, it will exit with 404 | ``1`` code. 405 | 406 | .. tip:: 407 | 408 | Behat doesn't come with its own assertion tool, but you can use any proper assertion 409 | tool out there. Proper assertion tool is a library, which assertions throw 410 | exceptions on fail. For example, if you're familiar with PHPUnit, you can use 411 | its assertions in Behat by installing it via composer: 412 | 413 | .. code-block:: bash 414 | 415 | $ php composer.phar require --dev phpunit/phpunit 416 | 417 | and then by simply using assertions in your steps: 418 | 419 | .. code-block:: php 420 | 421 | PHPUnit_Framework_Assert::assertCount(intval($count), $this->basket); 422 | 423 | .. tip:: 424 | 425 | You can get exception stack trace with ``-vv`` option provided to Behat: 426 | 427 | .. code-block:: bash 428 | 429 | $ vendor/bin/behat features/example.feature -vv 430 | 431 | Skipped Steps 432 | ~~~~~~~~~~~~~ 433 | 434 | Steps that follow **undefined**, **pending** or **failed** steps are never 435 | executed, even if there is a matching definition. These steps are marked 436 | **skipped**: 437 | 438 | .. note:: 439 | 440 | Skipped steps are always marked as **cyan** if colors are supported by 441 | your console. 442 | 443 | Ambiguous Steps 444 | ~~~~~~~~~~~~~~~ 445 | 446 | When Behat finds two or more definitions that match a single step, this step is 447 | marked as **ambiguous**. 448 | 449 | Consider your ``FeatureContext`` has following code: 450 | 451 | .. code-block:: php 452 | 453 | // features/bootstrap/FeatureContext.php 454 | 455 | use Behat\Behat\Context\Context; 456 | use Behat\Step\Given; 457 | 458 | class FeatureContext implements Context 459 | { 460 | #[Given('/^.* step with .*$/')] 461 | public function someStepWithArgument() 462 | { 463 | } 464 | 465 | #[Given('/^number step with (\d+)$/')] 466 | public function numberStepWith($argument1) 467 | { 468 | } 469 | } 470 | 471 | Executing Behat with this feature context will result in a ``Ambiguous`` 472 | exception being thrown. 473 | 474 | Behat will not make a decision about which definition to execute. That's your 475 | job! But as you can see, Behat will provide useful information to help you 476 | eliminate such problems. 477 | 478 | Redundant Step Definitions 479 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ 480 | 481 | Behat will not let you define a step expression's corresponding pattern more 482 | than once. For example, look at the two ``Given`` patterns defined in this 483 | feature context: 484 | 485 | .. code-block:: php 486 | 487 | // features/bootstrap/FeatureContext.php 488 | 489 | use Behat\Behat\Context\Context; 490 | use Behat\Step\Given; 491 | 492 | class FeatureContext implements Context 493 | { 494 | #[Given('/^number step with (\d+)$/')] 495 | public function workWithNumber($number1) 496 | { 497 | } 498 | 499 | #[Given('/^number step with (\d+)$/')] 500 | public function workDifferentlyWithNumber($number1) 501 | { 502 | } 503 | } 504 | 505 | Executing Behat with this feature context will result in a ``Redundant`` 506 | exception being thrown. 507 | 508 | .. tip:: 509 | Behat provides a :ref:`command line 510 | option` 511 | that allows you to easily browse definitions in order to reuse them or adapt 512 | them. 513 | -------------------------------------------------------------------------------- /user_guide/context/hooks.rst: -------------------------------------------------------------------------------- 1 | Hooking into the Test Process 2 | ============================= 3 | 4 | You've learned :doc:`how to write step definitions ` and 5 | that with :doc:`Gherkin ` you can move common steps into a 6 | background block, making your features DRY. But what if that's not enough? What 7 | if you want to execute some code before the whole test suite or after a 8 | specific scenario? Hooks to the rescue: 9 | 10 | .. code-block:: php 11 | 12 | // features/bootstrap/FeatureContext.php 13 | 14 | use Behat\Behat\Context\Context; 15 | use Behat\Testwork\Hook\Scope\BeforeSuiteScope; 16 | use Behat\Behat\Hook\Scope\AfterScenarioScope; 17 | use Behat\Hook\AfterScenario; 18 | use Behat\Hook\BeforeSuite; 19 | 20 | class FeatureContext implements Context 21 | { 22 | #[BeforeSuite] 23 | public static function prepare(BeforeSuiteScope $scope) 24 | { 25 | // prepare system for test suite 26 | // before it runs 27 | } 28 | 29 | #[AfterScenario('@database')] 30 | public function cleanDB(AfterScenarioScope $scope) 31 | { 32 | // clean database after scenarios, 33 | // tagged with @database 34 | } 35 | } 36 | 37 | Behat Hook System 38 | ----------------- 39 | 40 | Behat provides a number of hook points which allow us to run arbitrary 41 | logic at various points in the Behat test cycle. Hooks are a lot like 42 | step definitions or transformations - they are just simple methods 43 | with special attributes inside your context classes. There is no 44 | association between where the hook is defined and which node it is run 45 | for, but you can use tagged or named hooks if you want more fine grained 46 | control. 47 | 48 | All defined hooks are run whenever the relevant action occurs. The action 49 | tree looks something like this: 50 | 51 | .. code-block:: text 52 | 53 | ├── Suite #1 54 | │ ├── Feature #1 55 | │ │ ├── Scenario #1 56 | │ │ │ ├── Step #1 57 | │ │ │ └── Step #2 58 | │ │ └── Scenario #2 59 | │ │ ├── Step #1 60 | │ │ └── Step #2 61 | │ └── Feature #2 62 | │ └── Scenario #1 63 | │ └── Step #1 64 | └── Suite #2 65 | └── Feature #1 66 | └── Scenario #1 67 | └── Step #1 68 | 69 | This is a basic test cycle in Behat. There are many test suites, each of 70 | which has many features, which themselves have many scenarios with many 71 | steps. Note that when Behat actually runs, scenario outline examples are 72 | interpreted as scenarios - meaning each outline example becomes an actual 73 | scenario in this action tree. 74 | 75 | .. _user-guide--testing-features--hooking-into-the-test-process--hooks: 76 | 77 | Hooks 78 | ----- 79 | 80 | Hooks allow you to execute your custom code just before or just after each 81 | of these actions. Behat allows you to use the following hooks: 82 | 83 | #. The ``BeforeSuite`` hook is run before any feature in the suite runs. For 84 | example, you could use this to set up the project you are testing. This 85 | hook receives an optional argument with an instance of the 86 | ``Behat\Testwork\Hook\Scope\BeforeSuiteScope`` class. 87 | 88 | #. The ``AfterSuite`` hook is run after all features in the suite have run. 89 | This hooks is useful to dump or print some kind of statistics or tear 90 | down your application after testing. This hook receives an optional 91 | argument with an instance of the 92 | ``Behat\Testwork\Hook\Scope\AfterSuiteScope`` class. 93 | 94 | #. The ``BeforeFeature`` hook is run before a feature runs. This hook receives 95 | an optional argument with an instance of the 96 | ``Behat\Behat\Hook\Scope\BeforeFeatureScope`` class. 97 | 98 | #. The ``AfterFeature`` hook is run after Behat finishes executing a feature. 99 | This hook receives an optional argument with an instance of the 100 | ``Behat\Behat\Hook\Scope\AfterFeatureScope`` class. 101 | 102 | #. The ``BeforeScenario`` hook is run before a specific scenario will run. This 103 | hook receives an optional argument with an instance of the 104 | ``Behat\Behat\Hook\Scope\BeforeScenarioScope`` class. 105 | 106 | #. The ``AfterScenario`` hook is run after Behat finishes executing a scenario. 107 | This hook receives an optional argument with an instance of the 108 | ``Behat\Behat\Hook\Scope\AfterScenarioScope`` class. 109 | 110 | #. The ``BeforeStep`` hook is run before a step runs. This hook receives an 111 | optional argument with an instance of the 112 | ``Behat\Behat\Hook\Scope\BeforeStepScope`` class. 113 | 114 | #. The ``AfterStep`` hook is run after Behat finishes executing a step. This 115 | hook receives an optional argument with an instance of the 116 | ``Behat\Behat\Hook\Scope\AfterStepScope`` class. 117 | 118 | You can use any of these hooks by adding attributes to any of your methods in your context 119 | class: 120 | 121 | .. code-block:: php 122 | 123 | #[BeforeSuite] 124 | public static function prepare($scope) 125 | { 126 | // prepare system for test suite 127 | // before it runs 128 | } 129 | 130 | We use attributes as we did before with :doc:`definitions `. 131 | Simply use the attribute of the name of the hook you want to use (e.g. 132 | ``BeforeSuite``). 133 | 134 | Suite Hooks 135 | ----------- 136 | 137 | Suite hooks are run outside of the scenario context. It means that your context 138 | class (e.g. ``FeatureContext``) is not instantiated yet and the only way Behat 139 | can execute code in it is through the static calls. That is why suite hooks must 140 | be defined as static methods in the context class: 141 | 142 | .. code-block:: php 143 | 144 | use Behat\Testwork\Hook\Scope\BeforeSuiteScope; 145 | use Behat\Testwork\Hook\Scope\AfterSuiteScope; 146 | use Behat\Hook\AfterSuite; 147 | use Behat\Hook\BeforeSuite; 148 | 149 | #[BeforeSuite] 150 | public static function setup(BeforeSuiteScope $scope) 151 | { 152 | } 153 | 154 | #[AfterSuite] 155 | public static function teardown(AfterSuiteScope $scope) 156 | { 157 | } 158 | 159 | There are two suite hook types available: 160 | 161 | * ``BeforeSuite`` - executed before any feature runs. 162 | * ``AfterSuite`` - executed after all features have run. 163 | 164 | Feature Hooks 165 | ------------- 166 | 167 | Same as suite hooks, feature hooks are ran outside of the scenario context. 168 | So same as suite hooks, your feature hooks must be defined as static methods 169 | inside your context: 170 | 171 | .. code-block:: php 172 | 173 | use Behat\Behat\Hook\Scope\BeforeFeatureScope; 174 | use Behat\Behat\Hook\Scope\AfterFeatureScope; 175 | use Behat\Hook\AfterFeature; 176 | use Behat\Hook\BeforeFeature; 177 | 178 | #[BeforeFeature] 179 | public static function setupFeature(BeforeFeatureScope $scope) 180 | { 181 | } 182 | 183 | #[AfterFeature] 184 | public static function teardownFeature(AfterFeatureScope $scope) 185 | { 186 | } 187 | 188 | 189 | There are two feature hook types available: 190 | 191 | * ``BeforeFeature`` - gets executed before every feature in suite. 192 | * ``AfterFeature`` - gets executed after every feature in suite. 193 | 194 | Scenario Hooks 195 | -------------- 196 | 197 | Scenario hooks are triggered before or after each scenario runs. These 198 | hooks are executed inside an initialized context instance, so not only could they 199 | be simple context instance methods, they will also have access to 200 | any object properties you set during your scenario: 201 | 202 | .. code-block:: php 203 | 204 | use Behat\Behat\Hook\Scope\BeforeScenarioScope; 205 | use Behat\Behat\Hook\Scope\AfterScenarioScope; 206 | use Behat\Hook\AfterScenario; 207 | use Behat\Hook\BeforeScenario; 208 | 209 | #[BeforeScenario] 210 | public function before(BeforeScenarioScope $scope) 211 | { 212 | } 213 | 214 | #[AfterScenario] 215 | public function after(AfterScenarioScope $scope) 216 | { 217 | } 218 | 219 | There are two scenario hook types available: 220 | 221 | * ``BeforeScenario`` - executed before every scenario in each feature. 222 | * ``AfterScenario`` - executed after every scenario in each feature. 223 | 224 | Now, the interesting part: 225 | 226 | The ``BeforeScenario`` hook executes not only 227 | before each scenario in each feature, but before **each example row** in 228 | the scenario outline. Yes, each scenario outline example row works almost the 229 | same as a usual scenario. 230 | 231 | ``AfterScenario`` functions exactly the same way, being executed both after 232 | usual scenarios and outline examples. 233 | 234 | Step Hooks 235 | ---------- 236 | 237 | Step hooks are triggered before or after each step runs. These hooks are 238 | run inside an initialized context instance, so they are just plain context 239 | instance methods in the same way as scenario hooks are: 240 | 241 | .. code-block:: php 242 | 243 | use Behat\Behat\Hook\Scope\BeforeStepScope; 244 | use Behat\Behat\Hook\Scope\AfterStepScope; 245 | use Behat\Hook\AfterStep; 246 | use Behat\Hook\BeforeStep; 247 | 248 | #[BeforeStep] 249 | public function beforeStep(BeforeStepScope $scope) 250 | { 251 | } 252 | 253 | #[AfterStep] 254 | public function afterStep(AfterStepScope $scope) 255 | { 256 | } 257 | 258 | 259 | There are two step hook types available: 260 | 261 | * ``BeforeStep`` - executed before every step in each scenario. 262 | * ``AfterStep`` - executed after every step in each scenario. 263 | 264 | Tagged Hooks 265 | ------------ 266 | 267 | Sometimes you may want a certain hook to run only for certain scenarios, 268 | features or steps. This can be achieved by associating a ``BeforeFeature``, 269 | ``AfterFeature``, ``BeforeScenario`` or ``AfterScenario`` hook with one 270 | or more tags. You can also use ``OR`` (``||``) and ``AND`` (``&&``) tags: 271 | 272 | .. code-block:: php 273 | 274 | #[BeforeScenario('@database,@orm')] 275 | public function cleanDatabase() 276 | { 277 | // clean database before 278 | // @database OR @orm scenarios 279 | } 280 | 281 | Use the ``&&`` tag to execute a hook only when it has *all* provided tags: 282 | 283 | .. code-block:: php 284 | 285 | #[BeforeScenario('@database&&@fixtures')] 286 | public function cleanDatabaseFixtures() 287 | { 288 | // clean database fixtures 289 | // before @database @fixtures 290 | // scenarios 291 | } 292 | -------------------------------------------------------------------------------- /user_guide/context/transformations.rst: -------------------------------------------------------------------------------- 1 | Step Argument Transformations 2 | ============================= 3 | 4 | Step argument transformations allow you to abstract common operations performed 5 | on step definition arguments into reusable methods. In addition, these methods 6 | can be used to transform a normal string argument that was going to be used 7 | as an argument to a step definition method, into a more specific data type 8 | or an object. 9 | 10 | Each transformation method must return a new value. This value then replaces 11 | the original string value that was going to be used as an argument to a step 12 | definition method. 13 | 14 | Transformation methods are defined using the same attribute style as step 15 | definition methods, but instead use the ``Transform`` attribute with 16 | a matching pattern as argument. 17 | 18 | As a basic example, you can automatically cast all numeric arguments to 19 | integers with the following context class code: 20 | 21 | .. code-block:: php 22 | 23 | // features/bootstrap/FeatureContext.php 24 | 25 | use Behat\Behat\Context\Context; 26 | use Behat\Step\Then; 27 | use Behat\Transformation\Transform; 28 | 29 | class FeatureContext implements Context 30 | { 31 | #[Transform('/^(\d+)$/')] 32 | public function castStringToNumber(string $string): int 33 | { 34 | return intval($string); 35 | } 36 | 37 | #[Then('a user :name, should have :count followers')] 38 | public function checkFollowers($name, $count): void 39 | { 40 | if ('integer' !== gettype($count)) { 41 | throw new Exception('Integer expected'); 42 | } 43 | } 44 | } 45 | 46 | .. note:: 47 | 48 | In the same way as with step definitions, you can use both simple patterns and 49 | regular expressions. 50 | 51 | Let's go a step further and create a transformation method that takes an 52 | incoming string argument and returns a specific object. In the following 53 | example, our transformation method will be passed a username, and the method 54 | will create and return a new ``User`` object: 55 | 56 | .. code-block:: php 57 | 58 | // features/bootstrap/FeatureContext.php 59 | 60 | use Behat\Behat\Context\Context; 61 | use Behat\Step\Then; 62 | use Behat\Transformation\Transform; 63 | 64 | class FeatureContext implements Context 65 | { 66 | #[Transform(':user')] 67 | public function castUsernameToUser($user): User 68 | { 69 | return new User($user); 70 | } 71 | 72 | #[Then('a :user, should have :count followers')] 73 | public function checkFollowers(User $user, $count): void 74 | { 75 | if ('integer' !== gettype($count)) { 76 | throw new Exception('Integer expected'); 77 | } 78 | } 79 | } 80 | 81 | Table Transformation 82 | ~~~~~~~~~~~~~~~~~~~~ 83 | 84 | Let's pretend we have written the following feature: 85 | 86 | .. code-block:: gherkin 87 | 88 | # features/table.feature 89 | Feature: Users 90 | 91 | Scenario: Creating Users 92 | Given the following users: 93 | | name | followers | 94 | | everzet | 147 | 95 | | avalanche123 | 142 | 96 | | kriswallsmith | 274 | 97 | | fabpot | 962 | 98 | 99 | And our ``FeatureContext`` class looks like this: 100 | 101 | .. code-block:: php 102 | 103 | // features/bootstrap/FeatureContext.php 104 | 105 | use Behat\Behat\Context\Context; 106 | use Behat\Gherkin\Node\TableNode; 107 | use Behat\Step\Given; 108 | 109 | class FeatureContext implements Context 110 | { 111 | #[Given('the following users:')] 112 | public function pushUsers(TableNode $usersTable): void 113 | { 114 | $users = array(); 115 | foreach ($usersTable as $userHash) { 116 | $user = new User(); 117 | $user->setUsername($userHash['name']); 118 | $user->setFollowersCount($userHash['followers']); 119 | $users[] = $user; 120 | } 121 | 122 | // do something with $users 123 | } 124 | } 125 | 126 | Instead of having to transform the data in the step definition, we can define a transformation that transforms a whole 127 | table. A table transformation is matched via the prefix ``table:`` followed by the list of table headers: 128 | 129 | .. code-block:: php 130 | 131 | // features/bootstrap/FeatureContext.php 132 | 133 | use Behat\Behat\Context\Context; 134 | use Behat\Gherkin\Node\TableNode; 135 | use Behat\Step\Given; 136 | use Behat\Step\Then; 137 | use Behat\Transformation\Transform; 138 | 139 | class FeatureContext implements Context 140 | { 141 | #[Transform('table:name,followers')] 142 | public function castUsers(TableNode $usersTable): array 143 | { 144 | $users = array(); 145 | foreach ($usersTable->getHash() as $userHash) { 146 | $user = new User(); 147 | $user->setUsername($userHash['name']); 148 | $user->setFollowersCount($userHash['followers']); 149 | $users[] = $user; 150 | } 151 | 152 | return $users; 153 | } 154 | 155 | #[Given('the following users:')] 156 | public function pushUsers(array $users): void 157 | { 158 | // do something with $users 159 | } 160 | 161 | #[Then('I expect the following users:')] 162 | public function assertUsers(array $users): void 163 | { 164 | // do something with $users 165 | } 166 | } 167 | 168 | The function that performs the transformation receives as an argument a ``TableNode`` with the table data and it should 169 | return an array with one or more transformed values (one for each row in the table). 170 | 171 | You can also transform each row individually instead of transforming the table as a whole. In this case you should use 172 | the ``row:`` prefix followed by a list of column headers: 173 | 174 | .. code-block:: php 175 | 176 | #[Transform('row:name,followers')] 177 | public function castUserRow(array $row): User 178 | { 179 | $user = new User(); 180 | $user->setUsername($row['name']); 181 | $user->setFollowersCount($row['followers']); 182 | return $user; 183 | } 184 | 185 | In this case, the function that performs the transformation receives as an argument an array with the data for each 186 | individual row and should return the transformed value. 187 | 188 | If you prefer, you can pass the data in columns instead of rows, for example with this feature file: 189 | 190 | .. code-block:: gherkin 191 | 192 | # features/row-table.feature 193 | Feature: Users 194 | 195 | Scenario: Creating Users 196 | Given the following users: 197 | | name | everzet | avalanche123 | kriswallsmith | 198 | | followers | 147 | 142 | 274 | 199 | 200 | You should then modify your transformation function to use the ``rowtable:`` prefix with a list of row headers: 201 | 202 | .. code-block:: php 203 | 204 | #[Transform('rowtable:name,followers')] 205 | public function castUsersTable(TableNode $usersTable) 206 | { 207 | $users = array(); 208 | foreach ($usersTable->getRowsHash() as $userHash) { 209 | $user = new User(); 210 | $user->setUsername($userHash['name']); 211 | $user->setFollowersCount($userHash['followers']); 212 | $users[] = $user; 213 | } 214 | 215 | return $users; 216 | } 217 | 218 | Again the function that performs the transformation receives as an argument a ``TableNode`` with the table data and it 219 | should return an array with one or more transformed values (one for each column in the table). Notice that we are 220 | using ``getRowsHash()`` instead of ``getHash()`` to get the data from the table. 221 | 222 | One final possibility is to transform one or more columns in a table into a different value. For example you might 223 | have this feature: 224 | 225 | .. code-block:: gherkin 226 | 227 | # features/table-column.feature 228 | Feature: Users 229 | 230 | Scenario: Assigning followers to users 231 | When I assign this number of followers to users: 232 | | user | followers | 233 | | everzet | 147 | 234 | | avalanche123 | 142 | 235 | | kriswallsmith | 274 | 236 | | fabpot | 962 | 237 | 238 | In your transformation you should use the ``column:`` prefix with a list of the column names that can be matched: 239 | 240 | .. code-block:: php 241 | 242 | #[Transform('column:user')] 243 | public function castUserName(string $name): User 244 | { 245 | $user = new User(); 246 | $user->setUsername($name); 247 | return $user; 248 | } 249 | 250 | #[Given('I assign this number of followers to users:')] 251 | public function assignFollowers(array $data): void 252 | { 253 | foreach ($data as $row) { 254 | $user = $row['user']; 255 | $user->setFollowersCount($row['followers']); 256 | } 257 | } 258 | 259 | As you can see, the transformation function receives a string with the value of the corresponding column in each row 260 | and should return a transformed value. This is used to build an array where the corresponding columns will have the 261 | transformed values and this is what is passed to the step definition. 262 | 263 | .. note:: 264 | 265 | Transformations are powerful and it is important to take care how you 266 | implement them. A mistake can often introduce strange and unexpected 267 | behavior. Also, they are inherently hard to debug because of their 268 | highly dynamic nature. 269 | -------------------------------------------------------------------------------- /user_guide/features_scenarios.rst: -------------------------------------------------------------------------------- 1 | Features and Scenarios 2 | ====================== 3 | 4 | .. _user-guide--features-scenarios--features: 5 | 6 | Features 7 | -------- 8 | 9 | Every ``*.feature`` file conventionally consists of a single feature. Lines 10 | starting with the keyword ``Feature:`` (or its localized equivalent) followed 11 | by three indented lines starts a feature. A feature usually contains a list of 12 | scenarios. You can write whatever you want up until the first scenario, which 13 | starts with ``Scenario:`` (or localized equivalent) on a new line. You can use 14 | :ref:`user-guide--organizing-features-and-scenarios--tags` to group features 15 | and scenarios together, independent of your file and directory structure. 16 | 17 | Every scenario consists of a list of 18 | :ref:`user-guide--writing-scenarios--steps`, which must start with one of the 19 | keywords ``Given``, ``When``, ``Then``, ``But`` or ``And`` (or a localized 20 | version of one of these). Behat treats them all the same, but you shouldn't. 21 | Here is an example: 22 | 23 | .. code-block:: gherkin 24 | 25 | Feature: Serve coffee 26 | In order to earn money 27 | Customers should be able to 28 | buy coffee at all times 29 | 30 | Scenario: Buy last coffee 31 | Given there are 1 coffees left in the machine 32 | And I have deposited 1 dollar 33 | When I press the coffee button 34 | Then I should be served a coffee 35 | 36 | In addition to basic :ref:`user-guide--features-scenarios--scenarios`, 37 | features may contain :ref:`user-guide--writing-scenarios--scenario-outlines` and 38 | :ref:`user-guide--writing-scenarios--backgrounds`. 39 | 40 | .. _user-guide--features-scenarios--scenarios: 41 | 42 | Scenarios 43 | --------- 44 | 45 | Scenarios are one of the core Gherkin structures. Every scenario starts with 46 | the ``Scenario:`` keyword (or localized keyword), followed by an optional scenario 47 | title. Each feature can have one or more scenarios and every scenario consists 48 | of one or more :ref:`user-guide--writing-scenarios--steps`. 49 | 50 | The following scenarios each have 3 steps: 51 | 52 | .. code-block:: gherkin 53 | 54 | Scenario: Wilson posts to his own blog 55 | Given I am logged in as Wilson 56 | When I try to post to "Expensive Therapy" 57 | Then I should see "Your article was published." 58 | 59 | Scenario: Wilson fails to post to somebody else's blog 60 | Given I am logged in as Wilson 61 | When I try to post to "Greg's anti-tax rants" 62 | Then I should see "Hey! That's not your blog!" 63 | 64 | Scenario: Greg posts to a client's blog 65 | Given I am logged in as Greg 66 | When I try to post to "Expensive Therapy" 67 | Then I should see "Your article was published." 68 | -------------------------------------------------------------------------------- /user_guide/gherkin.rst: -------------------------------------------------------------------------------- 1 | The Gherkin Language 2 | ==================== 3 | 4 | Behat is a tool to test the behavior of your application, described in a special 5 | language called Gherkin. Gherkin is a 6 | `Business Readable, Domain Specific Language`_ 7 | created specifically for behavior descriptions. It gives you the ability to 8 | remove logic details from behavior tests. 9 | 10 | Gherkin serves as your project's documentation as well as your project's 11 | automated tests. Behat also has a bonus feature: It talks back to you using 12 | real, human language telling you what code you should write. 13 | 14 | .. tip:: 15 | 16 | If you're still new to Behat, jump into the :doc:`/quick_start` first, 17 | then return here to learn more about Gherkin. 18 | 19 | Gherkin Syntax 20 | -------------- 21 | 22 | Like YAML and Python, Gherkin is a whitespace-oriented language that uses 23 | indentation to define structure. Line endings terminate statements (called 24 | steps) and either spaces or tabs may be used for indentation (we suggest you 25 | use spaces for portability). Finally, most lines in Gherkin start with a 26 | special keyword: 27 | 28 | .. code-block:: gherkin 29 | 30 | Feature: Some terse yet descriptive text of what is desired 31 | In order to realize a named business value 32 | As an explicit system actor 33 | I want to gain some beneficial outcome which furthers the goal 34 | 35 | Additional text... 36 | 37 | Scenario: Some determinable business situation 38 | Given some precondition 39 | And some other precondition 40 | When some action by the actor 41 | And some other action 42 | And yet another action 43 | Then some testable outcome is achieved 44 | And something else we can check happens too 45 | 46 | Scenario: A different situation 47 | ... 48 | 49 | The parser divides the input into features, scenarios and steps. Let's walk 50 | through the above example: 51 | 52 | #. ``Feature: Some terse yet descriptive text of what is desired`` starts 53 | the feature and gives it a title. Learn more about ":ref:`user-guide--features-scenarios--features`". 54 | 55 | #. The next three lines (``In order to ...``, ``As an ...``, ``I want to 56 | ...``) provide context to the people reading your feature and describe the 57 | business value derived from the inclusion of the feature in your software. 58 | These lines are not parsed by Behat and don't have a required structure. 59 | 60 | #. ``Scenario: Some determinable business situation`` starts the scenario 61 | and contains a description of the scenario. Learn more about 62 | ":ref:`user-guide--features-scenarios--scenarios`". 63 | 64 | #. The next 7 lines are the scenario steps, each of which is matched to 65 | a pattern defined elsewhere. Learn more about 66 | ":ref:`user-guide--writing-scenarios--steps`". 67 | 68 | #. ``Scenario: A different situation`` starts the next scenario and so on. 69 | 70 | When you're executing the feature, the trailing portion of each step (after 71 | keywords like ``Given``, ``And``, ``When``, etc) is matched to 72 | a pattern, which executes a PHP callback function. You can read more about 73 | steps matching and execution in ":doc:`/user_guide/context/definitions`". 74 | 75 | Gherkin in Many Languages 76 | ------------------------- 77 | 78 | Gherkin is available in many languages, allowing you to write stories 79 | using localized keywords from your language. In other words, if you 80 | speak French, you can use the word ``Fonctionnalité`` instead of ``Feature``. 81 | 82 | To check if Behat and Gherkin support your language (for example, French), 83 | run: 84 | 85 | .. code-block:: bash 86 | 87 | behat --story-syntax --lang=fr 88 | 89 | .. note:: 90 | 91 | Keep in mind that any language different from ``en`` should be explicitly 92 | marked with a ``# language: ...`` comment at the beginning of your 93 | ``*.feature`` file: 94 | 95 | .. code-block:: gherkin 96 | 97 | # language: fr 98 | Fonctionnalité: ... 99 | ... 100 | 101 | This way your features will hold all the information about its content 102 | type, which is very important for methodologies like BDD and also gives 103 | Behat the ability to have multilanguage features in one suite. 104 | 105 | .. _`Business Readable, Domain Specific Language`: http://martinfowler.com/bliki/BusinessReadableDSL.html 106 | -------------------------------------------------------------------------------- /user_guide/initialize.rst: -------------------------------------------------------------------------------- 1 | Initialize a New Behat Project 2 | ============================== 3 | 4 | The easiest way to start using Behat in your project is to call ``behat`` 5 | with the ``--init`` option inside your project directory: 6 | 7 | .. code-block:: bash 8 | 9 | $ vendor/bin/behat --init 10 | 11 | After you run this command, Behat will set up a ``features`` directory 12 | inside your project: 13 | 14 | The newly created ``features/bootstrap/FeatureContext.php`` will have 15 | an initial context class to get you started: 16 | 17 | .. code-block:: php 18 | 19 | // features/bootstrap/FeatureContext.php 20 | 21 | use Behat\Behat\Context\SnippetAcceptingContext; 22 | use Behat\Gherkin\Node\PyStringNode; 23 | use Behat\Gherkin\Node\TableNode; 24 | 25 | class FeatureContext implements SnippetAcceptingContext 26 | { 27 | /** 28 | * Initializes context. 29 | */ 30 | public function __construct() 31 | { 32 | } 33 | } 34 | 35 | All 36 | :doc:`step definitions` 37 | and :ref:`user-guide--testing-features--hooking-into-the-test-process--hooks` 38 | necessary for testing your project against your features will be represented as 39 | methods inside this class. 40 | 41 | .. _user-guide--initialize-a-new-behat-project--suite-initialisation: 42 | 43 | Suite Initialisation 44 | -------------------- 45 | 46 | Suites are a core part of Behat. Any feature of Behat knows about 47 | them and can give you a hand with them. For example, if you defined 48 | your suites in ``behat.yml`` before running ``--init``, it will actually 49 | create the folders and suites you configured, instead of the default ones. 50 | -------------------------------------------------------------------------------- /user_guide/integrations.rst: -------------------------------------------------------------------------------- 1 | Integrations 2 | ============ 3 | 4 | Behat can be integrated with a large number of other projects and frameworks to enhance its 5 | capabilities. We'll mention here the main integrations that may prove useful in your projects: 6 | 7 | Mink 8 | ---- 9 | 10 | Behat can be used to describe the business logic of many different projects, but one of the 11 | main uses is for web applications where it can be used to provide functional testing. `Mink`_ 12 | is a library which lets you control or emulate a web browser and which lets you simulate the 13 | interactions of users with a web page. It supports a number of drivers for tools like 14 | Selenium, BrowserKit, and Chrome DevTools Protocol to implement these capabilities 15 | 16 | `Mink Extension`_ is a Behat extension that lets you interact with Mink from Behat, providing 17 | additional services like ``Sessions`` or ``Drivers`` and providing a number of base step 18 | definitions and hooks for your contexts. See the documentation of the extension for more info 19 | and usage. 20 | 21 | Symfony 22 | ------- 23 | 24 | Symfony integration is provided by the `Symfony Extension`_. This extension provides an 25 | integration with your Symfony project, including the capability to define your contexts as 26 | regular Symfony services, autowiring and autoconfiguring of your contexts and using Mink 27 | with a dedicated Symfony driver that allows you to test your application by interacting 28 | directly with the Symfony kernel without having to create real HTTP requests. See the 29 | documentation of the extension for more info and usage. 30 | 31 | Drupal 32 | ------ 33 | 34 | Drupal integration is provided by the `Drupal Extension`_. This extension provides an 35 | integration with your Drupal project, including using Mink to access your Drupal site using 36 | Guzzle and a number of useful step definitions for common testing scenarios specific to 37 | Drupal. See the documentation of the extension for more info and usage. 38 | 39 | .. _`Mink`: https://mink.behat.org/ 40 | .. _`Mink Extension`: https://github.com/FriendsOfBehat/MinkExtension 41 | .. _`Symfony Extension`: https://github.com/FriendsOfBehat/SymfonyExtension 42 | .. _`Drupal Extension`: https://github.com/jhedstrom/drupalextension 43 | -------------------------------------------------------------------------------- /user_guide/organizing.rst: -------------------------------------------------------------------------------- 1 | Organizing Features and Scenarios 2 | ================================= 3 | 4 | .. _user-guide--organizing-features-and-scenarios--tags: 5 | 6 | Tags 7 | ---- 8 | 9 | Tags are a great way to organize your features and scenarios. Consider this 10 | example: 11 | 12 | .. code-block:: gherkin 13 | 14 | @billing 15 | Feature: Verify billing 16 | 17 | @important 18 | Scenario: Missing product description 19 | 20 | Scenario: Several products 21 | 22 | A Scenario or Feature can have as many tags as you like, just separate them 23 | with spaces: 24 | 25 | .. code-block:: gherkin 26 | 27 | @billing @bicker @annoy 28 | Feature: Verify billing 29 | 30 | .. note:: 31 | 32 | If a tag exists on a ``Feature``, Behat will assign that tag to all 33 | child ``Scenarios`` and ``Scenario Outlines`` too. 34 | -------------------------------------------------------------------------------- /user_guide/writing_scenarios.rst: -------------------------------------------------------------------------------- 1 | Writing Scenarios 2 | ================= 3 | 4 | .. _user-guide--writing-scenarios--steps: 5 | 6 | Steps 7 | ----- 8 | 9 | :ref:`user-guide--features-scenarios--features` consist of steps, also known as 10 | `Givens`_, `Whens`_ and `Thens`_. 11 | 12 | Behat doesn't technically distinguish between these three kind of steps. 13 | However, we strongly recommend that you do! These words have been carefully 14 | selected for their purpose and you should know what the purpose is to get into 15 | the BDD mindset. 16 | 17 | Robert C. Martin has written a `great post`_ about BDD's Given-When-Then concept 18 | where he thinks of them as a finite state machine. 19 | 20 | Givens 21 | ^^^^^^ 22 | 23 | The purpose of the **Given** steps is to **put the system in a known state** before 24 | the user (or external system) starts interacting with the system (in the When 25 | steps). Avoid talking about user interaction in givens. If you have worked with 26 | use cases, givens are your preconditions. 27 | 28 | .. sidebar:: Given Examples 29 | 30 | Two good examples of using **Givens** are: 31 | 32 | * To create records (model instances) or set up the database: 33 | 34 | .. code-block:: gherkin 35 | 36 | Given there are no users on site 37 | Given the database is clean 38 | 39 | * Authenticate a user (an exception to the no-interaction recommendation. 40 | Things that "happened earlier" are ok): 41 | 42 | .. code-block:: gherkin 43 | 44 | Given I am logged in as "Everzet" 45 | 46 | .. tip:: 47 | 48 | It's OK to call into the layer "inside" the UI layer here (in Symfony: talk 49 | to the models). 50 | 51 | .. sidebar:: Using Givens as Data Fixtures 52 | 53 | If you use ORMs like Doctrine or Propel, we recommend using a Given step 54 | with a `tables`_ argument to set up records instead of fixtures. This 55 | way you can read the scenario all in one place and make sense out of it 56 | without having to jump between files: 57 | 58 | .. code-block:: gherkin 59 | 60 | Given there are users: 61 | | username | password | email | 62 | | everzet | 123456 | everzet@knplabs.com | 63 | | fabpot | 22@222 | fabpot@symfony.com | 64 | 65 | Whens 66 | ^^^^^ 67 | 68 | The purpose of **When** steps is to **describe the key action** the user 69 | performs (or, using Robert C. Martin's metaphor, the state transition). 70 | 71 | .. sidebar:: When Examples 72 | 73 | Two good examples of using **Whens** are: 74 | 75 | * Interact with a web page (the Mink library gives you many web-friendly 76 | ``When`` steps out of the box): 77 | 78 | .. code-block:: gherkin 79 | 80 | When I am on "/some/page" 81 | When I fill "username" with "everzet" 82 | When I fill "password" with "123456" 83 | When I press "login" 84 | 85 | * Interact with some CLI library (call commands and record output): 86 | 87 | .. code-block:: gherkin 88 | 89 | When I call "ls -la" 90 | 91 | Thens 92 | ^^^^^ 93 | 94 | The purpose of **Then** steps is to **observe outcomes**. The observations 95 | should be related to the business value/benefit in your feature description. 96 | The observations should inspect the output of the system (a report, user 97 | interface, message, command output) and not something deeply buried inside it 98 | (that has no business value and is instead part of the implementation). 99 | 100 | .. sidebar:: Then Examples 101 | 102 | Two good examples of using **Thens** are: 103 | 104 | * Verify that something related to the Given + When is (or is not) in the 105 | output: 106 | 107 | .. code-block:: gherkin 108 | 109 | When I call "echo hello" 110 | Then the output should be "hello" 111 | 112 | * Check that some external system has received the expected message: 113 | 114 | .. code-block:: gherkin 115 | 116 | When I send an email with: 117 | """ 118 | ... 119 | """ 120 | Then the client should receive the email with: 121 | """ 122 | ... 123 | """ 124 | 125 | .. caution:: 126 | 127 | While it might be tempting to implement ``Then`` steps to just look in the 128 | database – resist the temptation. You should only verify output that is 129 | observable by the user (or external system). Database data itself is 130 | only visible internally to your application, but is then finally exposed 131 | by the output of your system in a web browser, on the command-line or an 132 | email message. 133 | 134 | 135 | And & But 136 | ^^^^^^^^^ 137 | 138 | If you have several ``Given``, ``When`` or ``Then`` steps you can write: 139 | 140 | .. code-block:: gherkin 141 | 142 | Scenario: Multiple Givens 143 | Given one thing 144 | Given another thing 145 | Given yet another thing 146 | When I open my eyes 147 | Then I see something 148 | Then I don't see something else 149 | 150 | Or you can use **And** or **But** steps, allowing your Scenario to read more 151 | fluently: 152 | 153 | .. code-block:: gherkin 154 | 155 | Scenario: Multiple Givens 156 | Given one thing 157 | And another thing 158 | And yet another thing 159 | When I open my eyes 160 | Then I see something 161 | But I don't see something else 162 | 163 | Behat interprets steps beginning with ``And`` or ``But`` exactly the same as all other 164 | steps; it doesn't differentiate between them - you should! 165 | 166 | .. _user-guide--writing-scenarios--backgrounds: 167 | 168 | Backgrounds 169 | ----------- 170 | 171 | Backgrounds allows you to add some context to all scenarios in a single 172 | feature. A Background is like an untitled scenario, containing a number of 173 | steps. The difference is when it is run: the background is run *before each* of 174 | your scenarios, but after your ``BeforeScenario`` 175 | :ref:`user-guide--testing-features--hooking-into-the-test-process--hooks`. 176 | 177 | .. code-block:: gherkin 178 | 179 | Feature: Multiple site support 180 | 181 | Background: 182 | Given a global administrator named "Greg" 183 | And a blog named "Greg's anti-tax rants" 184 | And a customer named "Wilson" 185 | And a blog named "Expensive Therapy" owned by "Wilson" 186 | 187 | Scenario: Wilson posts to his own blog 188 | Given I am logged in as Wilson 189 | When I try to post to "Expensive Therapy" 190 | Then I should see "Your article was published." 191 | 192 | Scenario: Greg posts to a client's blog 193 | Given I am logged in as Greg 194 | When I try to post to "Expensive Therapy" 195 | Then I should see "Your article was published." 196 | 197 | .. _user-guide--writing-scenarios--scenario-outlines: 198 | 199 | Scenario Outlines 200 | ----------------- 201 | 202 | Copying and pasting scenarios to use different values can quickly become 203 | tedious and repetitive: 204 | 205 | .. code-block:: gherkin 206 | 207 | Scenario: Eat 5 out of 12 208 | Given there are 12 cucumbers 209 | When I eat 5 cucumbers 210 | Then I should have 7 cucumbers 211 | 212 | Scenario: Eat 5 out of 20 213 | Given there are 20 cucumbers 214 | When I eat 5 cucumbers 215 | Then I should have 15 cucumbers 216 | 217 | Scenario Outlines allow us to more concisely express these examples through the 218 | use of a template with placeholders: 219 | 220 | .. code-block:: gherkin 221 | 222 | Scenario Outline: Eating 223 | Given there are cucumbers 224 | When I eat cucumbers 225 | Then I should have cucumbers 226 | 227 | Examples: 228 | | start | eat | left | 229 | | 12 | 5 | 7 | 230 | | 20 | 5 | 15 | 231 | 232 | The Scenario Outline steps provide a template which is never directly run. A 233 | Scenario Outline is run once for each row in the Examples section beneath it 234 | (except for the first header row). 235 | 236 | The Scenario Outline uses placeholders, which are contained within 237 | ``< >`` in the Scenario Outline's steps. For example: 238 | 239 | .. code-block:: gherkin 240 | 241 | Given 242 | 243 | Think of a placeholder like a variable. It is replaced with a real value from 244 | the ``Examples:`` table row, where the text between the placeholder angle 245 | brackets matches that of the table column header. The value substituted for 246 | the placeholder changes with each subsequent run of the Scenario Outline, 247 | until the end of the ``Examples`` table is reached. 248 | 249 | .. tip:: 250 | 251 | You can also use placeholders in `Multiline Arguments`_. 252 | 253 | .. note:: 254 | 255 | Your step definitions will never have to match the placeholder text itself, 256 | but rather the values replacing the placeholder. 257 | 258 | So when running the first row of our example: 259 | 260 | .. code-block:: gherkin 261 | 262 | Scenario Outline: Eating 263 | Given there are cucumbers 264 | When I eat cucumbers 265 | Then I should have cucumbers 266 | 267 | Examples: 268 | | start | eat | left | 269 | | 12 | 5 | 7 | 270 | 271 | The scenario that is actually run is: 272 | 273 | .. code-block:: gherkin 274 | 275 | Scenario: Eating 276 | # replaced with 12: 277 | Given there are 12 cucumbers 278 | # replaced with 5: 279 | When I eat 5 cucumbers 280 | # replaced with 7: 281 | Then I should have 7 cucumbers 282 | 283 | Multiline Arguments 284 | ------------------- 285 | 286 | The one line `steps`_ let Behat extract small strings from your steps 287 | and receive them in your step definitions. However, there are times when you 288 | want to pass a richer data structure from a step to a step definition. 289 | 290 | This is what multiline step arguments are designed for. They are written on 291 | lines immediately following a step and are passed to the step definition 292 | method as the last argument. 293 | 294 | Multiline step arguments come in two flavours: `tables`_ or `pystrings`_. 295 | 296 | Tables 297 | ^^^^^^ 298 | 299 | Tables as arguments to steps are handy for specifying a larger data set - 300 | usually as input to a ``Given`` or as expected output from a ``Then``: 301 | 302 | .. code-block:: gherkin 303 | 304 | Scenario: 305 | Given the following people exist: 306 | | name | email | phone | 307 | | Aslak | aslak@email.com | 123 | 308 | | Joe | joe@email.com | 234 | 309 | | Bryan | bryan@email.org | 456 | 310 | 311 | .. attention:: 312 | 313 | Don't confuse tables with `scenario outlines`_ - syntactically 314 | they are identical, but they have a different purpose. Outlines declare 315 | multiple different values for the same scenario, while tables are used to 316 | expect a set of data. 317 | 318 | .. sidebar:: Matching Tables in your Step Definition 319 | 320 | A matching definition for this step looks like this: 321 | 322 | .. code-block:: php 323 | 324 | use Behat\Gherkin\Node\TableNode; 325 | use Behat\Step\Given; 326 | 327 | // ... 328 | 329 | #[Given('the following people exist:')] 330 | public function thePeopleExist(TableNode $table) 331 | { 332 | foreach ($table as $row) { 333 | // $row['name'], $row['email'], $row['phone'] 334 | } 335 | } 336 | 337 | A table is injected into a definition as a ``TableNode`` object, from 338 | which you can get hash by columns (``TableNode::getHash()`` method) or by 339 | rows (``TableNode::getRowsHash()``). 340 | 341 | Pystrings 342 | ^^^^^^^^^ 343 | 344 | Multiline Strings (also known as PyStrings) are useful for specifying a 345 | larger piece of text. The text should be offset by delimiters consisting of 346 | three double-quote marks (``"""``), placed on their own line: 347 | 348 | .. code-block:: gherkin 349 | 350 | Scenario: 351 | Given a blog post named "Random" with: 352 | """ 353 | Some Title, Eh? 354 | =============== 355 | Here is the first paragraph of my blog post. 356 | Lorem ipsum dolor sit amet, consectetur adipiscing 357 | elit. 358 | """ 359 | 360 | .. note:: 361 | 362 | The inspiration for PyString comes from Python where ``"""`` is used to 363 | delineate docstrings, much in the way ``/** ... */`` is used for multiline 364 | docblocks in PHP. 365 | 366 | .. sidebar:: Matching PyStrings in your Step Definition 367 | 368 | In your step definition, there's no need to find this text and match it in 369 | your pattern. The text will automatically be passed as the last 370 | argument into the step definition method. For example: 371 | 372 | .. code-block:: php 373 | 374 | use Behat\Gherkin\Node\PyStringNode; 375 | use Behat\Step\Given; 376 | 377 | // ... 378 | 379 | #[Given('a blog post named :title with:')] 380 | public function blogPost($title, PyStringNode $markdown) 381 | { 382 | $this->createPost($title, $markdown->getRaw()); 383 | } 384 | 385 | PyStrings are stored in a ``PyStringNode`` instance, which you can simply 386 | convert to a string with ``(string) $pystring`` or ``$pystring->getRaw()`` 387 | as in the example above. 388 | 389 | .. note:: 390 | 391 | Indentation of the opening ``"""`` is not important, although common practice 392 | is two spaces in from the enclosing step. The indentation inside the triple 393 | quotes, however, is significant. Each line of the string passed to the step 394 | definition's callback will be de-indented according to the opening ``"""``. 395 | Indentation beyond the column of the opening ``"""`` will therefore be 396 | preserved. 397 | 398 | .. _`great post`: https://sites.google.com/site/unclebobconsultingllc/the-truth-about-bdd 399 | --------------------------------------------------------------------------------