JavaScript Tests and Their Automation

This post is also available in: Russian

Test case efficiency is vital for large scale projects, when parts of applications vary in their behavior. Perhaps the most common case is when a large group of developers is working on the same or interfacing modules. This often results in unexpected changes in behavior of functions written by other programmers. Also, firefighting sometimes may result in inadvertent changes to critical parts of the application.

Testing of a Web application usually involves visual review of page elements and empirical assessment of the functional performance. In other words, a tester browses the application sections and makes actions on dynamic elements.

Over time, the project grows in functionality so that its testing becomes longer and more sophisticated. To automate the process, modular (unit) testing is used.

There are two approaches to build test cases:

  • Whitebox Testing – tests are based on the implemented functionality. It means that testing uses the same algorithms that are run by the system modules. This approach fails to ensure end-to-end system performance.
  • Blackbox Testing – test cases are based on specifications and system requirements. This way, you can verify the end results of your application, however you cannot troubleshoot small and occasional bugs.

What to Test

It could have seemed that you should test every feature you have implemented in your app. This is not entirely the case. It takes some time for the developer to write tests, so to optimize the development process write tests just for complex, critical features or that using results from other modules.  Use tests to cover controversial logic that can potentially cause errors. Also it is worth creating tests for those parts of the code that you plan to optimize, so that after optimization you can assess their performance.

Generally, it is essential to assess testing costs against your timeframes. Of course, if you’re not limited in time, you can afford covering each feature by tests.  But as a rule, development process runs under a hard time pressure, and the analyst or an expert developer should be able to discern where testing is necessary. Moreover, writing of tests increases the cost of the project.

Based on this, we can formulate three cases where unit testing is justified:

1)     When tests ensure quicker bug identification than common troubleshooting.

2)     When tests reduce debugging time

3)     When tests apply to frequently changed code.

Of the 3 major frontend components (HTML, CSS, JavaScript), perhaps only the JavaScript should be tested. CSS is tested but visually, when a developer/tester/customer browses the GUI in different browsers. HTML markup is tested in a similar way.

How to Test

While building your test cases, please adhere to the following guidelines:

  • Make your tests as simple as possible. In this case, the test result is more likely to indicate that very bug you are trying to reproduce.
  • Decompose tests run on large modules. This will make it easier to locate a specific bug .
  • Make your tests independent. The result of one test should in no case depend on the result of another one.
  • Test results should be fully repeatable and expectable. Each time you rerun your test, the result should be identical to the previous run.
  • For any application error, a separate test case should be created. This way you can make sure that a bug has really been fixed and would not emerge to the end users.

How to Test

For unit testing of JS code, several libraries exist. Perhaps the most commonly used library is QUnit. To run unit tests using this library, we should create a "sandbox", a simple HTML page hosting a test library, your tested code, and the tests themselves.

Test Functions:

(function() {
    window.stepen = function(int) {
        var result = 2;

        for (var i = 1; i< int; i ++) {
            result = result * 2;
        }

        return result;
    }

    window.returnFunc = function() {
        return 'ok';
    }
})();

Test Listing:

test('stepen()', function() {
    equal(stepen(2), 4, '2^2 - equal method');
    ok(stepen(3) === 8, '2^3 - ok method');
    deepEqual(stepen(5), 32, '2^5 - deepEqual method');
});

asyncTest('returnFunc()', function() {
    setTimeout(function() {
        equal(returnFunc(), 'ok', 'Async Func Test');
        start();
    }, 1000);
});

As you can see, QUnit supports three functions to validate code results against the expected performance:

  • ok() – considers the test successful if the return value = true
  • equal () – validates result against expectation
  • deepEqual () – validates result against expectation, including result type

Execution result:

As you can see, QUnit can test your code in multiple browsers at once.

There is a number of other unit test libraries. However, they share the same concept of building test cases, so you can easily move to another library, if needed.

Please Keep in Mind

Modern JS code is asynchronous. As a rule, test libraries support asynchronous tests. But if you are trying to test a feature that is sending a GET request to the backend and returning a response from it, you’ll have to stop the flow using stop(), run the tested feature and then re-start the flow using start() wrapped in setTimeout().  So you have to make for a certain time for the function to complete.  Please choose carefully the period length as, on the one hand, lengthy execution of a method may be specific or even necessary to an application or signify of its error.

Testing Backbone Applications

To test applications based on Backbone.js, let’s use the project described in our post "How to Develop Large-Scale JavaScript Based Applications."

Using unit tests, you can validate:

  • Your models and controllers
  • Data used in the models
  • Controller method execution (for this, they have to return a result)
  • Success of loading the views

Test code:

test('Backbone.js', function() {
    ok(sample, 'Namespace check');

    ok(sample.routers.app, 'Router check');
    ok(sample.core.pageManager.open('chat'), 'Page opening test
                 (Controller method call)')

    ok(sample.core.state, 'Model check');
    equal(sample.core.state.get('content'), 'sintel', 'Model data get test');

    stop();
    ok(function() {
        $.ajax({
            url: 'app/templates/about.tpl',
            dataType: 'text'
        }).done(function(data) {
                self.$el.html(data);
                return data;
            })
    }, 'Template loading check');

    setTimeout(function() {
        start();
    }, 1000);
});

Result of testing error handling:

Automating Test Execution

As a rule, application deployment is quite a frequent task in heavy development. Therefore, this operation is typically automated. We prefer to use Jenkins, a continuous integration tool. The idea is to combine Jenkins based deployment with automated tests.

QUnit tests are run in a browser. To bypass this, phantomjs is used to emulate the browser. Phantomjs developers have made a preset script to run QUnit tests, but we updated it a bit for better performance.

Test.js:

/**
 * Wait until the test condition is true or a timeout occurs.
 * Useful for waiting
 * on a server response or for a ui change (fadeIn, etc.) to occur.
 *
 * @param testFx javascript condition that evaluates to a boolean,
 * it can be passed in as a string (e.g.: "1 == 1" or
 * "$('#bar').is(':visible')" or
 * as a callback function.
 * @param onReady what to do when testFx condition is fulfilled,
 * it can be passed in as a string (e.g.: "1 == 1" or
 * "$('#bar').is(':visible')" or
 * as a callback function.
 * @param timeOutMillis the max amount of time to wait. If not
 * specified, 3 sec is used.
 */
function waitFor(testFx, onReady, timeOutMillis) {
    var maxtimeOutMillis = timeOutMillis ? timeOutMillis : 3001,
            //< Default Max Timout is 3s
        start = new Date().getTime(),
        condition = false,
        interval = setInterval(function() {
            if ( (new Date().getTime() - start < maxtimeOutMillis) &&
                   !condition ) {
                // If not time-out yet and condition not yet fulfilled
                condition = (typeof(testFx) === "string" ? eval(testFx)
                           : testFx());
                   //< defensive code
            } else {
                if(!condition) {
                    // If condition still not fulfilled
                    // (timeout but condition is 'false')
                    console.log("'waitFor()' timeout");
                    phantom.exit(1);
                } else {
                    // Condition fulfilled (timeout and/or condition is
                    //'true')
                    console.log("'waitFor()' finished in " +
                         (new Date().getTime() - start) + "ms.");
                    typeof(onReady) === "string" ? eval(onReady)
                         : onReady();
                      //< Do what it's supposed to do once the
                      // condition is fulfilled
                    clearInterval(interval); //< Stop this interval
                }
            }
        }, 100); // repeat check every 250ms };
};

if (phantom.args.length === 0 || phantom.args.length > 2)
    console.log('Usage: run-qunit.js URL');
    phantom.exit();
}

var page = new WebPage();

// Route "console.log()" calls from within the Page
// context to the main Phantom context (i.e. current "this")
page.onConsoleMessage = function(msg) {
    console.log(msg);
};

page.open(phantom.args[0], function(status){
    if (status !== "success") {
        console.log("Unable to access network");
        phantom.exit();
    } else {
        waitFor(function(){
            return page.evaluate(function(){
                var el = document.getElementById('qunit-testresult');
                if (el && el.innerText.match('completed')) {
                    return true;
                }
                return false;
            });
        }, function(){
            var failedNum = page.evaluate(function(){
                var el = document.getElementById('qunit-testresult');
                console.log(el.innerText);
                try {
                    return document.getElementsByClassName('fail')[0].
                                    innerHTML.length;
                } catch (e) { return 0; }
                return 10000;
            });
            phantom.exit((parseInt(failedNum, 10) > 0) ? 1 : 0);
        });
    }
});

To display results in the message console, add the logging function:

QUnit.log = function(test){
            if (!test.result) {
                console.log('- ' + test.message +
                 ' (expected:' + test.expected + '; actual: ' +test.actual);
                console.log('    ' + test.source);
            } else {
                console.log('+ ' + test.message);
            }
        }

Now let’s launch Jenkins, create a new project and make a new job in it:

Add a new build step with a shell command:

Now let’s enter the following command:

phantomjs test.js test.html

Now we just have to specify a build directory (i.e., that hosting a sandbox) and run the build:

Example of test failure:

Helpful links

Leave a Reply

HTTP Streaming: Load Balancing

This post is also available in: Russian

balancing-http-streaming

In most cases, modern video content delivery systems are based on HTTP Streaming. This technology delivers video in a series of fragments (chunks), several seconds each. The most popular video delivery formats are Apple HTTP Live Streaming and Adobe HTTP Dynamic Streaming. In the near future, MPEG DASH will also probably become popular. The benefits offered by this type of delivery consist in a more stable video platform operation over a heterogeneous public network (Internet) and use of existing mechanisms of traditional CDN networks (HTTP chunk caching). But often traditional media is not sufficient, and large-scale online video services usually require a more flexible management of video content delivery. In our projects, we use our Video Load Balancer. In this post, we are going to outline the basics of its operation that might be helpful to your projects regardless of the load balancing technology you use. (more…)

Leave a Reply

HTML5 Application Optimization

This post is also available in: Russian

This article provides tips on how to optimize the client part of large-scale Web projects (most of the recommendations also apply to small projects, however). Just within a few years, Web sites have made a giant leap, having evolved from simple static pages to complex applications. The website frontends have as dramatically grown in size and logic, and now they demand no less attention in the context of optimization than backends, as the frontends are responsible for interacting with the users. (more…)

Leave a Reply

iOS DRM: Secure iOS Video Delivery and Playback

This post is also available in: Russian

HTTP Live Streaming (HTTP LS) is a protocol strongly recommended and actively promoted by Apple as the best solution for online video delivery to mobile devices. But neither the protocol itself, nor its tools offered by the iOS API, implement a full-fledged DRM support. The new version of Adobe Access 4.0 adds this missing functionality to the iOS-devices. (more…)

Leave a Reply

Overviewing Best Practices in iOS Application Development

This post is also available in: Russian

It is usually said, that the most important driver behind Apple’s mobile devices popularity is its maximum attention to detail. Small, seemingly insignificant details make up product’s appeal for the end user. But to develop a good application, it is not sufficient to give heed to detail at the user interface level. You need to be aware of and comply with the best practices at all stages of development, from design to support. This paper gives an overview of such best practices presented both by Apple and by independent iOS developers. (more…)

Leave a Reply

How to Develop Large-Scale JavaScript Based Applications

This post is also available in: Russian

Development of interactive Web services involves extensive use of JavaScript. If 10 years ago the language had but a meager functionality, today JS has substantially transformed. Therefore, more of service logics is moved to the client side. In this post, we will share with you our hands-on experience in designing and implementing large-scale Web applications. (more…)

Leave a Reply

Handling Camera in iOS Applications

This post is also available in: Russian

In the latest iOS based devices, camera is a major driver of popularity of these devices. The capability to capture video and encode it to H.264 High Definition on the hardware level that emerged in iPhone 4, has been accepted enthusiastically by both users and developers of new applications. Continuing our series dedicated to AV Foundation and the related frameworks, in this post we will discuss how to capture a stream from the camera and how to process, save and distribute it. (more…)

Leave a Reply

Video Playback in iOS Applications

This post is also available in: Russian

iOS video player

Apple iOS mobile devices have deservedly won universal popularity among the customers. Every Apple’s event is followed with a keen interest as the people expect the emergence of new functionality and improvement of existing features. But the common users can only skim the surface, assessing the externals of the product. Much more is visible to the developers. Of all the four-day WWDC event, the public attention has been given to the keynote presentation only, giving an overview of all the major innovations. All the other presentations, totaling to about one hundred, have been dedicated to the developers. From this ratio, we can estimate the hidden mass of the iceberg, which is no less fascinating than its tip. Starting a series of posts on iOS based development, we will tell you of the Apple devices built-in frameworks to support online video handling. (more…)

Leave a Reply

OTT: Video Services on LG TV

This post is also available in: Russian

Over the past two years, we have witnessed a rapid growth of a new trend in the mass TV set market. Next generation TV sets that are connected to the Internet and empowered with an application platform, are becoming exceedingly popular. Like with smart phones, the TV applications can be supplied with a device or installed from a directory. The mass media and promotional publications often dub such an approach as "Smart TV" or, less frequently, "Connected TV" or "Hybrid TV". We believe that this trend is mostly neglected today by the developer community. It is little written and spoken about, but at the same time, it offers tremendous opportunities to attract that part of the audience which is poorly or even not yet covered by the ubiquity of computer and smart phone technology. In particular, for applications designed to deliver video content, the television video platforms are a natural environment, and they should be given the same attention as the mobile applications. (more…)

Leave a Reply