0

I am writing a Behat Scenario that checks that an email generated by the Drupal system under test is sent to the correct recipient but I [could not] capture the mail messages from test_mail_collector. How do I do this?

[Update]
The code below is essentially my current working version and should work for you ... at least as long as you don't have an anti-spam module interceding ... in which case see the extra code in my answer.

In FeatureContext.php I switch over to the mail system sender test_mail_collector in the @BeforeScenario tagged with @email and revert to the site's original mail system sender in the @AfterScenario tagged with @email.

However, [before I discovered my answer, there were] never any messages in the array returned by \Drupal::state()->get('system.test_mail_collector') when a scenario should generate them,

Here's a typical scenario:

...
@email
Scenario: Send an email when a user resets her password
Given users:
  | name    | mail           | status |
  | Jo User | [email protected] | 1      |
When I visit "user/password"
And I fill in "name" with "[email protected]" 
When I press the "Submit" button
Then an email should be sent to "[email protected]"

And here are the FeatureContext.php snippets:

...
/**
*  @Then an email should be sent to :recipient
*/
public function anEmailShouldBeSentTo($recipient)
{
  // Reset state cache.
  \Drupal::state()->resetCache();
  $mails = \Drupal::state()->get('system.test_mail_collector') ?: array();
  $last_mail = end($mails);
  if(!$last_mail) {
    throw new Exception('No mail was sent.');
  }
  if ($last_mail['to'] != $recipient) {
    throw new \Exception("Unexpected recpient: " . $last_mail['to']);
  }
  $this->lastMail = $last_mail;
}

/** 
 *  @BeforeScenario @email
 */
public function beforeEmailScenario(BeforeScenarioScope $scope)
{
  $this->m_config = \Drupal::configFactory()->getEditable('mailsystem.settings');
  $this->saved_mailsystem_defaults = $this->m_config->get('defaults'); 
  $this->m_config->set('defaults.sender', 'test_mail_collector')->save();
  \Drupal::state()->set('system.test_mail_collector', array());
}

/** 
 *  @AfterScenario @email 
 */
public function afterEmailScenario(AfterScenarioScope $scope)
{
  // revert mail system after scenarios agged with @email
  $this->m_config->set('defaults.sender', $this->saved_mailsystem_defaults['sender'])->save();
}

The website is normally set up to run with swiftmailer enabled but I have tried all the other available mail system formatters and senders - like Default PHP Mailer - without success.

As a kind of "proof of setup" within FeatureContext.php, I can send test messages to the test_mail_collector:

$language_interface = \Drupal::languageManager()->getCurrentLanguage();
\Drupal::service('plugin.manager.mail')
    ->mail('test', 'test', '[email protected]', $language_interface->getId());

This shows me that the test_mail_collector is working - but messages generated from the website [were] not being collected.

2

3 Answers 3

1

Ensure you don't have anti-spam modules installed and doing their job!

The @BeforeScenario and @AfterScenario code in the original question is correct except in the case where you have an anti-spam module that is interfering with a Scenario.

For example, if you've got honeypot installed, ensure that you disable the time_limit setting. time_limit is there to protect the Drupal site against spambots .... and test programs! ... on the assumption that humans can't enter stuff in less than a few seconds whereas bots and test programs do so almost instantaneously.

So, if you, as I did, have honeypot installed, then you will need to temporarily disable time_limit for the duration of the test. For example my @BeforeScenario and @AfterScenario code is now as follows:

/** 
   *  @BeforeScenario @email
   */
  public function beforeEmailScenario(BeforeScenarioScope $scope)
  {
    $this->m_config = \Drupal::configFactory()->getEditable('mailsystem.settings');
    $this->saved_mailsystem_defaults = $this->m_config->get('defaults'); 
    $this->m_config
      ->set('defaults.sender', 'test_mail_collector')
      ->set('defaults.formatter', 'test_mail_collector')
      ->save();
    // Reset the state variable that holds sent messages.
    \Drupal::state()->set('system.test_mail_collector', array());

    // If we have honeypot installed then ensure that we disable time_limit
    // So that automated tests / bots can run  
    $this->h_config = \Drupal::configFactory()->getEditable('honeypot.settings');
    $this->saved_honeypot_time_limit = $this->h_config->get('time_limit');
    if ($this->saved_honeypot_time_limit) {
      $this->h_config
        ->set('time_limit', '0')
        ->save();
    }
  }

  /** 
   *  @AfterScenario @email 
   */
  public function afterEmailScenario(AfterScenarioScope $scope)
  {
    // revert mail system after scenarios agged with @email
    $this->m_config
      ->set('defaults.sender', $this->saved_mailsystem_defaults['sender'])
      ->set('defaults.formatter', $this->saved_mailsystem_defaults['formatter'])
      ->save();

    // Ensure we protect against spambots again if honeypot is installed
    if ($this->saved_honeypot_time_limit) {
      $this->h_config
        ->set('time_limit', $this->saved_honeypot_time_limit)
        ->save();
    }
  }
1

If an anti-spam module like honeypot is disabling a test because it detects that the form completion is too fast to be human, consider adding a delay to your test steps.

When I visit "user/password"
And I wait for 2 seconds
And I fill in "name" with "[email protected]" 
And I wait for 1 second
When I press the "Submit" button
Then an email should be sent to "[email protected]"

I suggest this gives a simpler and more realistic test than messing with the anti-spam settings. You'll need to add a step definition something like:

/**
 * @Given /^I wait ([0-9^"]*) seconds$/
 */
public function iWaitSeconds($wait) {
  for ($i = 0; $i < $wait; $i++) {
    sleep(1);
  }
}

(Borrowed from http://fuseinteractive.ca/blog/test-wrangling#.VqS6J_mLQYs, also see http://docs.behat.org/en/v2.5/cookbook/using_spin_functions.html)

1
  • Yes, the wait will be useful in other cases too. Thanks
    – iainH
    Jan 24, 2016 at 12:18
0

The above configuration did not work for me properly as it failed to swap the mail handler plugin to the test one. According to https://api.drupal.org/api/drupal/core%21modules%21system%21src%21Tests%21Mail%21MailTest.php/8.2.x?no_cache=1475753055 the following worked for me

/**
   * Swaps the mailing system settings with a test one.
   *
   * @BeforeScenario @email
   */
  public function beforeEmailScenario() {
    $this->mailConfig = \Drupal::configFactory()->getEditable('system.mail');
    $this->savedMailDefaults = $this->mailConfig->get('interface.default');
    $this->mailConfig->set('interface.default', 'test_mail_collector')->save();
    \Drupal::state()->set('system.test_mail_collector', array());
  }

  /**
   * Restores the mailing system settings with the default one.
   *
   * @AfterScenario @email
   */
  public function afterEmailScenario() {
    $this->mailConfig->set('interface.default', $this->savedMailDefaults)->save();
  }

These are only the before and after scenario. The rest are working fine. Also, if you want to use the test plugin to work only for a specific case, you can use 'interface.mail_key' instead replacing the 'mail_key' with the mail key of your module.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.