Testing signup flow with activation by email
Tags: testing, javascript
Published: 2019-07-09 10:21:00

Functional testing isn’t something new. We all do it, less or more, with different tools and approaches. However when it comes to flows, where transactional emails (signup confirmations, password resets, purchase notifications and others) involved that may still bring questions. For example, we instruct the testing tool to navigate to the registration page, fill out the form and press the submit button. The web-application sends email with activation link. So we need the testing tool to read the email message, parse it and navigate the link. The first challenge is to connect the testing tool with the mail server. It’s not a big deal if your mail server exposes a REST API. Otherwise you need to consider a specialized service such as [Sendgrid](https://sendgrid.com/ "Sendgrid"), [Mailgun](https://www.mailgun.com/ "Mailgun"), [Email Yak](http://www.emailyak.com/ "Email Yak"), [Postmark](http://www.postmarkapp.com/ "Postmark").

  

Mail Server API
---------------

To say truth, it can be also achieved with [Restmail.net](http://restmail.net/ "Restmail.net"). It's free, it's requires no registration, it allows to create dynamically inboxes, it exposes a REST API to read received emails. However all the sent messages are public. The REST API is dead simple:

```
    GET /mail/<user>
    DELETE /mail/<user>
```

So you can send an email to, let’s say, `joe1@restmail.net` and receive its contents with `GET /mail/joe1`. Naturally you can delete it afterwards with `DELETE /mail/joe1`

  

Polling Inbox
-------------

Well, but how we can use it in test cases? We need a function, which polls mail server API for inbox updates. The function shall find the email messages sent during the testing session, parse the action link and return it for testing methods. I suggest the following implementation:

```
     function pollForValue({ url, interval, timeout, parserFn, parserPayload = {}, requestFn = null }) {
        const request = requestFn ? requestFn : async ( url ) => {
          const rsp = await fetch( url );
          if ( rsp.status < 200 || rsp.status >= 300  ) {
            return {};
          }
          return await rsp.json();
        };
      
        return new Promise(( resolve, reject ) => {
          const startTime = Date.now();
          pollForValue.attempts = 0;
      
          async function attempt() {
            if ( Date.now() - startTime > timeout ) {
              return reject( new Error( `Polling: Exceeded timeout of ${ timeout }ms` ) );
            }
            const value = parserFn( await request( url ), parserPayload );
            pollForValue.attempts ++;
            if ( !value ) {
              return setTimeout( attempt, interval );
            }
            resolve( value );
          }
          attempt();
      
        });
     }
```

As you call the function it polls a given URL until message(s) received or timeout. It returns the parsed value (e.g. activation link) and accepts an options object with the following properties:

*   `url` – REST API resource. Here [http://restmail.net/mail/](http://restmail.net/mail/<user>)
*   `interval` – interval between polling requests in ms
*   `timeout` – maximal allowed time span for the function to loop in ms
*   `parserFn` – callback that receives the REST API response and parses it for the desired value. The pollForValue function will poll the provided URL until parserFn returns a truthy value (or timeout)
*   `requestFn` – (OPTIONAL) a callback to replace default window.fetch
*   `parserPayload` - (OPTIONAL) extra payload for parserFn callback

  

Test Application
----------------

So we have mail server API and polling function. Next, we going to try it in diverse testing tools. For that we will need a real world example. Imagine, we are testing ACME forum application built with NodeBB. Our goal is to fill out the registration form (http://localhost:4567/register) and submit it:

![ACME forum registration form](http://dsheiko.com/download/000000292/example-app-1.png "ACME forum registration form")

It brings us to the next page where we have tick on the GDPR checkboxes.

![ACME forum GDPR form](http://dsheiko.com/download/000000292/example-app-1.png "ACME forum GDPR form")

As the form submitted the application sends confirmation email. Here we go with `pollForValue` function. We call it to poll the REST API until the email message arrived. The function will use the following parsing logic to get the activation link from NodeBB default email template:

```
    function parseActivationLink( text ) {
        const re = /(http\:[^\"]+4567\/con[^\"]+)/g,
              res = text.match( re );
        return res ? res[ 0 ].replace( "=\r\n", "" ) : null;
    }
```    

Thus we obtain the activation URL, which we follow to complete the registration.


Testing with Selenium WebDriver
-------------------------------

[Selenium WebDriver](https://github.com/SeleniumHQ/selenium "Selenium WebDriver") is probably the most popular testing tool. Not the most effortless, I would say, but still, it’s definetelly one you’ve heard about. So we setup the [dev environment for Node.js](https://www.npmjs.com/package/selenium-webdriver "dev environment for Node.js") and write our test case. Untill the point where we make ACME forum to send activation email everything is certain:

```
    const { Builder, By, Key, until } = require( "selenium-webdriver" );
    
    (async function main() {
      const driver = await new Builder().forBrowser("chrome").build(),
            USER = "ctest1";
      try {
        await driver.get( "http://localhost:4567/register" );
    
        await driver.findElement( By.id("email" ) )
          .sendKeys( `${ USER }@restmail.net`, Key.RETURN );
        await driver.findElement( By.id("username" ) )
          .sendKeys( USER , Key.RETURN );
        await driver.findElement( By.id("password" ) )
          .sendKeys( `Password1234`, Key.RETURN );
        await driver.findElement( By.id("password-confirm" ) )
          .sendKeys( `Password1234`, Key.RETURN );
        
        await driver.findElement( By.id("gdpr_agree_email" ) )
          .click();
        
        await driver.findElement( By.id("gdpr_agree_data" ) )
          .click();
    
        await driver.findElement( By.css("#content form button" ) )
          .click();
    
        //…  
    
      } catch ( e ) {
        console.log( e );
      } finally {
        await driver.quit();
      }
    })();
```

We populate the first form with test values, where email shall be in restmail.net domain. As we are done with the last field, the form gets automatically submitted. Then we tick on the checkboxes and click on submit button. Now let’s do the polling. So we put at the beginning of the script a module to simplify HTTP(S) requests:

```
    const fetch = require( "node-fetch" );
```    

Next we place our `pollForValue` and `parseActivationLink` functions. Now we can extend test steps with:

```
       const activationLink = await pollForValue({ url: `http://restmail.net/mail/${ USER }`, 
          interval: 1000, 
          timeout: 600000,  
          parserFn: ( messages ) => {
              if ( !messages ) {
                return null;
              }
               const sentAt = new Date( Date.now() - 1000 ),
                     unseen = messages.find( msg => new Date( msg.receivedAt ) > new Date( sentAt ) );                
              return parseActivationLink( messages[0].html );
          }
        });
    
        console.log( "Activation link:", activationLink );
    
        await driver.get( activationLink );
```
  

Thus after submitting the second form we make the script polling for newly sent email message. When it received we parse the message body for the activation link. Bingo! We get the link and we make the driver navigating to it.

![Selenium Webdriver results](http://dsheiko.com/download/000000292/selenium-res.png "Selenium Webdriver results")

  

Testing with Cypress
--------------------

Recently is gaining momentum a tool called [Cypress](https://www.cypress.io/ "Cypress"). I do like it personally for the test debugging. Without polling for mail messages the test script may look like that:

```
    const USER = "ctest1";
    describe("User registration flow", () => {
        it( "registers user", ( done ) => {
          cy.visit( "http://localhost:4567/register" );
    
          cy.get( "#email" ).type( `${ USER }@restmail.net` );
          cy.get( "#username" ).type( USER );
          cy.get( "#password" ).type( "Password1234" );
          cy.get( "#password-confirm" ).type( "Password1234" );
          cy.get( "#register" ).click();
          cy.wait( 1000 );
          cy.get("#gdpr_agree_email").click();
          cy.get("#gdpr_agree_data").click();
          cy.get("#content form button.btn-primary").click();
          
          //...
          done();
          
        })
      })
```

Similar to what we did with Selenium we extend the script with `pollForValue` and `parseActivationLink` functions. However this time instead of using node-fetch we rather go with built-in cy.request function. That’s where pollForValue’s `requestFn` option jumps in action:

```
          pollForValue({ url: `http://restmail.net/mail/${ USER }`, 
                interval: 1000, 
                timeout: 600000,  
                parserFn: ( messages ) => {
                    if ( !messages ) {
                        return null;
                      }
                       const sentAt = new Date( Date.now() - 1000 ),
                             unseen = messages.find( msg => new Date( msg.receivedAt ) > new Date( sentAt ) );                
                      return parseActivationLink( messages[0].html );
                },
                requestFn: ( url ) => {
                    return new Promise(( resolve ) => {
                        cy.request( url )
                            .then( (response) => {
                                resolve( response.body );
                            } );
                    });
                }
          }).then(( link ) => { 
            activationLink = link;
            console.log( "Activation link:", activationLink );
            done(); 
          });
```

So it’s just left to declare `activationLink`
```
    let activationLink;
```

and visit the activation link
```
    it( "follows the activation link", () => {
       cy.visit( activationLink );
    })
```

![Cypress results](http://dsheiko.com/download/000000292/cypress-res.png "Cypress results")

Testing with Puppetry
---------------------

We’ve just examined how we can do the trick with script-based testing tools. Let’s take now a code-less one – [Puppetry](https://puppetry.app "Puppetry"). With this tool we don’t script, but use GUI to fulfill our test specifications. Alternatively we [record user behavior](https://docs.puppetry.app/suite#record-suite "record user behavior"). Anyways we end up with a test suite, which contains a table of the target elements:

![Test target elements](http://dsheiko.com/download/000000292/puppetry-1.png "Test target elements")

And the test case:

![Test case](http://dsheiko.com/download/000000292/puppetry-2.png "Test case")

In this example I used template variables. First I defined a new variable `TEST_USER_ALIAS` that resolves with every test run in `ctest1`, `ctest2` and so on. Then I referred to the variable when typing into email field. Besides I applied template expression `{{ faker( "internet.userName", "en" ) }}` to generate real-world-like user name. And I also addressed and few environment-dependent variables. Other then that I don’t think you may have difficulties to read the test steps.

![Environment-dependent variables](http://dsheiko.com/download/000000292/puppetry-vars.png "Environment-dependent variables")

Now we extend the test for mail server polling. So we add the command corresponding to the earlier described function `pollForValue`:

![Polling for newly sent mail message](http://dsheiko.com/download/000000292/puppetry-3.png "Polling for newly sent mail message")

We give in options the retrieving and parsing function similar to one we used with Selenium and Cypress. That will resolve into new template variable `ACTIVATION_LINK`, which we use to visit the page:

![Test case to visit activation page](http://dsheiko.com/download/000000292/puppetry-4.png "Test case to visit activation page")

There it is. We’ve got the results:

![Test results in browser](http://dsheiko.com/download/000000292/puppetry-res1.png "Test results in browser")

![Test report](http://dsheiko.com/download/000000292/puppetry-res2.png "Test report")

Recap
-----

Testing user flows that involve transactional emails in a nutshell is not that complex as it’s may be seen. You just need an API to access the mail server and polling method (for example the function from this article). You can achieve with different testing tools, likely with one you’re currently working with.