/ react

End to End Testing (with React)

Edit I now prefer webdriver.io. It's SO much nicer. I may write a post on it soon, but this post still may help you :)

Recently, I read an article on testing that said unit tests are pointless. I'm inclined to agree. Why not focus on writing integration tests? Hit a part of your app and test every possible input and output. Why worry about the smallest parts in your app? This way, you can refactor those unit sized pieces without rewriting tests. With that being said, I am not advocating to take it a step further and only write end to end tests. Just wanted to make you think a little about your testing strategy.

Personally, if I am writing both the backend and frontend on a site, I am completely fine with integration testing the whole backend, and then testing the entire frontend through in-browser tests. If you can cover every part of your app this way, I see no point in unit tests. Sure, they may make you feel better, but are they needed? If I run a test in the browser that toggles through every piece of state in a react component, why unit test it? One argument for unit tests is that integration tests are slower. You could possibly test everything through unit tests and important paths through integration tests. With that being said, I'll leave the answer up to you :)

Why End to End?

I want to make it clear why anyone would want to write these sort of tests. The gist is that you can run your tests in any browser on any version and be confident your application is always working. Thanks to tools like Protractor, you can write these with ease. Browserstack is an awesome tool that will allow you to run them on any browser / version / OS combo. If you're not convinced this is a good idea, you can leave now ;)

Protractor

Protractor is an end to end testing solution for Angular. Yes... I said it... that dreaded framework that I'm not allowed to talk about because I'm in the React community right? Wrong. If the angular community has ever done something good for me it's build protractor, because it is awesome and for the most part, works great with React! All kidding aside I have nothing against angular, that's for another post on another day

Setup

To get started, we need to download Protractor and also setup selenium's web driver. We start the process by running npm install protractor in your project. You can follow the guide on the official website if you want to. It has you install it globally, but we don't really need to do that, especially if we are going to run our tests as an npm script.

After installing, we have to update the webdriver by running ./node_modules/.bin/webdriver-manager update then we can add these two scripts to your package.json:

"scripts": {
  "e2e": "protractor ./tests/config.js",
  "e2e-driver": "webdriver-manager start",
}

Before we write our first tests or even make our protractor config file, we can run npm run e2e-driver and have the webdriver ready.

Config

This can be the most confusing part of the setup. Do you want to run your tests locally? In Chrome? On browserstack? Across multiple browsers? Thankfully I did the grunt work for you, I'll share a couple different configs that will cover each of these use cases. Let's make this file inside tests/config.js like we referenced in our npm script.

Local Chrome Config

exports.config = {
  framework: 'mocha',
  seleniumAddress: 'http://localhost:4444/wd/hub',
  baseUrl: 'http://localhost:3000',
  specs: ['e2e/**/*.js'],
  onPrepare: () => {
    browser.ignoreSynchronization = true
    var width = 2250
    var height = 1200
    browser.driver.manage().window().setSize(width, height)

    require('babel-register')
    require('./setup')
  },
  mochaOpts: {
    enableTimeouts: false,
  },
  allScriptsTimeout: 15000,
}

Here's a quick explanation of each key:

  • framework: the testing framework you're using
  • seleniumAddress: the address to the web driver that should be running
  • baseUrl: location of your development server
  • specs: Test files
  • onPrepare: Sets up the browser width and anything else you want required
  • mochaOpts: disable timeouts for mocha in case a tests runs for a while
  • allScriptsTimeout Set protractor's timeout to 15 seconds

Inside of the onPrepare we turn off synchronization. This is an angular thing, since protractor was made for it after all. If we left it on, the tests would never run because it would be waiting for angular to load.

I set the browser width and height on my own, because for the sites I test, some elements are different on mobile, and I test the desktop version first. If you want to use es-next things that are not in the latest version of node, you can install babel-register and require it like I did.

BrowserStack Config

Here's the cool part that is super useful for real world testing:


exports.config = {
  framework: 'mocha',
  baseUrl: 'https://dev-env.io',
  specs: ['e2e/**/*.js'],
  onPrepare: () => {
    browser.ignoreSynchronization = true
    var width = 2250
    var height = 1200
    browser.driver.manage().window().setSize(width, height)

    require('babel-register')
    require('./setup')
  },
  mochaOpts: {
    enableTimeouts: false,
  },
  allScriptsTimeout: 15000,
  browserstackUser: 'browserstackuser',
  browserstackKey: 'browserstackkey',
  multiCapabilities: [
    {
      browserName: 'Chrome',
      browser_version: '54.0',
      os: 'OS X',
      os_version: 'Yosemite',
      resolution: '1920x1080',
      'browserstack.local': true,
    }
  ],

}

There's a couple small changes here. We removed seleniumAddress because the BrowserStack config takes care of it. We added browserstackUser and browserstackKey that you'll have to get from registering. Last we have the cool part: multiCapabilities. This allows us to create an array of browsers. We can specify the OS, browser version, etc. You can check all of the supported values here.

A quick note about BrowserStack. It's free for open source projects, otherwise it can be expensive. If you work for a big company, the price is worth the peace of mind you'll have knowing that your site works across any browser. I just wanted to mention that I'm not affiliated with them in any way, I just find their product to be an incredible testing tool. If you don't want to set this up, you can test locally in your browser by using the first config.

Testing Private Development environments

When I first set this up, I needed my tests to run on a server that is only accessible from a work IP address. I imagine many people out there will have this same issue: How do I allow BrowserStack to access this server? Thankfully they have a binary you can download called BrowserStack Local that will route requests through your private server, or anywhere else that can access your development servers.

setup.js

Hopefully you noticed I am requiring another file in the onPrepare method of both configs. This file sets up a few things for us:

import chai from 'chai'
import chaiAsPromised from 'chai-as-promised'

chai.use(chaiAsPromised);
global.expect = chai.expect

You'll need to install these three things before we can actually run our tests:

npm install chai chai-as-promised mocha --save

Chai is an assertion library, and chai-as-promised lets us assert things about a pending promise. Also don't forget to install mocha, it's the testing library we set protractor to use from above.

Let's write a test

Now that we have all of that out of the way, let's write a test!

At this point, we should have a folder called tests with config.js and setup.js. Let's create tests/e2e/login.js. We will make a few assertions about navigating to our login page.

tests/e2e/login.js

describe('Login', () => {

  beforeEach(() => {
    browser.get('/auth/login')
  })

  it('should have correct title', () => {
    expect(browser.getTitle()).to.eventually.equal("Login")
  })

})

Since we are using babel-register we can safely use ES6 in any version of node. Protractor exposes a browser object that has a bunch of useful functions on it.

In this file, we are saying before each test go to /auth/login and we have one test so far. It expects the <title> to eventually be Login on the page. browser.getTitle() returns a promise. Thanks to chai as promised, we can just put to.eventually.equal instead of to.equal like you may be used to. If you have an login page on your app, change the url and title to the correct one, and try npm run e2e and a new browser window should pop up and close very fast. This is because this test can run near instantly. Your console should show that one test passed successfully :)

Real world tests

Now that we have that basic test done, let's trigger a click and fill in some fields.

Let's act like we have a contact form built in React. It might look something like this:

import React from 'react'

export default class Contact extends React.Component {
  constructor() {
    super()
    this.state = {
      message: '',
    }
  }

  onSubmit() {
    ...
    this.setState({ response: 'Successfully sent!' })
  }

  render() {
    let { message, response } = this.state
    return (
      <div class="contact">
        {response ? 
          <div className="response">{response}</div>
        :
          null
        }

        <label htmlFor="message">Message</label>
        <input 
          type="text"
          value={message}
          id="message"
          onChange={e => this.setState({ message: e.target.value })}
        />

        <div className="submit" onClick={() => this.submit()}>Submit</div>
      </div>
    )
  }
}

How you render this component to the page is up to you. Let's just assume this component is rendered on /contact.

tests/e2e/contact.js

describe('Contact', () => {
  beforeEach(() => {
    browser.get('/contact')
  })

  it('fills out message field', () => {
    let message = 'I am a bot and I am awesome'
    element(by.id('message')).sendKeys(message)
    
    expect(element(by.id('message')).getText())
      .to.eventually.equal(message)


    browser.findElement(by.className('submit')).click()
    
    browser.sleep(2000)

    expect(element(by.className('response')).getText())
      .to.eventually.equal('Successfully sent!')
  })
})

First, we fill in the input field with some text. Then we check to make sure that the value is what we put in. This makes sure setting state is working correctly. After, we click the submit button, wait two seconds and check to see if the response is what it should be. This test would hit your whole stack. Testing the rendering as you type, testing your api since you are triggering a submit, fully end to end!

The one drawback here is that we are manually doing a browser sleep. This is because .response does not exist when we first click the submit button. If it did exist, we could remove the sleep and wait on the promise for it to finish. You should try your best to stay away from using manual sleeps, but I've found that it is necessary at times. If I instead opted to always render an empty .response on the other hand, we wouldn't need it.

Getting advanced

If you take the things I taught you here, you can go a long way with testing your app. You're not far from clicking onto other pages, testing that new pages exist after creating them, and so much more.

There's going to be many scenarios where you want to do more than sending keys though. Thankfully, Protractor's docs are awesome. There's a reference section that will let you search through the whole api for any functions you may need.

If you have any questions or need help, please leave a comment. The code written here was all pseudo code. If you want to see more posts like this, I would appreciate a subscribe!