📜 ⬆️ ⬇️

Functional testing of modern web applications


Modern web applications often contain many moving parts and third-party dependencies. In the process of refactoring and adding / changing functionality in such an application, the breakdown of existing use-case scripts and unstable work in certain browsers can occur.


For the timely detection of such situations and the implementation of continuous integration, functional testing of the web application is necessary. The article will discuss two free open-source solutions:



These solutions provide a similar, at first glance, a number of possibilities.



Function Test Example


An example for testing would be a TodoMVC-type web application, with a server on node.js and a client SPA page on React + Redux. To bring the testing conditions closer to real ones, random delays were added to all redux action actions that emulate network interaction with the backend (this is based on).


In the future, it will be assumed that the test web application is launched at http: // localhost: 4000 / . The functional test will be simple and include adding a todo-element, editing its contents, marking the completed / failed task and deleting it.


The language for writing tests in both frameworks is JS (ES2016 and ES5, respectively, for TestCafe and Nightwatch), but this is perfect for web applications written in any language. If you haven’t been developing on JS for a long time, then you need to take into account that modern editions are very far from the old ES3, and include convenient tools for writing code, object-oriented and functional programming, and much more.


TestCafe is installed with just one npm install -g testcafe . After completing the download and installation of the necessary dependencies, the test is executed by the testcafe <browser-name> <tests-directory>/ command in the appropriate directory.


The source code of the functional test checking the above use-case script may be as follows:


 import { expect } from 'chai'; import { Selector } from 'testcafe'; const MAX_TIME_AJAX_WAIT = 2500; // 2.5 seconds by default const querySelector = Selector((val) => document.querySelector(val), { timeout: MAX_TIME_AJAX_WAIT }); const querySelectorCondition = Selector((val, checkFunc) => { const foundElems = document.querySelectorAll(val); if(!foundElems) return null; for(let i=0; i < foundElems.length; i++) { if(checkFunc(foundElems[i])) return foundElems[i]; } return null; }, { timeout: MAX_TIME_AJAX_WAIT }); fixture `Example page` .page `http://localhost:4000/`; test('Emulate user actions and perform a verification', async t => { await t.setNativeDialogHandler(() => true); const inputNewTodo = await querySelector('header.header input.new-todo'); await t.typeText(inputNewTodo, 'New TODO element\r\n'); const addedTodoElement = await querySelectorCondition( 'section.main label', (elm) => (elm.innerText === 'New TODO element') ); await t.doubleClick(addedTodoElement); const addedTodoEditInput = await querySelectorCondition( 'section.main input[type=text]', (elm) => (elm.value === 'New TODO element') ); await t.typeText(addedTodoEditInput, ' changed\r\n'); const addedTodoCheckboxAC = await querySelectorCondition( 'section.main input[type=checkbox]:not([checked])', (elm) => (elm.nextSibling.innerText === 'New TODO element changed') ); await t.click(addedTodoCheckboxAC); const addedTodoCheckboxBC = await querySelectorCondition( 'section.main input[type=checkbox]', (elm) => (elm.nextSibling.innerText === 'New TODO element changed') ); await t.click(addedTodoCheckboxBC); const addedTodoDelBtn = await querySelectorCondition( 'button.destroy', (elm) => (elm.previousSibling.innerText === 'New TODO element changed') ); await t.click(addedTodoDelBtn); }); 

The address of the tested web page is determined in the fixture-part, followed by functional tests, upon completion of each of which the web-page is automatically restored to its original state. To search for DOM elements on the page, testcafe-specific Selectors are used, which are used as a wrapper for the function that will perform the query to the DOM model, possibly using arguments.


In this example, the first selector represents a wrapper over document.querySelector, and the second - above document.querySelectorAll with a callback function that helps select the desired item from the list. The Selector wrapper accepts options; in particular, the maximum time is set here during which testcafe will wait for an element with specified characteristics to appear in the DOM model.


The function test itself is a set of asynchronous calls to the Selectors, between which actions are performed by means of a test controller, instantiated by the variable t. The purpose of most of its methods is obvious from the names (click, typeText, etc.), and t.setNativeDialogHandler used to prevent the generation of alert-like windows that can “hang” the test — which is very convenient.


The installation of Nightwatch also starts with a simple npm install -g nightwatch , however, to run functional tests, you also need Selenuim-server (you can download it here , Java SE runtime must be installed on your computer) and webdrivers for each of the browsers that will be launched test.


To run the tests, you first need to create the nightwatch.json configuration file that describes the location of the tests, the paths and settings for the selenium-server and webdrivers, and then use the simple nightwatch command in the current directory.
If you use Microsoft Web Driver (Edge), then nightwatch.json might look something like this:


 { "src_folders" : ["nightwatch-tests"], "output_folder" : "reports", "custom_commands_path" : "", "custom_assertions_path" : "", "page_objects_path" : "", "globals_path" : "", "selenium" : { "start_process" : true, "server_path" : "nightwatch-bin/selenium-server-standalone-3.0.1.jar", "log_path" : "", "port" : 4444, "cli_args" : { "webdriver.edge.driver" : "nightwatch-bin/MicrosoftWebDriver.exe" } }, "test_settings" : { "default" : { "selenium_port" : 4444, "selenium_host" : "localhost", "desiredCapabilities": { "browserName": "MicrosoftEdge", "acceptSslCerts": true } } } } 

The source code of the function test, which checks the use-case script similar to the one discussed above, may look like this:


 module.exports = { 'Demo test' : function (client) { client.url('http://localhost:4000/') .waitForElementVisible('body', 1000) // May use CSS selectors for element search .waitForElementVisible('header.header input.new-todo', 1000) .setValue('header.header input.new-todo', ['New TODO element', client.Keys.ENTER]) // Or use Xpath - it's more powerful tool .useXpath() .waitForElementVisible('//section[@class=\'main\']//label[text()=\'New TODO element\']', 2000) .execute(function() { // For dispatch double click - Nightwatch doesn't support it by default var evt = new MouseEvent('dblclick', {'view': window, 'bubbles': true,'cancelable': true}); var foundElems = document.querySelectorAll('section.main label'); if(!foundElems) return; var elm = null; for(var i=0; i < foundElems.length; i++) { if(foundElems[i].innerText === 'New TODO element') { elm = foundElems[i]; break; } } elm.dispatchEvent(evt); }) .waitForElementVisible('//section[@class=\'main\']//input[@type=\'text\']', 2000) .clearValue('//section[@class=\'main\']//input[@type=\'text\']') .setValue('//section[@class=\'main\']//input[@type=\'text\']', ['New TODO element changed', client.Keys.ENTER] ) .waitForElementVisible( '//section[@class=\'main\']//label[text()=\'New TODO element changed\']' + '/preceding-sibling::input[@type=\'checkbox\' and not(@checked)]', 2000 ) .click( '//section[@class=\'main\']//label[text()=\'New TODO element changed\']' + '/preceding-sibling::input[@type=\'checkbox\' and not(@checked)]' ) .waitForElementVisible( '//section[@class=\'main\']//label[text()=\'New TODO element changed\']' + '/preceding-sibling::input[@type=\'checkbox\']', 2000 ) .click( '//section[@class=\'main\']//label[text()=\'New TODO element changed\']' + '/preceding-sibling::input[@type=\'checkbox\']' ) .waitForElementVisible( '//section[@class=\'main\']//label[text()=\'New TODO element changed\']' + '/following-sibling::button[@class=\'destroy\']', 2000 ) .click( '//section[@class=\'main\']//label[text()=\'New TODO element changed\']' + '/following-sibling::button[@class=\'destroy\']' ) .waitForElementNotPresent( '//section[@class=\'main\']//label[text()=\'New TODO element changed\']', 2000 ) .pause(2000) .end(); } } 

Among the features of the code, it is easy to see that Nightwatch does not support the double-click emulation of an element — instead, you have to implement a workaround with an inject-function on the client that performs dispatchEvent on the target control.


A convenient feature of Nightwatch is support for XPath, which provides much wider possibilities for selecting elements of the DOM model, compared to CSS selectors — to take at least an element extraction by its textual content, which is quite often found in the functional test.


TestCafe versus Nightwatch functionality


Installation :
[T] estCafe : One npm install -g testcafe , and you can immediately start writing and running tests in the current web project directory
[N] ightwatch : Although the installation of the npm module is done simply - npm install -g nightwatch , manual loading of the selenium-standalone-server, webdrivers for all browsers of interest, manual creation of the configuration file will be required (Or maybe even downloading Java, if it is not installed)


Selection of DOM elements :
T : The Selectors mechanism is used - wrappers over client JS functions that select one or multiple DOM nodes; this provides almost unlimited possibilities for selection, from simple wrappers over document.querySelector or document.getElementById to arbitrary logic for traversing DOM elements. At the same time, TestCafe independently provides for checking the presence / absence of elements according to a given Selector during the specified time.
N : There is a choice of CSS selectors or XPath - in principle, the second option is enough to solve most of the tasks for selecting elements on the page, although this certainly doesn’t compare with the ability to specify complex arbitrary search logic.


Test writing language :
T : From the box, it is possible to write tests directly in the ES2016 language, which allows you to write simple and readable test code in the same language as the web application itself. This is also convenient in cases when you need to import a specific module from the project under test.
N : Outdated ES5 syntax and exports-constructs in the source code of the functional test (You can still screw ES6, but on crutches)


Asynchronous operation support :
T : All API functions are based on Promise, which allows you to describe arbitrary asynchronous logic of the test, and at the same time integrate your own functions from the node.js side. With support for ES2016, this code can be written using async and await constructs in a consistent style.
N : In the test, you can implement a sequence of commands for analyzing and managing the contents of a web page, but they are added to the internal event queue, and integrating your own asynchronous functions with them is problematic.


Insert client code on the fly :
T : Easily performed using the appropriate API, supports the creation and subsequent execution of client functions, a single execution of an injected code, the replacement of existing functions in the source web page, as well as the execution of client functions in the callback node.js functions the controller.
N : There is functionality for executing JS code on the client side, or even inserting a whole script block, but no means of integration with the test controller is provided. Suitable for simple synchronous integrable JS code, but more generally integration is problematic.


Descriptions of statements and expectations :
T : By default, the usual assertion language is chai / expect, and you can use any other compatible assertion library.
N : The advanced assert and expect language, which includes tools for describing the state of a page, including the presence of an element and focus on it, belonging to a CSS class, and so on. It looks convenient, but primarily due to the lack of full support for asynchronous operations in the test, if necessary, the presence of statements and expectations.


Managing the tested web page :
T : Assumes the possibility of mock-a for the functions blocking the execution of scripts alert , confirm and so on, with the ability to set the return value. Complex manipulations with the DOM model, emulation of user interaction with controls, and even the ability to suspend the JS script due to wrapping client JS functions are supported.
N : Commands for direct control of the browser window displayed and the underlying DOM model are supported, integration with the client JS script is difficult


Working with the mouse cursor :
T : A virtual cursor is provided, through which hover, click and drag events are performed for the target visual elements of the page. During the test, you can observe the movement of the cursor and the actions performed.
N : Means for working with a cursor are in general - these are functions from webdriver api, but working with actions is more difficult than a single left click, rather problematic - to take at least a double click.


Implement browser interaction in TestCafe and NightWatch


The NightWatch framework is based on the well-known, to some extent already traditional, Selenium webdriver library, the benefits of which include an established API, a high degree of documentation and extensive Q & A on the Internet, as well as universal and fairly low-level access to the browser ... Interaction with browsers is organized through webdrivers, one for each browser.
The main disadvantage is that webdrivers do not exist for all browsers, and also require a separate update when changing the version of the browser itself, but also requires external dependency on Java. Read more about webdriver technology at http://www.w3.org/TR/webdriver/


The TestCafe framework is based on an approach in which interaction with the browser application is minimized - only the functions of opening the window / tab, resizing the window, navigating to the URL address, and some others are used.
The web page interacts with the testcafe-hammerhead web-proxy server, which loads the remote web page and modifies the source JS code so that it performs the functional test steps, through integration with the DOM model, searching and managing visual elements, performing arbitrary js code and so on. ( https://github.com/DevExpress/testcafe-hammerhead/ )


The TestCafe method is more versatile, as it can potentially work with any browser that supports HTML5 and ES 5+, while NightWatch requires an appropriate webdriver. In addition, it allows you to run tests not only on the local browser, but on any browser on the network, including any mobile - without installing any software on the phone.


An example of testing the above web application in the Android browser is shown in the following video: https://youtu.be/2na5jkqvUx0


However, testcafe-hammerhead has potential drawbacks: the overhead of analyzing and modifying the source JS code of the test page, produced in turn by the Testcafe core JS code, as well as theoretically incorrect work of the test web page or test integration, if the source code was proxied wrong. (For example, replacing an alert window in testcafe can be circumvented by such an example http://pastebin.com/p6gLWA75 - and, unfetteredly or specifically, “suspend” its execution)


findings


Of course, the selenium-webdriver, on which Nightwatch is based, is a popular and widely known solution that has a standard API interface, which is undoubtedly its advantage. In addition, in related task areas, for example, automating the target web resource in a given browser — actually writing a bot for a remote web site — selenium-webdriver is better suited.


However, for functional testing of a TestCafe web application being developed or supported is undoubtedly ahead for a wide range of reasons:


1) Running tests in any browser, including mobile phones and tablets, and this can be done in a batch manner for all browsers of interest and does not require installation of any additional software.
2) Convenience of writing a test on ES2016, including async / await constructs for sequential writing code, importing program elements from the project itself, transferring functions to the client and back, and so on - ample opportunities for integration and management of the client web application.
3) Broad support for selectors for visual elements, easy interaction with the DOM model, virtual mouse cursor, emulation of various and complex user interactions with the page.


Thus, among the existing open-source solutions for functional testing of web applications, TestCafe looks like a very attractive option, moreover, with the combination of lightness and functionality.


» Source codes of the application and functional tests


')

Source: https://habr.com/ru/post/315526/


All Articles