Add a way to capture mails sent through the mailer.transport service during tests

Created on 28 October 2023, over 1 year ago

Problem/Motivation

Mails sent through the mailer.transport service are dropped by default during test runs. Some tests need to verify whether mails were sent during a test and also their headers / content.

Steps to reproduce

Proposed resolution

Add a way to capture mails sent via mailer.transport during tests.

Remaining tasks

User interface changes

API changes

Data model changes

Release notes snippet

πŸ“Œ Task
Status

Active

Version

11.0 πŸ”₯

Component
MailΒ  β†’

Last updated about 3 hours ago

No maintainer
Created by

πŸ‡¨πŸ‡­Switzerland znerol

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Merge Requests

Comments & Activities

  • Issue created by @znerol
  • πŸ‡¨πŸ‡­Switzerland znerol
  • πŸ‡¨πŸ‡­Switzerland znerol

    Pushed code removed in πŸ“Œ [PP-1] Add symfony mailer transports to DIC Postponed . I think this could be much less complex if we'd just replace the mailer.transport service directly with the capturing mail transport. Then the mailer_dsn config wouldn't have any effect at all and we wouldn't need to override the override it in tests.

  • Status changed to Postponed over 1 year ago
  • πŸ‡¨πŸ‡­Switzerland znerol
  • πŸ‡¨πŸ‡­Switzerland znerol
  • Status changed to Needs review 2 months ago
  • πŸ‡¨πŸ‡­Switzerland znerol
  • Pipeline finished with Failed
    2 months ago
    Total: 233s
    #496649
  • Pipeline finished with Failed
    2 months ago
    Total: 256s
    #496654
  • Pipeline finished with Success
    2 months ago
    Total: 465s
    #496688
  • πŸ‡ΊπŸ‡ΈUnited States smustgrave

    Wasn't sure how to test this one.

    I installed the new test modules and clicked "Send mail"
    Then I checked the db directly and see the mailer_capture entry in key_value table.

    Code wise LGTM

  • πŸ‡¬πŸ‡§United Kingdom adamps

    This looks good.

    It would be interesting to check if this still works in the case that a Contrib module has overridden the service for TransportInterface. It seems like it would, which is good news.

    Which categories of testing does this work for (i.e. Unit, Kernel, Functional)? All 3 categories are likely to be useful.

    There is a similar issue ✨ Framework for testing Symfony Emails Active , and good news is that the proposed solutions are similar. The other issue goes an extra step, proposing a full collection of assert functions for efficiently checking emails, for example: assertBodyContains, assertSubject, assertTo. The functions operate on the email at the front of the queue, and the function readMail() removes an email from the queue and then testing begins on the next one. This reduces the amount of test code substantially compared with each module having to navigate the email array directly. So the other issue can remain as a follow-up - or if Core doesn't want it, then it can remain in Contrib. See MailerTestTrait in DSM+.

  • πŸ‡¬πŸ‡§United Kingdom adamps
  • πŸ‡¬πŸ‡§United Kingdom adamps

    I tried to integrate this patch into DSM+. I found that MailerCaptureTrait::getMails() currently returns SentMessage rather than Email as indicated by the comment. I fixed it as below, removing the reference to AbstractTransport.

    class CaptureTransport implements TransportInterface {
    
      /**
       * {@inheritdoc}
       */
      public function send(RawMessage $message, ?Envelope $envelope = null): ?SentMessage {
        $capturedMails = \Drupal::keyValue('mailer_capture')->get('mails', []);
        $capturedMails[] = $message;
        \Drupal::keyValue('mailer_capture')->set('mails', $capturedMails);
      }
    

    In the tests for this patch we should add some asserts on fields on the email, which would prevent errors like this from being introduced.

  • πŸ‡¬πŸ‡§United Kingdom adamps

    Follow up from #9.

    • It works if a Contrib module has overridden the service for TransportInterface.
    • It works for Kernel and Functional tests

    I like it - it has some clear improvements over the current DSM+. I have made an issue to change DSM+ to match this πŸ“Œ Alter mailer testing to match Core proposal Active . I'll wait until this is committed.

    Also DSM+ has some clear improvements over this. We can use ✨ Framework for testing Symfony Emails Active if we wish to add them to Core.

    I made some detailed comments in the MR.

  • πŸ‡¬πŸ‡§United Kingdom adamps

    Below is a comparison of the code we could have in MailerCaptureTest before and after the trait added in ✨ Framework for testing Symfony Emails Active . The character count reduces from 559 to 264 = 53% reduction, and it is more readable IMO.

    The trait itself is 300 lines, so not a big maintenance burden. Given that, I hope we might add it to core. If so, then it would potentially make sense to adjust this issue to have module mailer_test containing MailerTestTrait.

        // Before
        $capturedEmails = $this->getMails();
        $this->assertCount(0, $capturedEmails, 'The captured emails queue is empty.');
    
        $mailer->send($email);
    
        $capturedEmails = $this->getMails();
        $this->assertCount(1, $capturedEmails, 'One email was captured.');
        $this->assertEquals('admin@example.com', $capturedEmails[0]->getFrom());
        $this->assertEquals('State machine energy a production like service.', $capturedEmails[0]->getSubject());
        $this->assertStringContainsString('environmental along agree let', $capturedEmails[0]->getHtmlBody());
    
        // After
        $this->assertNoMail();
    
        $mailer->send($email);
    
        $this->readMail();
        $this->assertFrom('admin@example.com');
        $this->assertSubject('State machine energy a production like service.');
        $this->assertBodyContains('environmental along agree let');
    
    
    
  • Pipeline finished with Failed
    15 days ago
    Total: 632s
    #535115
  • Pipeline finished with Failed
    15 days ago
    Total: 601s
    #535146
  • Pipeline finished with Success
    15 days ago
    Total: 477s
    #535295
  • πŸ‡¨πŸ‡­Switzerland znerol

    Thanks for the review. Regarding #11, we should stick with AbstractTransport, otherwise transport events will not fire. I've added methods for both cases (getEmails() in order to assertions on the original Email and getCapturedMessages() for assertions on SentMessage).

  • Pipeline finished with Success
    15 days ago
    Total: 1032s
    #535325
  • πŸ‡¬πŸ‡§United Kingdom adamps

    Thanks that looks good. How do you feel about #13?

  • πŸ‡¬πŸ‡§United Kingdom adamps

    Before this gets committed I feel we should discuss #13 please.

    In preparation for ✨ Framework for testing Symfony Emails Active it would likely make sense to adjust this issue to have module mailer_test containing MailerTestTrait. Otherwise the follow on issue creates extra work for us and the core committers by renaming everything.

  • πŸ‡¨πŸ‡­Switzerland znerol

    Sorry, I do not get it. What needs to be renamed and when?

  • πŸ‡¬πŸ‡§United Kingdom adamps

    The module name is currently mailer_capture. This is good for the current use case.

    In ✨ Framework for testing Symfony Emails Active I propose to add some extra functions that I believe will be very useful, see also #13 explaining the benefits. These functions would naturally live in the same module, probably even the same trait. Therefore I propose that we change the module name now to mailer_test, and the trait to MailerTestTrait. The module/trait are responsible for assisting testing of emails, which includes both capturing the email and asserting the values. Developers will almost always want both parts I believe, so we don't need a module for each.

    How do you feel about that??

  • πŸ‡¨πŸ‡­Switzerland znerol

    I think the additional assertions could be useful in unit tests too. For that reason it would be better to keep mail capturing and test assertions separate. The former only works in kernel and browser tests. The latter will work everywhere.

  • πŸ‡¬πŸ‡§United Kingdom adamps

    OK that makes sense.

    Is it OK with you if I add ✨ Framework for testing Symfony Emails Active into the roadmap??

  • πŸ‡¨πŸ‡­Switzerland berdir Switzerland

    I'm not convinced that providing a test API in a test module is the right call here and replacing the transport completely, so essentially I challenge #3. We already customize the mailer_dsn in test setup and expect this to work. I think it will be confusing for example if you want to have your own collector for something or test your own transport (and maybe mock it's internal API or something) and then that won't work. See comments.

  • Pipeline finished with Failed
    9 days ago
    Total: 234s
    #540287
  • πŸ‡¬πŸ‡§United Kingdom adamps

    I think it's strange that we then use a trait from a test module as an API. Because this is an API.

    Is there any harm in just switching to collecting by default?

    +1

    There are two main classes in this issue: the capture transport, and the API for accessing captured emails. We can put these in the natural location as @Berdir suggests - not inside a test module or anything.

    Then we just need a mechanism for enabling the capture transport. It's much less important where this is located because the exact mechanism is internal. It could be a test module, however I feel there is likely a better option. My impression is that the experimental mail plugin is mostly just a proof of concept / placeholder, and maybe we could even delete it when the new mailer lands. Even if we don't, then could we keep testing of the experimental module separate (a minority case) from normal testing, e.g. in with a separate base class hence provide a separate dsn?

  • Pipeline finished with Canceled
    9 days ago
    Total: 221s
    #540324
  • Pipeline finished with Failed
    9 days ago
    Total: 568s
    #540327
  • Pipeline finished with Canceled
    9 days ago
    Total: 185s
    #540335
  • Pipeline finished with Canceled
    9 days ago
    Total: 326s
    #540337
  • Pipeline finished with Success
    9 days ago
    Total: 403s
    #540344
  • πŸ‡¨πŸ‡­Switzerland znerol

    Moved MailerCaptureTrait to core, properly inject the keyvalue service into CaptureTransport.

    Regarding how the transport is enabled, see the comment in the MR. Also keep in mind that the CaptureTransport requires (supports testing of) the experimental mailer module. That alone should be reason enough to not enable it by default.

  • πŸ‡¬πŸ‡§United Kingdom adamps

    Thanks for the updates (not yet checked). Personally I prefer the existing name of AssertMailTrait or similar. It would then be natural to add extra functions with specific asserts in ✨ Framework for testing Symfony Emails Active .

  • πŸ‡¬πŸ‡§United Kingdom adamps

    Looks good to me, thanks @znerol. I will leave in NR until @berdir checks.

    You can ignore my previous remark as we can change the name as part of ✨ Framework for testing Symfony Emails Active (I suggest MailerTestTrait).

  • Pipeline finished with Success
    8 days ago
    Total: 1570s
    #541151
  • πŸ‡¨πŸ‡­Switzerland znerol

    Update and rebase for πŸ› The keyvalue service cannot be autowired Active .

  • Pipeline finished with Success
    7 days ago
    Total: 562s
    #541930
  • πŸ‡¨πŸ‡­Switzerland berdir Switzerland

    I see the problem with the config for the old API vs. the new API and the whole thing being an opt-in through the experimental module.

    Looking through the existing mailer test, we have existing test coverage about not sending mails in \Drupal\Tests\mailer\Functional\TransportServiceFactoryTest, and also a kernel test and both are essentially based on the system.mail:mailer_dsn config, so setting that *does* work. But if I understood correctly, the problem is that setting that explicitly to a "capture" scheme instead of null would not work for \Drupal\Core\Mail\Plugin\Mail\SymfonyMailer. We could work around that with some hardcoded logic that sets capture back to null and say we don't support that there. Or we actually do add support for that in there ourself. It is just another transport.

    That said, I can also live with the module that you have to enable for now, we can re-evaluate that when we make the new mailer API non-experimental and switch existing mail sending and their tests over.

    What we do need is a change record that describes how to do that I think, even if this is experimental. Needs work for that.

  • πŸ‡¨πŸ‡­Switzerland znerol

    Added a section to the existing CR [#3519253].

  • πŸ‡¨πŸ‡­Switzerland berdir Switzerland

    Setting this back to RTBC. Leaving it to a committer to decide on whether or not we want to have this opt-in switch with a test module or a different approach, see #27 and previous discussions.

  • πŸ‡¨πŸ‡­Switzerland znerol

    Slightly extended the issue summary.

    There was an issue link under Remaining tasks which doesn't belong there (a follow up doesn't need to be resolved before this issue can be committed). The follow-up ✨ Framework for testing Symfony Emails Active is linked from the related issue list.

Production build 0.71.5 2024