Transformers in Behat

Warning: This post is over a year old. The information may be out of date.

Transformers can provide a lot of helpful utility in Behat, reducing the work required to get what you need within your step definitions.

Say you have the following step in your .feature file:

Then I should have 2 products in the basket

Under the surface, you might have a step definition that looks like this:

/**
 * @Then I should have :count product(s) in the basket
 */
public function iShouldHaveNProductsInTheBasket($count)
{
    Assert::assertCount($count, $this->basket);
}

Now, the problem here, is that PHPUnit’s Assertion assertCount() will complain that $count is not an integer and you’ll get an error:

Argument #1 (No Value) of PHPUnit\Framework\Assert::assertCount() must be a integer (PHPUnit\Framework\Exception)

We could address this by casting it to an integer whenever we use it:

/**
 * @Then I should have :count product(s) in the basket
 */
public function iShouldHaveNProductsInTheBasket($count)
{
    Assert::assertCount((int) $count, $this->basket);
}

But ultimately, this feels:

  1. Unnecessary - why should we have to do this every single time?
  2. Tedious - someone will forget at some point, and it’ll just be frustrating and a bad developer experience to have to go in and add it.

Transformers to the rescue!

Instead, we can add a transformer to look for a given argument in turnip syntax, or regex:

<?php

namespace Transformers;

trait CountToNumberTransformer
{

    /**
     * @Transform :count
     */
    public function castCountToInteger(string $count): int
    {
        return (int) $count;
    }
}

The transformer above will look for any step definition with :count as an argument. To use it, you’d just use the trait in your context class.

Alternatively, if you wanted to transform any number found (remember, it’ll come out as a string though) into an integer, you could do:

<?php

namespace Transformers;

trait StringToNumberTransformer
{

    /**
     * @Transform /^(\d+)$/
     */
    public function castStringToNumber(string $number): int
    {
        return (int) $number;
    }
}

Personally, I prefer the first approach using the turnip syntax using :count. Transformers can be hard to debug, and a small mistake with regex can lead to all sorts of weird and interesting things happening. I generally avoid regex where possible and stick to the turnip syntax in most places when working with Behat. It’s a personal preference thing, and I can absolutely see the benefit of regex in certain circumstances.

Getting more advanced

Casting to types isn’t all a transformer can do. You could return an object constructed from the string in the argument:

<?php

namespace Transformers;

use App\Entity\Product;

trait ProductNameToProductEntityTransformer
{

    /**
     * @Transform :product
     */
    public function castProductNameToProduct(string $name): Product
    {
        return new Product($name);
    }
}

Or, even get something from the database if you’re working with one:

<?php

namespace Transformers;

use App\Entity\Product;

trait ProductEntityFromDatabaseTransformer
{

    /**
     * @Transform :product
     */
    public function castProductNameToProductEntity(string $name): Product
    {
        /* setup $this->productRepository however you might do in your framework */
        $product = $this->productRepository->findOneBy(['name' => $name]);

        if ( ! $product) {
            throw new \Exception(sprintf('Cannot find product with name %s', $name));
        }

        return $product;
    }
}

Now if we need a product in our step definitions and our contexts are using this transformer, we know we’ll just have a product entity to work with that’s been retrieved from the database. If not, an exception will be thrown and the test would fail (which is what you’d want, if you can’t find a product you’re expecting to work with, you’ve got something that needs fixing!)

This post is another tagged being-awesome-with-behat - I’m using it as a place to post information I find useful as I work with Behat regularly.