intern

So you’ve had a chance to try out Intern Recorder, our new Chrome DevTools extension for recording functional tests, and now you want to efficiently work these tests into your workflow? This post will guide you through these steps and provide helpful advice for improving the tests you record.

The goal with Intern Recorder is to reduce the tedium of creating functional tests by 80-90%, but to make tests work flawlessly, you still have some steps to follow to perfect them. Intern Recorder is very useful for getting started when creating functional tests, but the test code should ultimately be fine-tuned by hand and maintained and updated manually. It may take multiple runs with the Recorder to get the sequence of actions just right, but once you have it, plan to discontinue using Recorder for ongoing updates to the test as you will now have code you can continue to improve.

Depending on the complexity and length of the test sequence (keep it as low as you can, so it’s easier to decouple your tests!), it may be useful to write a descriptive list of the actions to be performed in the test, as well as the conditions to be tested. This list can serve as the script for the person performing the actions while using Intern Recorder, as well as a reference for adding test assertions and comments to the generated code.

Tips

Here are some tips for getting optimal results when working with Intern Recorder.

Perform as few actions as necessary

Don’t perform extraneous actions as they will be recorded and included in generated code, and may later be irrelevant to your test, or in the worst case, break your test if your application changes.

Set-up data and preconditions

Provide as much set-up data and preconditions as possible in the test page being loaded. Don’t use UI actions driven by test code to put the page into a desired beginning test state – only use UI actions for things that are the focus of the test.

Configure and use hotkeys

Intern Recorder includes a number of hotkeys for your usage when recording functional tests.

  • Pause/resume recorder: use this to pause if you need to do some actions that should not be recorded
  • Insert callback: use this to insert a call to Command#then – you can later add code such as assertions, or replace the call with another function (e.g. execute / executeAsync)
  • Insert move to current mouse position: while mouse clicks are recorded (with their coordinates), mouse movement is not, so you can use this to record mouse movement and trigger hover actions

Use the appropriate element selection strategy

  • Use path of element: this will usually be the best option; the Recorder will compute a concise XPath expression for the element
  • Use text of element: if you know the text content of the element is unique (and will not change), this is a better option than XPath

Use findDisplayed when appropriate

This method can incur performance penalties, so disable it as much as possible (it can be toggled on and off during test recording so it only applies to actions performed while it is enabled).

It is useful when animations or visibility transitions cause an element to be created in the DOM (and be discoverable by Leadfoot’s findByXXX methods), but the element is not displayed.

In some cases, trying to interact with it will result in failure. Leadfoot’s findDisplayedByXXX methods will wait until the element exists and is displayed. Note that this will not help in cases where you want to interact with an element that is displayed, but is obscured, for example, by a Dijit dialog overlay. In that case you must perform actions to remove the obscuring element(s).

Fine tuning

A simple example of Recorder output looks like this:

define(function (require) {
	var tdd = require('intern!tdd');
	tdd.suite('recorder-generated suite', function () {
		tdd.test('Test 1', function () {
			return this.remote
				.get('http://localhost/dojo/dgrid/test/intern/functional/Editor.html')
				.findByXpath('//TD[normalize-space(string())="one"]')
					.moveMouseTo(100, 16)
					.clickMouseButton(0)
				.pressKeys('a')
					.end()
				.findByXpath('//TD[normalize-space(string())="two"]')
					.moveMouseTo(96, 8)
					.clickMouseButton(0)
				.pressKeys('b')
					.end()
				.then(function () {})
				.findByXpath('//TD[normalize-space(string())="onea"]')
					.moveMouseTo(69, 13)
					.clickMouseButton(0)
				.then(function () {});
		});
	});
});

While the actions have been recorded, more work needs to be done to transform this into a working test case. Here are some strategies for further fine tuning your tests.

Add descriptive comments

Add code comments describing what UI actions are being performed by each section of code.

Trim unnecessary code

Identify any unnecessary actions and remove the code for them.

Replace brittle element selectors

The Recorder application has no knowledge of your overall application architecture or DOM structure – it simply creates the most concise and accurate element selectors that can be deduced in a very straightforward manner and expressed using XPath. When an element is interacted with, Recorder looks for the nearest ancestor that has an id assigned, and if the target element itself has no id, then the DOM hierarchy below the identified ancestor is expressed using XPath.

  • Bad ids: the ids that Dijit auto-generates for nodes include a numeric value that will vary as the widgets in the page vary, so they should not be used. The Recorder does not account for this and uses them. Replace unsuitable ids with better ones, possibly replacing the call to findByXpath with a call to findByCssSelector
  • Non-ideal targets: widgets are often constructed using multiple elements to provide cross-browser consistency and flexible styling options, but the resulting DOM structure is more complicated than would be ideal. A human looking at the DOM structure could identify that a certain node should be the target, despite the fact that the node has descendants several levels deep. The Recorder cannot make this distinction and will select the deepest descendant as the target. Replace interaction targets with the most appropriate node based on inspecting the DOM and UI layout.

Add extra logic

Occasionally you will need to add extra logic to tests, e.g. calling Command#then or Command#execute. Some actions trigger animations in the UI that require a wait period, so you might need to insert a call to Command#sleep.

Add test assertions

Identify the points in the action sequence where conditions need to be tested and insert appropriate assertions.

Comment/uncomment UI actions

While debugging and developing a test based on generated code, it can be helpful to comment out all the UI actions and uncomment them 1 or 2 at a time, re-run the test, and observe the results. This can help you verify that each section of code is performing the intended action and if there are problems, identify exactly where the code is failing.

When invoking Intern from the command line the --leaveRemoteOpen argument is particularly useful – it will leave the web browser open after Intern finishes, allowing you to open the developer tools and inspect the state of the test page.

After fine tuning, the recorded test case might look like this:

define(function (require) {
	var tdd = require('intern!tdd');
	var assert = require('intern/chai!assert');
	tdd.suite('recorder-generated suite', function () {
		tdd.test('Test 1', function () {
			return this.get('remote')
				.get(require.toUrl('./Editor.html'))
				// Click the cell at row 1, col 3 to activate the editor
				.findByXpath('//TD[normalize-space(string())="one"]')
					.moveMouseTo(100, 16)
					.clickMouseButton(0)
					.end()
				// Wait for the editor to activate (indicated by presence of an input element)
				.findAllByCssSelector('.field-description input')
					.pressKeys('a')
					.end()
				// Click the cell at row 2, col 3 to activate the editor
				.findByXpath('//TD[normalize-space(string())="two"]')
					.moveMouseTo(96, 8)
					.clickMouseButton(0)
					.end()
				// Wait for the editor to activate (indicated by presence of an input element)
				.findAllByCssSelector('.field-description input')
					.pressKeys('b')
					.end()
				// Verify first edit was successful
				.findAllByCssSelector('td.field-description')
					.getVisibleText()
					.then(function (descriptionValues) {
						assert.strictEqual(descriptionValues[0], 'onea', 'Row 1 column 3 value should be edited');
					})
					.end()
				// Click somewhere to de-activate row 2's editor
				.findByXpath('//TD[normalize-space(string())="onea"]')
					.moveMouseTo(69, 13)
					.clickMouseButton(0)
					.end()
				// Verify second edit was successful
				.findAllByCssSelector('td.field-description')
					.getVisibleText()
					.then(function (descriptionValues) {
						assert.strictEqual(descriptionValues[1], 'twob', 'Row 2 column 3 value should be edited');
					});
		});
	});
});

Running tests

Once you are finished optimizing your tests, then you just include them in your normal Intern configuration and run them like any other tests in your suite!

Installing Intern Recorder

You can download and install Intern Recorder from the Chrome Web Store. Once installed, the Recorder will show up on the “Intern” tab in Dev Tools. Full usage instructions can be found in the README.

Making improvements

This version of the Intern Recorder was sponsored by SITA. If you or your company find Intern or Intern Recorder useful, please help support ongoing development of these tools and consider a similar sponsorship to add features and fixes you’d like to see! Just send us an email letting us know of your interest and we’ll set up a call to talk you through the process and what you can expect.

Learning more

We cover the creation of functional tests in our Intern and Dojo 202 workshops, and we support developers worldwide in their efforts with JavaScript and testing. If you would like to discuss how we can help your organization improve their approach to automated testing, please contact us to start the conversation.