4 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/_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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/_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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/_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/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/_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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/_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 |
13 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/_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 | }
--------------------------------------------------------------------------------
/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.4
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.49.1
32 | typing_extensions==4.13.1
33 | urllib3==2.6.0
34 | uvicorn==0.34.0
35 | watchfiles==1.0.4
36 | websockets==15.0.1
37 | wheel==0.44.0
38 |
--------------------------------------------------------------------------------
/_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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/_theme/templates/searchbox.html:
--------------------------------------------------------------------------------
1 | {#
2 | This custom searchbox triggers the ReadTheDocs search instead of a local search
3 | #}
4 | {%- if builder != "singlehtml" %}
5 |
6 |
7 |
8 |
9 |
10 |
11 |
16 |
17 |
18 |
24 | {%- endif %}
25 |
--------------------------------------------------------------------------------
/_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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/_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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/_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/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)
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | ``Remove Prefix``: This allows you to define a list of prefixes that need to be removed from paths
25 | when printing them. This affects only the visible paths, not the ones used in the editorUrl. This is
26 | useful if your feature or context files are located in some subfolders, for example ``tests/behat/features``
27 | and ``src/behat``. It is not very informative if these folders are always printed when printing paths
28 | and you may prefer to remove them
29 |
30 | .. note::
31 |
32 | The path style can be different for the visible path and the one used in the editor URL. For example you
33 | might have a visible relative path and use an absolute path in the URL.
34 |
35 | These options can be set using the ``withPathOptions()`` function of the ``Profile`` config object, for example:
36 |
37 | .. code-block:: php
38 |
39 | withProfile((new Profile('default'))
47 | ->withPathOptions(
48 | printAbsolutePaths: true,
49 | editorUrl: 'phpstorm://open?file={relPath}&line={line}',
50 | removePrefix: [
51 | 'tests/behat/features',
52 | 'src/behat',
53 | ]
54 | ));
55 |
56 | They can also be set as command line options (notice that the editor URL will usually need to be quoted):
57 |
58 | .. code-block:: bash
59 |
60 | behat --print-absolute-paths --editor-url="phpstorm://open?file={relPath}&line={line}" --remove-prefix=tests/behat/features,src/behat
61 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/_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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/_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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | `v4.x`_ tbc 2025/6 See below See below `Changelog `__
14 | ======= ========== ========== ============ =======================================================================
15 |
16 | As a minimum, a major version series will receive:
17 |
18 | * Bugfixes for 12 months after the release of the next major.
19 | * Security fixes for 24 months after the release of the next major.
20 |
21 | Each time a new major version is released, the Behat maintainers will set End-of-Life dates for the previous version
22 | series. This will be based on the scale of the breaking changes, the complexity of supporting the older version, and the
23 | likely effort required for users and third-party extensions to upgrade.
24 |
25 | Bugfixes will usually only be applied to the most recent minor of each supported major version, unless they are
26 | particularly severe or have security implications. This will impact
27 | :ref:`support for End-of-Life PHP & Symfony versions`.
28 |
29 | Release timescales
30 | ------------------
31 |
32 | There is no fixed schedule for releasing new major versions - but we will try to keep them to a frequency that is
33 | manageable for users.
34 |
35 | Minor versions
36 | ~~~~~~~~~~~~~~
37 |
38 | Minor & patch versions will be released whenever there is something to release. These releases do not come with any
39 | specific support timescale, and we expect that users will upgrade to the next minor when it becomes available.
40 |
41 | Please bear in mind that this is free software, maintained by volunteers as a gift to users, and the license
42 | specifically explains that it is provided without warranty of any kind.
43 |
44 |
45 | Support for PHP and dependency versions
46 | ---------------------------------------
47 |
48 | Behat only supports current versions of PHP and third-party dependency packages (e.g. Symfony components).
49 |
50 | By "current", we mean:
51 |
52 | * PHP versions that are listed as receiving active support or security fixes
53 | on the `official php.net version support page`_.
54 | * Symfony versions that are listed as maintained or receiving security fixes on the `official Symfony releases page`_.
55 |
56 | Note that Symfony 8 introduces breaking changes to interfaces that Behat cannot support without ourselves making
57 | breaking changes. Therefore, Symfony 8 will only be supported from Behat 4.0 onwards.
58 |
59 | Once a PHP or Symfony version reaches End of Life we will remove it from our composer.json and CI flows.
60 |
61 | .. note::
62 | When we drop support for a PHP / dependency version we will highlight this in the CHANGELOG, but we will treat
63 | it as a minor release. Composer will automatically protect users from upgrading to a version that does not support
64 | their environment. Users running Behat as a ``.phar`` should review the release notes before downloading
65 | a new version.
66 |
67 | We will not ship bugfix releases for unsupported PHP / dependency versions, unless:
68 |
69 | * It fixes a security vulnerability within the security support period for a Behat major version.
70 | * An external contributor wishes to take on the work of backporting, including any changes required
71 | to get a green build in CI.
72 |
73 | End-of-Life versions
74 | --------------------
75 |
76 | These behat series are no longer maintained and will not receive any further releases. We strongly recommend that users
77 | upgrade to a supported version as soon as possible.
78 |
79 | ======= ========== ============ ============ =====================================================================
80 | Major Released Bugfix EOL Security EOL
81 | ======= ========== ============ ============ =====================================================================
82 | `v2.x`_ July 2011 June 2015 June 2015 `Changelog `__
83 | ======= ========== ============ ============ =====================================================================
84 |
85 |
86 | .. _`Semantic Versioning`: http://semver.org/
87 | .. _`official php.net version support page`: https://www.php.net/supported-versions.php
88 | .. _`official Symfony releases page`: https://symfony.com/releases
89 | .. _`v2.x`: https://github.com/Behat/Behat/releases?q=v2
90 | .. _`v3.x`: https://github.com/Behat/Behat/releases?q=v3
91 | .. _`v4.x`: https://github.com/Behat/Behat/releases?q=v4
92 |
--------------------------------------------------------------------------------
/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 | [](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 | [](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* | [](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 |
--------------------------------------------------------------------------------
/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 | * ``json`` - prints the output to a json file in json format.
31 |
32 | You can see the schema of this json file in the `Schema`_ definition in GitHub
33 |
34 | If you don't want to print output to the console, you can tell Behat
35 | to print output to a file instead of ``STDOUT`` with the ``--out`` option:
36 |
37 | .. code-block:: bash
38 |
39 | $ behat --format pretty --out report.txt
40 |
41 | .. note::
42 |
43 | Some formatters, like ``junit`` or ``json``, always require the ``--out`` option to be
44 | specified. The ``junit`` formatter generates ``*.xml`` files for every
45 | suite, so it needs a destination directory to put these XML files into. The ``json`` formatter
46 | outputs a single file, so it needs the path of this file (which will be created if it does
47 | not exist)
48 |
49 | Also, you can specify multiple formats to be used by Behat using multiple --format options:
50 |
51 | .. code-block:: bash
52 |
53 | $ behat --format pretty --format progress
54 |
55 | In this case, default output will be used as output for both formatters. But if you want
56 | them to use different ones - specify them with ``--out``:
57 |
58 | .. code-block:: bash
59 |
60 | $ behat -f pretty -o ~/pretty.out -f progress -o std
61 | -f junit -o xml
62 |
63 | In this case, output of pretty formatter will be written to ``~/pretty.out`` file, output of junit
64 | formatter will be written to ``xml`` folder and progress formatter will just print to console.
65 |
66 | Behat tries hard to identify if your terminal supports colors or not, but
67 | sometimes it still fails. In such cases, you can force Behat to
68 | use colors (or not) with the options ``--colors`` or ``--no-colors``,
69 | respectively:
70 |
71 | .. code-block:: bash
72 |
73 | $ behat --no-colors
74 |
75 | Format Options
76 | --------------
77 |
78 | The formatters can be configured with some options. The following options are available for
79 | all formatters:
80 |
81 | * ``output_verbosity`` indicates the level of detail of the output. Use one of the ``OutputFactory::*`` constants
82 | * ``output_path`` indicates the path where the output should be saved. Equivalent to the ``--out`` command
83 | line option. Should be a file or folder, depending on the formatter.
84 | * ``output_decorate`` determines whether the output generated by Behat is "decorated" with formatting,
85 | such as colors, bold text, or other visual enhancements. Should be a boolean, defaults to true.
86 | * ``output_styles`` can be used to override the default styles used by Behat to display the different output
87 | elements. It should be an array where the key is the style that needs to be overridden and which points to an array of
88 | three values. The first one is the foreground color, the second one the background color and the third one an array of
89 | optional styles.
90 |
91 | The styles available for redefinition are:
92 |
93 | * ``keyword`` style of Gherkin keywords
94 | * ``stdout`` style of stdout output
95 | * ``exception`` style of exceptions
96 | * ``undefined`` style of undefined steps
97 | * ``pending`` style of pending steps
98 | * ``pending_param`` style of pending step params
99 | * ``failed`` style of failed steps
100 | * ``failed_param`` style of failed step params
101 | * ``passed`` style of passed steps
102 | * ``passed_param`` style of passed steo params
103 | * ``skipped`` style of skipped steps
104 | * ``skipped_param`` style of skipped step params
105 | * ``comment`` style of comments
106 | * ``tag`` style of scenario/feature tags
107 |
108 | Available colors for first two arguments (``fg`` and ``bg``) are: ``black``, ``red``, ``green``, ``yellow``,
109 | ``blue``, ``magenta``, ``cyan`` and ``white``.
110 |
111 | Available optional styles are: ``bold``, ``underscore``, ``blink``, ``reverse`` and ``conceal``
112 |
113 | Pretty formatter
114 | ^^^^^^^^^^^^^^^^
115 |
116 | The following options are specific to the Pretty formatter:
117 |
118 | * ``timer`` show time and memory usage at the end of the test run. Boolean, defaults to true.
119 | * ``expand`` print each example of a scenario outline separately. Boolean, defaults to false.
120 | * ``paths`` display the file path and line number for each scenario and the context file and method for each step.
121 | Boolean, defaults to true.
122 | * ``multiline`` print out PyStrings and TableNodes in full. Boolean, defaults to true.
123 | * ``showOutput`` show the test stdout output as part of the formatter output. Should be one of the
124 | ``ShowOutputOption`` enum values, defaults to ``ShowOutputOption::Yes``.
125 | * ``shortSummary`` show just a list of failing scenarios at the end of the output. If false, a full summary
126 | (which also includes a list of failing steps) will be printed. Defaults to true
127 | * ``printSkippedSteps`` If the output should include any steps which are skipped by the runner. Useful when you
128 | don't want to see all the skipped steps after a failed step. Defaults to true
129 |
130 | Progress formatter
131 | ^^^^^^^^^^^^^^^^^^
132 |
133 | The following options are specific to the Progress formatter:
134 |
135 | * ``timer`` show time and memory usage at the end of the test run. Boolean, defaults to true.
136 | * ``showOutput`` show the test stdout output as part of the formatter output. Should be one of the
137 | ``ShowOutputOption`` enum values, defaults to ``ShowOutputOption::InSummary``.
138 | * ``shortSummary`` show just a list of failing scenarios at the end of the output. If false, a full summary
139 | (which also includes a list of failing steps) will be printed. Defaults to false
140 |
141 | JUnit formatter
142 | ^^^^^^^^^^^^^^^
143 |
144 | The following options are specific to the JUnit formatter:
145 |
146 | * ``timer`` show time spent in each scenario and feature. Boolean, defaults to true.
147 |
148 |
149 | JSON formatter
150 | ^^^^^^^^^^^^^^
151 |
152 | The following options are specific to the JSON formatter:
153 |
154 | * ``timer`` show time spent in each scenario, feature and suite. Boolean, defaults to true.
155 |
156 | Setting format options
157 | ^^^^^^^^^^^^^^^^^^^^^^
158 |
159 | Format options can be set using the ``withFormatter()`` function of the ``Profile`` PHP config class. For example:
160 |
161 | .. code-block:: php
162 |
163 | use Behat\Config\Config;
164 | use Behat\Config\Profile;
165 | use Behat\Config\Formatter\PrettyFormatter;
166 |
167 | $profile = (new Profile('default'))
168 | ->withFormatter((new PrettyFormatter(paths: false))
169 | ->withOutputStyles([
170 | 'comment' => [
171 | 'black', 'white',
172 | ['underscore', 'bold']
173 | ]
174 | ])
175 | )
176 | ;
177 |
178 | return (new Config())->withProfile($profile);
179 |
180 | These options can also be set on the command line by using the
181 | ``--format-setting`` option which accepts a json object with this configuration. For example:
182 |
183 | .. code-block:: bash
184 |
185 | $ behat --format-settings='{\"paths\": false}'
186 |
187 | Disabling a formatter
188 | ^^^^^^^^^^^^^^^^^^^^^
189 |
190 | You can disable a formatter so that it won't be available by using the ``disableFormatter()`` function of the
191 | ``Profile`` PHP config class. For example:
192 |
193 | .. code-block:: php
194 |
195 | use Behat\Config\Config;
196 | use Behat\Config\Profile;
197 | use Behat\Config\Formatter\PrettyFormatter;
198 |
199 | $profile = (new Profile('default'))
200 | ->disableFormatter(PrettyFormatter::NAME)
201 | ;
202 |
203 | return (new Config())->withProfile($profile);
204 |
205 | .. _`Schema`: https://github.com/Behat/Behat/blob/master/resources/schema.json
206 |
207 |
208 |
209 |
210 |
211 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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.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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------