Writing End-to-End Tests

Writing tests is part of web development. The more complex and the bigger your app gets, the more you need to test the app, otherwise, it will break at some point and you will spend lots of time fixing bugs and patching things up. In this chapter, you will write end-to-end tests with AVA and jsdom for Nuxt apps, and also get hands-on experience of browser automated testing with Nightwatch. You will learn how to install these tools and set up the testing environment – so let's get started.

The topics we will cover in this chapter are as follows:

  • End-to-end testing versus unit testing
  • End-to-end testing tools
  • Writing tests with jsdomn and AVA for Nuxt apps
  • Introducing Nightwatch
  • Writing tests with Nightwatch for Nuxt apps

End-to-end testing versus unit testing

There are two types of tests commonly practiced for web applications: unit testing and end-to-end testing. You might have heard a lot about unit testing and have done some (or loads) in the past. Unit testing is used to test the small and individual parts of your app, while in contrast, end-to-end testing is to test the overall functions of your app. End-to-end testing involves ensuring the integrated components of an app function as expected. In other words, the entire app is tested in a real-world scenario similar to how a real user would interact with your app. For example, the simplified end-to-end testing of your user login page might involve the following:

  1. Load the login page.
  2. Provide valid details to the inputs in the login form.
  3. Click on the Submit button.
  1. Log in to the page successfully and see a greeting message.
  2. Log out of the system.

What about unit testing? Unit testing runs fast and allows us to precisely identify exact problems and bugs. The main downside of unit testing is that it is time-consuming to write tests for every aspect of your app. And despite the fact that your app has passed the unit tests, the app as a whole may still break.

End-to-end testing can implicitly test many things at once and assure you that you have a working system. End-to-end testing runs slowly compared to unit testing and it can't explicitly point you to the root of the failure of your app. A small change in seemingly insignificant parts of your app can break your entire testing suite.

Combining unit and end-to-end tests for an app can be ideal and compelling because that gives you a more thorough test of your app, but again it can be time-consuming and costly. In this book, we focus on end-to-end testing because by default, Nuxt is configured seamlessly with the end-to-end testing tools that you will discover in the next section.

End-to-end testing tools

Nuxt makes end-to-end testing very easy and fun by using the AVA and jsdom Node.js modules together. But before implementing and combining them for the tests in a Nuxt app, let's dive into each of these Node.js modules to see how they work separately so you have a solid basic understanding of these tools. Let's start with jsdom in the next section.

jsdom

In a nutshell, jsdom is a JavaScript-based implementation of the W3C Document Object Model (DOM) for Node.js. But, what does it mean? What do we need it for? Imagine you need to manipulate DOM from a raw HTML on the server side in a Node.js app, such as Express and Koa apps, but there is no DOM on the server side and hence there isn't much you can do. This is when jsdom comes to our rescue. It turns the raw HTML into a DOM fragment that works like the DOM on the client side, but inside Node.js. And then, you can use a client-side JavaScript library such as jQuery to manipulate the DOM on Node.js like a charm. The following is an example of basic usage of this for server-side apps:

  1. Import jsdom on a server-side app:
import jsdom from 'jsdom'
const { JSDOM } = jsdom
  1. Pass in a string of any raw HTML to the JSDOM constructor and you will get back a DOM object:
const dom = new JSDOM(<!DOCTYPE html><p>Hello World</p>)
console.log(dom.window.document.querySelector('p').textContent)

The DOM object you get from the preceding code snippet has many useful properties, notably the window object, and then you can start manipulating the HTML string you pass in just like on the client side. Now let's apply this tool on the Koa API, which you learned about in the previous chapter and which can be found in /chapter-12/nuxt-universal/cross-domain/jwt/axios-module/backend/ in our GitHub repository, to print the Hello world message. Follow these steps:

  1. Install jsdom and jQuery via npm:
$ npm i jsdom --save-dev
$ npm i jquery --save-dev
  1. Import jsdom and pass an HTML string, just like we did in the preceding basic usage example:
// src/modules/public/home/_routes/index.js
import Router from 'koa-router'
import jsdom from 'jsdom'

const { JSDOM } = jsdom
const router = new Router()

const html = '<!DOCTYPE html><p>Hello World</p>'
const dom = new JSDOM(html)
const window = dom.window
const text = window.document.querySelector('p').textContent
  1. Output the text to the endpoint:
router.get('/', async (ctx, next) => {
ctx.type = 'json'
ctx.body = {
message: text
}
})

You should see the "Hello world" message in JSON format (shown in the following snippet) at localhost:4000/public when you run npm run dev on your terminal:

{"status":200,"data":{"message":"Hello world"}}
  1. Create a movie module in our API and use Axios to fetch an HTML page from the IMDb website, pass the HTML to the JSDOM constructor, import jQuery, and then apply it to the DOM window object created by jsdom as follows:
// src/modules/public/movie/_routes/index.js
const url = 'https://www.imdb.com/movies-in-theaters/'
const { data } = await axios.get(url)

const dom = new JSDOM(data)
const $ = (require('jquery'))(dom.window)
Note that Axios must be installed in your project directory via npm, which you do with npm i axios.
  1. Apply the jQuery object to all movies with the list_item class and extract the data (the name and showtime of each movie) as follows:
var items = $('.list_item')
var list = []
$.each(items, function( key, item ) {
var movieName = $('h4 a', item).text()
var movieShowTime = $('h4 span', item).text()
var movie = {
name: movieName,
showTime: movieShowTime
}
list.push(movie)
})
  1. Output the list to the endpoint:
ctx.type = 'json'
ctx.body = {
list: list
}

You should see a similar list of movies in the following JSON format at localhost:4000/public/movies:

{
"status": 200,
"data": {
"list": [{
"name": " Onward (2020)",
"showTime": ""
}, {
"name": " Finding the Way Back (2020)",
"showTime": ""
},
...
...
]
}
}
You can find these examples in /chapter-13/jsdom/ in our GitHub repository. For more information about this npm package, visit https://github.com/jsdom/jsdom.

You can see how useful this tool is on the server side. It gives us the ability to manipulate the raw HTML as if we were on the client side. Now let's move on to AVA and learn some basic usage of it in the next section before using it together with jsdom in our Nuxt app.

AVA

In short, AVA (not Ava or ava, pronounced /ˈeɪvə/) is a JavaScript test runner for Node.js. There are a lot of test runners out there: Mocha, Jasmine, and tape, among others. AVA is another alternative to the existing list. First of all, AVA is simple. It is really easy to set up. Besides, it runs the test in parallel by default, which means your tests will run fast. It works for both frontend and backend Javascript apps. All in all, it's certainly worth a try. Let's get started by creating a simple and basic Node.js app in the following steps:

  1. Install AVA via npm and save it to the devDependencies option in the package.json file:
$ npm i ava --save-dev
  1. Install Babel core and other Babel packages for us to write ES6 code in our tests for the app:
$ npm i @babel/polyfill
$ npm i @babel/core --save-dev
$ npm i @babel/preset-env --save-dev
$ npm i @babel/register --save-dev
  1. Configure the test script in the package.json file as follows:
// package.json
{
"scripts": {
"test": "ava --verbose",
"test:watch": "ava --watch"
},
"ava": {
"require": [
"./setup.js",
"@babel/polyfill"
],
"files": [
"test/**/*"
]
}
}
  1. Create a setup.js file in the root directory with the following code:
// setup.js
require('@babel/register')({
babelrc: false,
presets: ['@babel/preset-env']
})
  1. Create the following class and function that we want to test later in these two separate files in our app:
// src/hello.js
export default class Greeter {
static greet () {
return 'hello world'
}
}

// src/add.js
export default function (num1, num2) {
return num1 + num2
}
  1. Create a hello.js test in the /test/ directory for testing /src/hello.js:
// test/hello.js
import test from 'ava'
import hello from '../src/hello'

test('should say hello world', t => {
t.is('hello world', hello.greet())
})
  1. Create another test in a separate file in the /test/ directory again for testing /src/add.js :
// test/add.js
import test from 'ava'
import add from '../src/add'

test('amount should be 50', t => {
t.is(add(10, 50), 60)
})
  1. Run all the tests on your terminal:
$ npm run test

You also can run the test with the --watch flag to enable AVA's watch mode:

$ npm run test:watch

You should get the following result if the tests pass:

✓ add › amount should be 50
✓ hello › should say hello world

2 tests passed
You can find the preceding examples in /chapter-13/ava/ in our GitHub repository. For more information about this npm package, visit https://github.com/avajs/ava.

That is easy and fun, isn't it? It is always rewarding to see our code pass its tests. Now you have a basic understanding of this tool, so it is time to implement it with jsdom in the Nuxt app. Let's get to it in the next section.

Writing tests with jsdomn and AVA for Nuxt apps

You have learned about jsdom and AVA independently and have done some simple tests. Now, we can bring these two packages together into our Nuxt apps. Let's install them in our Nuxt app, which you created in the previous chapter, in /chapter-12/nuxt-universal/cross-domain/jwt/axios-module/frontend/ using the following steps:

  1. Install these two tools via npm and save them to the devDependencies option in the package.json file:
$ npm i ava --save-dev
$ npm i jsdom --save-dev
  1. Install Babel core and other Babel packages for us to write ES6 code in our tests in the app:
$ npm i @babel/polyfill
$ npm i @babel/core --save-dev
$ npm i @babel/preset-env --save-dev
$ npm i @babel/register --save-dev
  1. Add the AVA configuration to the package.json file as follows:
// package.json
{
"scripts": {
"test": "ava --verbose",
"test:watch": "ava --watch"
},
"ava": {
"require": [
"./setup.js",
"@babel/polyfill"
],
"files": [
"test/**/*"
]
}
}
  1. Create a setup.js file in the root directory, just like you did in the previous section, but using the following code:
// setup.js
require('@babel/register')({
babelrc: false,
presets: ['@babel/preset-env']
})
  1. Prepare the following test template for writing tests in the /test/ directory:
// test/tests.js
import test from 'ava'
import { Nuxt, Builder } from 'nuxt'
import { resolve } from 'path'

let nuxt = null

test.before('Init Nuxt.js', async t => {
const rootDir = resolve(__dirname, '..')
let config = {}
try { config = require(resolve(rootDir, 'nuxt.config.js')) }
catch (e) {}
config.rootDir = rootDir
config.dev = false
config.mode = 'universal'
nuxt = new Nuxt(config)
await new Builder(nuxt).build()
nuxt.listen(5000, 'localhost')
})

// write your tests here...

test.after('Closing server', t => {
nuxt.close()
})

The tests will run on localhost:5000 (or any port you like). You should test on the production build, so turn development mode off in the config.dev key and use universal in the config.mode key if your app was developed for both server and client sides. Then, make sure to close the Nuxt server after the test process is finished.

  1. Write the first test to test our home page to ensure that we have correct HTML rendered on this page:
// test/tests.js
test('Route / exits and renders correct HTML', async (t) => {
let context = {}
const { html } = await nuxt.renderRoute('/', context)
t.true(html.includes('<p class="blue">My marvelous Nuxt.js
project</p>'))
})
  1. Write the second test for the /about route to ensure that we have correct HTML rendered on this page:
// test/tests.js
test('Route /about exits and renders correct HTML', async (t) => {
let context = {}
const { html } = await nuxt.renderRoute('/about', context)
t.true(html.includes('<h1>About page</h1>'))
t.true(html.includes('<p class="blue">Something awesome!</p>'))
})
  1. Write the third test for the /about page to ensure the text content, class name, and style are as expected via DOM manipulation on the server side with jsdom:
// test/tests.js
test('Route /about exists and renders correct HTML and style',
async (t) => {

function hexify (number) {
const hexChars =
['0','1','2','3','4','5','6','7','8','9','a','b',
'c','d','e','f']
if (isNaN(number)) {
return '00'
}
return hexChars[(number - number % 16) / 16] +
hexChars[number % 16]
}

const window = await nuxt.renderAndGetWindow(
'http://localhost:5000/about')
const element = window.document.querySelector('.blue')
const rgb = window.getComputedStyle(element).color.match(/d+/g)
const hex = '' + hexify(rgb[0]) + hexify(rgb[1]) + hexify(rgb[2])

t.not(element, null)
t.is(element.textContent, 'Something awesome!')
t.is(element.className, 'blue')
t.is(hex, '0000ff')
})

You should get the following result if the tests pass with npm run test:

✓ Route / exits and renders correct HTML (369ms)
✓ Route /about exits and renders correct HTML (369ms)
✓ Route /about exists and renders correct HTML and style (543ms)

3 tests passed

You can see that in our third test, we created a hexify function to convert a decimal code (R, G, B), computed by the Window.getComputedStyle method, to a hex code. For example, you will get rgb(255, 255, 255) for the colour you set as color: white in your CSS style. So, you will get rgb(0, 0, 255) for 0000ff and the app must convert that to pass the test.

You can find these tests in /chapter-13/nuxt-universal/ava/ in our GitHub repository.

Well done. You have managed to write simple tests for Nuxt apps. We hope you find it easy and fun to write tests in Nuxt. The complexity of your test depends on what you want to test. Hence, it is very important to first understand what you want to test. Then, you can start writing a test that is sensible, meaningful, and relevant.

However, using jsdom with AVA to test the Nuxt app has some limitations because it does not involve a browser. Remember that jsdom is meant for turning the raw HTML into the DOM on the server side, hence we use the async/await statement to request a page asynchronously for our tests in the preceding exercise. If you want to use a browser to test your Nuxt app, Nightwatch can be a good solution, so we will take a look at it in the next section. Let's move on.

Introducing Nightwatch

Nightwatch is an automated testing framework that provides an end-to-end testing solution for web-based apps. It uses the W3C WebDriver API (it was called Selenium WebDriver formerly) behind the scenes to open the web browser to perform operations and assertions on DOM elements. It is a great tool if you want to use a browser to test your Nuxt apps. But before using it in a Nuxt app, let's use it on its own in the following steps to write some simple tests so that you have a basic idea of how it works:

  1. Install Nightwatch via npm and save it to the devDependencies option in the package.json file:
$ npm i nightwatch --save-dev
  1. Install GeckoDriver via npm and also save it to the devDependencies option in the package.json file:
$ npm install geckodriver --save-dev

Nightwatch relies on WebDriver, so we need to install a specific WebDriver server depending on your target browser – for example, if you want to write tests against Firefox only, then you will need to install GeckoDriver.

In this book, we focus on writing tests against a single browser. But if you want to target multiple browsers such as Chrome, Edge, Safari, and Firefox in parallel, then you will need to install the Selenium Standalone Server (also known as Selenium Grid), as follows:

$ npm i selenium-server --save-dev
Note that we will be testing on Firefox and Chrome in this book, so this selenium-server package will not be used.
  1. Add nightwatch to the test script in the package.json file:
// package.json
{
"scripts": {
"test": "nightwatch"
}
}
  1. Create a nightwatch.json file to configure Nightwatch as follows:
// nightwatch.json
{
"src_folders" : ["tests"],

"webdriver" : {
"start_process": true,
"server_path": "node_modules/.bin/geckodriver",
"port": 4444
},

"test_settings" : {
"default" : {
"desiredCapabilities": {
"browserName": "firefox"
}
}
},

"launch_url": "https://github.com/lautiamkok"
}

In this simple exercise, we want to test the repository search function of github.com on a specific contributor called Lau Tiam Kok, so we set https://github.com/lautiamkok in the launch_url option in this configuration.

We will write tests in the /tests/ directory, so we indicate the directory location in the src_folders option. We will test against Firefox only at 4444 (the server port) so we set this information in the webdriver and test_settings options.

You can find the options for the rest of test settings such as output_folder at https://nightwatchjs.org/gettingstarted/configuration/. If you want to find out the test settings for the Selenium Server, please visit https://nightwatchjs.org/gettingstarted/configuration/selenium-server-settings.
  1. Create a nightwatch.conf.js file in the project root for setting the driver path dynamically to the server path:
// nightwatch.conf.js
const geckodriver = require("geckodriver")
module.exports = (function (settings) {
settings.test_workers = false
settings.webdriver.server_path = geckodriver.path
return settings
})(require("./nightwatch.json"))
  1. Prepare the following Nightwatch test template in a .js file (for example, demo.js) in the /tests/ directory, as follows:
// tests/demo.js
module.exports = {
'Demo test' : function (browser) {
browser
.url(browser.launchUrl)
// write your tests here...
.end()
}
}
  1. Create a github.js file in the /tests/ directory with the following code:
// tests/github.js
module.exports = {
'Demo test GitHub' : function (browser) {
browser
.url(browser.launchUrl)
.waitForElementVisible('body', 1000)
.assert.title('lautiamkok (LAU TIAM KOK) · GitHub')
.assert.visible('input[type=text][placeholder=Search]')
.setValue('input[type=text][placeholder=Search]', 'nuxt')
.waitForElementVisible('li[id=jump-to-suggestion-
search-scoped]', 1000)
.click('li[id=jump-to-suggestion-search-scoped]')
.pause(1000)
.assert.visible('ul[class=repo-list]')
.assert.containsText('em:first-child', 'nuxt')
.end()
}
}

In this test, we want to assert that the repository search function is working as expected so we need to make sure that certain elements and text contents exist and are visible, such as the <body> and <input> elements, and the text for nuxt and lautiamkok (LAU TIAM KOK) · GitHub. You should get the following result (assuming the test passes) when you run it with npm run test:

[Github] Test Suite
===================
Running: Demo test GitHub

✓ Element <body> was visible after 34 milliseconds.
✓ Testing if the page title equals "lautiamkok (LAU TIAM KOK) ·
GitHub" - 4 ms.
✓ Testing if element <input[type=text][placeholder=Search]> is
visible - 18 ms.
✓ Element <li[id=jump-to-suggestion-search-scoped]> was visible
after 533 milliseconds.
✓ Testing if element <ul[class=repo-list]> is visible - 25 ms.
✓ Testing if element <em:first-child> contains text: "nuxt"
- 28 ms.

OK. 6 assertions passed. (5.809s)
You can find the preceding test in /chapter-13/nightwatch/ in our GitHub repository. For more information on Nightwatch, please visit https://nightwatchjs.org/.

Compared to AVA, Nightwatch is not as minimal as it requires some configuration that can be lengthy and complex, but if you follow the simplest nightwatch.json file, it should get you started with Nightwatch quite quickly. So, let's apply what you just have learned in this section to the Nuxt app in the next section.

Writing tests with Nightwatch for Nuxt apps

In this exercise, we want to test the user login authentication that we created in the previous chapter, Chapter 12, Creating User Logins and API Authentication, against the Chrome browser. We want to make sure the user can log in with their credentials and obtain their user data as expected. We will write the tests in the /frontend/ directory where we kept the Nuxt app, so we will need to modify the package.json file accordingly and write the tests in the following steps:

  1. Install ChromeDriver via npm and save it to the devDependencies option in the package.json file:
$ npm install chromedriver --save-dev
  1. Change the launch URL to localhost:3000 and other settings as shown in the following code block in the nightwatch.json file for testing against Chrome in the Nightwatch configuration file:
// nightwatch.json
{
"src_folders" : ["tests"],

"webdriver" : {
"start_process": true,
"server_path": "node_modules/.bin/chromedriver",
"port": 9515
},

"test_settings" : {
"default" : {
"desiredCapabilities": {
"browserName": "chrome"
}
}
},

"launch_url": "http://localhost:3000"
}
  1. Create a nightwatch.conf.js file in the project root for setting the driver path dynamically to the server path:
// nightwatch.conf.js
const chromedriver = require("chromedriver")
module.exports = (function (settings) {
settings.test_workers = false
settings.webdriver.server_path = chromedriver.path
return settings
})(require("./nightwatch.json"))
  1. Create a login.js file in the /tests/ directory with the following code:
// tests/login.js
module.exports = {
'Local login test' : function (browser) {
browser
.url(browser.launchUrl + '/login')
.waitForElementVisible('body', 1000)
.assert.title('nuxt-e2e-tests')
.assert.containsText('h1', 'Please login to see the
secret content')
.assert.visible('input[type=text][name=username]')
.assert.visible('input[type=password][name=password]')
.setValue('input[type=text][name=username]', 'demo')
.setValue('input[type=password][name=password]',
'123123')
.click('button[type=submit]')
.pause(1000)
.assert.containsText('h2', 'Hello Alexandre!')
.end()
}
}

The logic of this test is the same as the test in the previous section. We want to make sure that certain elements and texts are present on the login page before and after logging in.

  1. Before running the test, run the Nuxt and API apps at localhost:3000 and localhost:4000 on your terminal and then open another terminal with npm run test in the /frontend/ directory. You should get the following result if the test passes:
[Login] Test Suite
==================
Running: Local login test

✓ Element <body> was visible after 28 milliseconds.
✓ Testing if the page title equals "nuxt-e2e-tests" - 4 ms.
✓ Testing if element <h1> contains text: "Please login to see the
secret content" - 27 ms.
✓ Testing if element <input[type=text][name=username]> is
visible - 25 ms.
✓ Testing if element <input[type=password][name=password]> is
visible - 25 ms.
✓ Testing if element <h2> contains text: "Hello Alexandre!"
- 75 ms.

OK. 6 assertions passed. (1.613s)
Note that you must run the Nuxt app and the API concurrently before running the tests. 

You can find the preceding test in /chapter-13/nuxt-universal/nightwatch/ in our GitHub repository.

Well done. You have finished this short chapter on writing tests for Nuxt apps. The steps and exercises in this chapter have provided you with the basic foundation to expand your tests as your app gets larger and complicated. Let's summarize what you have learned in this chapter in the final section.

Summary

In this chapter, you have learned to use jsdom for server-side DOM manipulation and for writing simple tests with AVA and Nightwatch separately, and then tried using these tools together to run end-to-end tests on our Nuxt app. You also have learned the difference between end-to-end testing and unit testing and their respective pros and cons. Last but not least, you have learned from the exercises in this chapter that Nuxt is configured perfectly by default for you to write end-to-end tests with jsdom and AVA with much less effort.

In the coming chapter, we will cover how to keep our code clean with linters such as ESLint, Prettier, and StandardJS, integrating and mixing them for Vue and Nuxt apps. Finally, you will learn the Nuxt deployment commands and use them to deploy your app to a live server. So, stay tuned.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.117.99.152