arnapou / behat
Library - Tooling for behat.
Requires
- php: ~8.3.0 || ~8.4.0
- arnapou/appcfg: ^1.7
- behat/behat: ^3.0
- psr/cache: ^2.0 || ^3.0
- psr/clock: ^1.0
- psr/simple-cache: ^2.0 || ^3.0
Requires (Dev)
- ext-yaml: *
- friendsofphp/php-cs-fixer: ^3.52
- phpstan/extension-installer: ^1.3
- phpstan/phpstan: ^2.0
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-phpunit: ^2.0
- phpstan/phpstan-strict-rules: ^2.0
- phpunit/php-code-coverage: ^11.0
- phpunit/phpunit: ^11.0
README
Application config system.
Installation
composer require arnapou/behat
packagist 👉️ arnapou/behat
Introduction
The library mainly provides a BehatTool object you can use in your context to process the data.
It takes advantage of arnapou/appcfg to "compile" arrays with contexts.
This allows us to easily use the appcfg syntax to process data to test matching etc ...
Syntax %<processor>(<label>:<expression>)%
- processor: a way to interpret the label + expression (only
a-zA-Z0-9_
characters) - label: whatever it means for processor, can contain any character (only the
:
needs to be escaped like\:
) - expression (optional): it means for processor, can contain any character (only the
:
needs to be escaped like\:
)
Valid expressions:
%<processor>(<label>)%
: missing expression + colon:
, the expression has "no value".%<processor>(<label>:%<nested_processor>(<nested_label>:<nested_expression>)%)%
: you can have several levels of nested expressions.
More details in the arnapou/appcfg library.
Behat sugar
The BehatTool class contains a bunch of methods to help you with the manipulation of compiled arrays with appcfg.
In order to help matching tests, we added an interface MatchingProcessor you can implement and inject through the ProcessorsProvider interface.
The code is properly isolated. Thus, you can use our gherkin helper classes directly in your behat contexts :
- ArrayHelper : help to fold/unfold arrays + other helpers.
- TableNodeWithLayout : facade for behat
TableNode
, in order to keep the behaviour, format and typing consistent.
To ease testing, the lib contains simple implementations of some PSR:
- PSR-6 with RuntimeCacheItemPool
- PSR-16 with RuntimeSimpleCache
- PSR-20 with FixedClock
Processors
Default
%(array.context.path.value)%
%(array.context.path.value:<default>)%
Environment variable
%env(VARIABLE)%
%env(VARIABLE:<default>)%
Basic cast for static values (shorter syntax alternative)
%int(<value>)%
or%int(:<value>)%
%float(<value>)%
or%float(:<value>)%
%bool(<value>)%
or%bool(:<value>)%
%null()%
Filter: basic cast
%filter(int:<value>)%
or nullable%filter(?int:<value>)%
%filter(float:<value>)%
or nullable%filter(?float:<value>)%
%filter(bool:<value>)%
or nullable%filter(?bool:<value>)%
%filter(string:<value>)%
or nullable%filter(?string:<value>)%
Filter: string functions
%filter(md5:<value>)%
%filter(sha1:<value>)%
%filter(capitalize:<value>)%
%filter(lower:<value>)%
%filter(upper:<value>)%
Filter: string+array functions
%filter(length:<value>)%
Json manipulation
%json(decode:<value>)%
%json(encode:<value>)%
Date manipulation
%date(<format>:<value>)%
%date(Y-m-d\TH\:i\:sP:<value>)%
(escaped colon in format)%date(Y-m-d:10 june next year)%
%date(Y-m-d:1718990615)%
Cache manipulation
%save(<cache_key>:<value_to_save>)%
save value%cache(<cache_key>:<default_value>)%
retrieve value
Value pass through if valid, else null
%regex(<pattern>:<value>)%
: for any string, example%regex(/^\d+$/)%
%between(<min>,<max>:<value>)%
: only for numerics, example%between(10,20)%
Matching processors
%regex(<pattern>)%
: for any string, example%regex(/^\d+$/)%
%between(<min>,<max>)%
: only for numerics, example%between(10,20)%
%undefined(array-key)%
: only for array or object properties, example%undefined()%
examples.feature
This is an example, that means that the FeatureContext
from the feature
folder exists only for this example to be run on the CI.
You may copy code or take ideas from it, it's up to you.
Feature: Examples of what we can achieve
Background:
# For the example, this context is also interpreted and compiled.
# In a real scenario, for example, these context data would probably
# come from the last json response of a http query.
Given a common JSON used as context for all compilation examples
"""
{
"items": [1, 2, 3, 4, 5, 6, 7, 8, 9],
"text": "Hello World",
"date": "%int(:%date(U:2024-06-23T19:03:48+00:00)%)%",
"user": { "name": "John", "age": 20 }
}
"""
Scenario: "Rows" layout
# The compilation is done with the context of the background.
When we compile these "rows"
| column A | column B | column C |
| foo | %items.1% | %(date)% |
| %save(username:%(user.name)%)% | %(items.2)% | %filter(upper:%(text)%)% |
# Basic EQUAL comparison against the previous result
Then the last compiled is equal to these "rows"
| column A | column B | column C |
| foo | %int(2)% | %int(1719169428)% |
| John | %int(3)% | HELLO WORLD |
# Equivalent EQUAL comparison but with raw JSON (not compiled)
And the last compiled is equal to this JSON
"""
[{
"column A": "foo",
"column B": 2,
"column C": 1719169428
},{
"column A": "John",
"column B": 3,
"column C": "HELLO WORLD"
}]
"""
Scenario: "Columns" layout with only 1 column
# The compilation is done with the context of the background.
When we compile these "columns"
| row | foo |
| json | %json(encode:%(user)%)% |
| name | %cache(username)% |
| company | %cache(user.company:unknown)% |
# Basic EQUAL comparison against the previous result
Then the last compiled is equal to these "columns"
| row | foo |
| json | {"name":"John","age":20} |
| name | John |
| company | unknown |
# Equivalent EQUAL comparison but with raw JSON (not compiled)
And the last compiled is equal to this JSON
"""
{
"row": "foo",
"json": "{\"name\":\"John\",\"age\":20}",
"name": "John",
"company": "unknown"
}
"""
Scenario: "Columns" layout with multiple columns
# The compilation is done with the context of the background.
When we compile these "columns"
| name | John | Sue |
| age | %int(20)% | %int(23)% |
| company | Google | Microsoft |
# Equivalent EQUAL comparison but with raw JSON (not compiled)
And the last compiled is equal to this JSON
"""
[{
"name": "John",
"age": 20,
"company": "Google"
},{
"name": "Sue",
"age": 23,
"company": "Microsoft"
}]
"""
Scenario: Matching example
# The table is "unfolded" before compilation, that does the contrary
# of flatten, expanding the paths to keys and sub-keys, ...
When we unfold and compile these "columns"
| users.0.name | %cache(username)% |
| users.0.age | %(user.age)% |
| users.1.name | Sue |
| users.1.age | %int(21)% |
| items.values | %(items)% |
| items.count | %filter(length:%(items)%)% |
| date | %date(Y-m-d:%(date)%)% |
# MATCHING comparison which is OK if it does not trigger an exception.
# You may use either raw values, or matching processors like regex & between.
Then the last compiled is matching these "columns"
| users.0.age | %regex(/^\d+$/)% |
| users.1.age | %between(20,22)% |
| users.2 | %undefined(age)% |
| users | %undefined(2)% |
| date | 2024-06-23 |
| unknown | %undefined()% |
# EQUAL comparison with raw JSON (not compiled)
And the last compiled is equal to this JSON
"""
{
"users": [
{ "name": "John" , "age": 20 },
{ "name": "Sue" , "age": 21 }
],
"items": {
"values": [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ],
"count": 9
},
"date": "2024-06-23"
}
"""
Php versions
Date | Ref | 8.4 | 8.3 |
---|---|---|---|
25/11/2024 | 1.5.x, main | × | × |
23/06/2024 | 1.0 - 1.4 | × |