Skip to content

Behat extension ideas #1051

@nikophil

Description

@nikophil

Here are the features I think a Behat extension for Foundry should have.

@aegypius @mpdude @mellowgrab @kevin-schmitt @roboticflamingo any thoughts about this?

Any help about the API and the naming of the Given, When, Then definitions would be appreciated

1. Create objects with PyTable

Create one object

Given a post A is created with properties
    | name            | state     | published_at |
    | my awesome post | published | <now()>      |

Warning

Maybe a Gherkin definition like a post A is created with properties is too generic and would create conflicts with contexts from userland?

Note

We need to find a way to resolve the object name to a factory class:
"post" => PostFactory, "post category" => PostCategoryFactory

Solutions could be:

  • Automagically resolve the factory name from the class's shortname, based on camelCase. If a conflict is detected, we should throw an exception or fallback on another solution
  • introduce an attribute #[FactoryName('post')]
  • possibility to configure this in behat.yml:
    extensions:
      Zenstruck\Foundry\Test\Behat\FoundryExtension:
        factories:
          App\Factory\PostFactory: post
          App\Factory\PostCategoryFactory: post category

Create multiple objects

Given posts are created with properties
    | _ref | name                  | state     | published_at |
    | A    | my awesome post A     | published | <now()>      |
    | B    | my awesome post B     | draft     | <now()>      |

Note

We need a way to handle pluralized names:

  • use an inflector
  • Handle it in the attribute: #[FactoryName(shortName: 'post', plural: 'posts')]
  • Or in behat.yml
    extensions:
        Zenstruck\Foundry\Test\Behat\FoundryExtension:
          factories:
            App\Factory\PostFactory: {shortName: post, plural: posts}

Can reference another object when creating an object

Given a post category A is created with properties
    | name                | 
    | my awesome category | 

Given a post A is created with properties
    | name            | category |
    | my awesome post | A        |

2. Create objects with natural language

This is the more challenging feature

Create an object with zero or one property

Given a post A is created
Given a post A is created with name "awesome post"

Warning

This definition a post A is created is even more generic than the previous ones.
I think that we should find something more specific.
Maybe a fixture post named A is created? a post A is created by Foundry?

Create an object and reuse it in a later step

Given a post A is created
Given the post A has property "name" "awesome post"
Given this post has property "state" "published"

Note

This would mean that the objects are not created right away at the first line
So we need to store the factories somewhere, and create the objects before the first When

Reference an object in a later step

Given a post category A is created
And a post A is created
And the post A has category "post category B"

Note

We should detect that Post::$category is an object, and not a string
and then, we need to use a factory

Warning

In order not to create multiple categories in this example, we will need to leverage lazy() or memoize() helpers

Be able to call factory state methods

Given this factory:

class PostFactory extenbds PersistentObjectFactory
{
    public function publishedAWeekAgo(): self
    {
        return $this->with([
            'published' => true,
            'published_at' => now('-1 week'),
        ]);
    }
}

We should be able to reference to the "state method" in a later step:

Given a post A is created
Given the post A was published a week ago

Warning

I don't like this syntax, we must find something better

Note

We'd also need a way to pass one or more arguments to the state method

Note

The state method could be resolved automagically,
but we should also add a way to configure it, with an attribute #[FactoryStateMethod('published a week ago')]

3. Create objects within a Story

Given this story:

class PostStory extends Story
{
    public function build(): void
    {
        $this->addState('my blog post', PostFactory::createOne());
    }
}
@withStory postStory
When I do something...

Note

Once again, we need to resolve the story from this short name
This could be done automagically, from the class's short name
or we could leverage #[AsFixture()] attribute

4. Provide a way to access the created object in When and Then steps

Foundry could expose some sort of "objects registry", where we can store the created objects.

Given a post A is created
use Zenstruck\Foundry\Test\Behat\ObjectsRegistry;

class FoundryContext
{
    public function __construct(
        private ObjectsRegistry $objectsRegistry
    ) {}
    
    #[Given('a :object is created')]
    public function createObject(string $objectName): void
    {
        $object = // somehow create the object with Foundry
        
        $this->objectsRegistry->store($objectName, $object);
    }
}

This object registry could be injected in userland contexts, and be used to easily "act" and "assert" on the created objects.

Note

Objects created within a story should also be accessible from the registry

Note

Of course, the object registry should handle name conflicts

Note

The object registry should be reset between scenarios (or features? or on demand?)

5. Provide a way to make simple assertions on the created objects

Given a post A is created with properties
    | name            | state     | published_at |
    | my awesome post | published | <now()>      |

# this would use a userland context
When I unpublish the post A

# From Foundry extension
Then the post A property "state" is "unpublished"
Then the post A properties are now
    | state       | unpublished_at |
    | unpublished | <now()>        |

6. Reset the database

We should provide a way to reset the database between scenarios (or between features? or on demand?) the same way as
ResetDatabase trait does in PHPUnit.

The database reset could be configured to run automatically, this would be configured by the extension configuration:

extensions:
  Zenstruck\Foundry\Test\Behat\FoundryExtension:
    reset_database: before_scenario # or before_feature

(or maybe this should be done in the bundle's configuration?)

And for maximum flexibility, it could also be triggered manually, with a tag

Feature something
    @resetDB
    Scenario do something with fresh DB
    Given / When / Then

    Scenario another scenario using the same DB
    Given / When / Then

    @resetDB
    Scenario do something with fresh DB
    Given / When / Then

The reset database mechanism should work with and without dmaicher/doctrine-test-bundle

Warning

This will be problematic if we want the database to be reset at another pace than before every scenario, because dama
rollbacks the transaction after each scenario
and this is not configurable.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions