eiriksm.dev: Creating a Behat step definition with arguments over multiple lines

Here is a quick tip if you want to create a step definition that has an argument with multiple lines. A multiline string argument if you like.

I wanted to test that an email was sent, with a specific subject, to a specific person, and containing a specific body text.

My idea was to create a step definition that looked something like this:

Then an email has been sent to "user@example.com" with the subject "Subject example" and the body “one of the lines in the body

plus this is the other line of the body, after an additional line break”

So basically my full file is now this:

@api @test-feature
Feature: Test this feature
  Scenario: I can use this definition
    Then an email has been sent to "user@example.com" with the subject "Subject example" and the body “one of the lines in the body

    plus this is the other line of the body, after an additional line break”

My step definition looks like this:

  /**
   * @Then /^an email has been sent to :email with the subject :subject and the body :body$/
   */
  public function anEmailHasBeenSentToWithTheSubjectAndTheBody($email, $subject, $body) 
  {
      throw new PendingException();
  }

Let’s try to run that.

$ ./vendor/bin/behat --tags=test-feature

In Parser.php line 393:
                                                                                                                                                    
  Expected Step, but got text: "    plus this is the other line of the body, after an additional line break”" in file: tests/features/test.feature  

Doing it that way simply does not work. You see, by default a line break in the Gherkin DSL has an actual meaning, so you can not do a line break in your argument, expecting it to just pass along everything up until the closing quote. What we actually want is to use a PyString. But how do we use them, and how do we define a step to receive them? Let’s start by converting our step definition to use the PyString multiline syntax:

@api @test-feature
Feature: Test this feature
  Scenario: I can use this definition
    Then an email has been sent to "user@example.com" with the subject "Subject example" and the body
    """
    one of the lines in the body

    plus this is the other line of the body, after an additional line break
    """

Now let’s try to run it:

$ ./vendor/bin/behat --tags=test-feature                                                                                        
@api @test-feature
Feature: Test this feature

  Scenario: I can use this definition                                                                 # tests/features/test.feature:3
    Then an email has been sent to "user@example.com" with the subject "Subject example" and the body
      """
      one of the lines in the body
      
      plus this is the other line of the body, after an additional line break
      """

1 scenario (1 undefined)
1 step (1 undefined)
0m0.45s (32.44Mb)

 >> default suite has undefined steps. Please choose the context to generate snippets:

A bit closer. Our output actually tells us that we have a missing step definition, and suggests how to define it. That’s better. Let’s try the suggestion from the output, now defining our step like this:

  /**
   * @Then an email has been sent to :email with the subject :subject and the body
   */
  public function anEmailHasBeenSentToWithTheSubjectAndTheBody2($email, $subject, PyStringNode $string)
  {
      throw new PendingException();
  }

The difference here is that we do not add the variable name for the body in the annotation, and we specify that we want a PyStringNode type parameter last. This way behat will know ™.

After running the behat command again, we can finally use the step definition. Let’s have a look at how we can use the PyString class.

  /**
   * @Then an email has been sent to :email with the subject :subject and the body
   */
  public function anEmailHasBeenSentToWithTheSubjectAndTheBody2($email, $subject, PyStringNode $string)
  {
      // This is just an example.
      $mails = $this->getEmailsSomehow();
      // This is now the important part, you get the raw string from the PyStringNode class.
      $body_string = $string->getRaw();
      foreach ($mails as $item) {
          // Still just an example, but you probably get the point?
          if ($item['to'] == $mail && $item['subject'] == $subject && strpos($item['body'], $body_string) !== FALSE) {
              return;
          }
      }
      throw new Exception('The mail was not found');
  }

And that about wraps it up. Writing tests are fun, right? As a bonus, here is an animated gif called “Testing”.