Skip to Content

Introduction

Unit-tests are one of the crucial tools in Test-Driven Development (TDD) and should be an important pillar of your test strategy. If you’re coming from a different programming language or infrastructure setup and want to develop your JavaScript application test-driven, two main questions need to be solved:

  1. How can you setup your infrastructure to efficiently execute your unit tests?
  2. How can you write testable code in JavaScript?

This article tackles the first topic. It is part of a series of articles about Test-Driven Development and JavaScript in which also the second topic will be covered in depth. Press ‘like’ on the overview blog post to stay tuned and get updates about more topics around software engineering with SAPUI5 and JavaScript!

Scope of this Article

This article assumes that you’re building your project with maven. If your backend is implemented in Java, maven is one of the most popular ways to manage your dependencies and automate your build. Some parts and takeaways are still useful when you employ a different build technology but want to use jasmine for unit testing UI5 applications or if you use maven but want a different unit testing framework, such as QUnit.

The specific goals to achieve with the descriptions in this article are

  • to execute tests during development and have fast feedback cycles
  • to execute tests during local maven build
  • to execute tests on an Continuous Integration (CI) server such as jenkins

Furthermore, the tests during a maven build should require no additional setup on a developer’s workplace or on your CI server.

Challenges with JavaScript Unit Testing

If you have used a JavaScript unit testing framework such as jasmine or QUnit before, you know how they work: You write put your test code into a test.js file. Then they ask you to embed your test.js and productiveCode.js within a Test.html file containing some additional boilerplate, such as an area to display the test results, the test framework code itself, and any additional libraries you might need. When you open the Test.html with your browser, the tests are executed and the results are shown on the page. For small examples, this is easy, however, in the real world we encounter a number of challenges.

[High maintenance effort] You’ll end up with many test.js files, usually one for each unit of code you want to test. Every new file has to be embedded in a Test.html to actually execute the tests. This will lead to a large number of copy-and-pasted Test.html files which have to be maintained individually. This problem is exacerbated by the use of custom JavaScript libraries such as UI5 and by the size of projects when they grow bigger. Custom libraries may bring their own modularization concepts (such as UI5’s declare/require or require.js’s define/require) which have to be integrated into your Test.html as well.
[Simple feedback access during development] With a growing number of tests and Test.html files it gets harder for developers to simply check the test results when they are making changes to the code. The test infrastructure should provide whether all tests passed of a test failed at a glance.
[Modularization and Dependencies] UI5 and require.js load dependencies during runtime, while your browser’s Same-origin Policy denies loading files from your local filesystem. The two possible solutions are either deactivating this security setting just to execute your local tests or to manually set up a web server which contains all necessary test and productive JavaScript files, neither of which is desirable.
[Automatic test execution] The CI server should be able to execute the tests automatically during a build.
[Self-contained setup] The project’s build and test should be completely self-contained and require no additional setup. In many cases, you don’t have file-system access to your CI server itself and cannot rely on certain programs or libraries to be present on the path. Furthermore, everything that adds complexity to the setup of a developer’s workplace should be avoided.

Proposed Solution: jasmine-maven-plugin & PhantomJS

The jasmine-maven-plugin provides a solution for some of the previously stated challenges:

  • It collects all of your test.js files and embeds them into a Test.html file based on a template you provide. There are some pre-defined Test.html.template files available, e.g. for frameworks such as require.js which can be adapted for UI5 with little effort. This solves the challenges of high maintenance effort and simple feedback access: No manual embedding of test.js files and dependencies is necessary, and the framework bootstrapping can be maintained in a single file, your Test.html.template. Results are presented on a single page, providing a fast overview.
  • During development, executing mvn jasmine:bdd starts a web server at http://localhost:8234. This helps you to overcome the challenge of either deactivating web security settings or maintaining your own server for testing.
  • During a build, automatic execution can be achieved with PhantomJS, a headless webkit implementation in Qt. Let’s assume for the first version of this setup that it is installed on your CI server and developer machines. This assumption will be removed in one of the next setup steps.

This clearly leaves the challenge of a self-contained setup. Using another maven dependency, this can be solved as well. Pre-packaged binaries of PhantomJS are available as maven dependencies which can be downloaded and extracted during the build. Note that these binaries are OS-specific, which complicates the setup.

Technical details: How to configure your setup

The setup is described in three parts: Using the jasmine-maven-plugin, automatically installing PhantomJS during the build, and integrating SAPUI5 as a test dependency.

Using the jasmine-maven-plugin

Add the following example configuration for the jasmine-maven-plugin to your pom.xml:


<plugins>
 <plugin>
  <groupId>com.github.searls</groupId>
  <artifactId>jasmine-maven-plugin</artifactId>
  <version>1.3.1.4</version>
  <executions>
  <execution>
    <goals>
    <goal>test</goal>
    </goals>
  </execution>
  </executions>
  <configuration>
  <!-- Where is your productive JavaScript code -->
  <jsSrcDir>${basedir}/src/main/js</jsSrcDir>
  <!-- Where are your test.js files -->
  <jsTestSrcDir>${basedir}/src/test/js</jsTestSrcDir>
  <!-- Use PhantomJS to access the Test.html, assuming it is available on your PATH -->
  <webDriverClassName>org.openqa.selenium.phantomjs.PhantomJSDriver</webDriverClassName>
  <!-- Create a report in surefire format to be read by jenkins -->
  <junitXmlReportFileName>../surefire/TEST-${project.name}.jasmine.xml</junitXmlReportFileName>
  <!-- Execute tests automatically every 60 seconds -->
  <autoRefreshInterval>60</autoRefreshInterval>
  </configuration>
 </plugin>
</plugins>

Installing PhantomJS during the build

PhantomJS can be installed as a binary from the maven central repository by using the maven-dependency-plugin. Add this execution to your pom.xml:



<plugin>
  <artifactId>maven-dependency-plugin</artifactId>
  <version>2.6</version>
  <executions>
    <execution>
      <id>extract-js-unit-tests-runtime</id>
      <phase>process-test-resources</phase>
      <goals>
        <goal>unpack</goal>
      </goals>
      <configuration>
        <markersDirectory>${project.build.directory}/js-tests</markersDirectory>
        <overWriteReleases>false</overWriteReleases>
        <overWriteSnapshots>false</overWriteSnapshots>
        <outputAbsoluteArtifactFilename>true</outputAbsoluteArtifactFilename>
        <artifactItems>
          <artifactItem>
            <groupId>org.jboss.arquillian.extension</groupId>
            <artifactId>arquillian-phantom-binary</artifactId>
            <version>1.9.2</version>
            <classifier>${phantomjs.os.classifier}</classifier>
          </artifactItem>
        </artifactItems>
        <outputDirectory>${project.build.directory}/js-tests/phantomjs</outputDirectory>
        <stripVersion>true</stripVersion>
      </configuration>
    </execution>
  </executions>
</plugin>

This code uses the variable phantomjs.os.classifier to distinguish between the different binaries for your operating system. This variable can be filled using profiles which are activated based on the used operating system:


<!-- Set OS-type variable used for installing phantomjs binary -->
<profile>
 <id>linux</id>
 <activation>
  <os>
  <family>unix</family>
  </os>
  <activeByDefault>false</activeByDefault>
 </activation>
 <properties>
  <phantomjs.os.classifier>linux-64</phantomjs.os.classifier>
  <phantomjs.path>${project.build.directory}/js-tests/phantomjs/bin/phantomjs</phantomjs.path>
 </properties>
 <!-- Set the executable bit for the phantomjs.sh -->
 <build>
  <plugins>
  <plugin>
    <artifactId>maven-antrun-plugin</artifactId>
    <version>1.7</version>
    <executions>
    <execution>
      <id>make-phantomjs-executable</id>
      <phase>process-test-classes</phase>
      <goals>
      <goal>run</goal>
      </goals>
      <configuration>
      <target>
        <chmod file="${phantomjs.path}" perm="ugo+rx" />
      </target>
      </configuration>
    </execution>
    </executions>
  </plugin>
  </plugins>
 </build>
</profile>
<profile>
 <id>mac</id>
 <activation>
  <os>
  <family>mac</family>
  </os>
  <activeByDefault>false</activeByDefault>
 </activation>
 <properties>
  <phantomjs.os.classifier>macosx</phantomjs.os.classifier>
  <phantomjs.path>${project.build.directory}/js-tests/phantomjs/bin/phantomjs</phantomjs.path>
 </properties>
</profile>
<profile>
 <id>windows</id>
 <activation>
  <os>
  <family>windows</family>
  </os>
  <activeByDefault>false</activeByDefault>
 </activation>
 <properties>
  <phantomjs.os.classifier>windows</phantomjs.os.classifier>
  <phantomjs.path>${project.build.directory}/js-tests/phantomjs/phantomjs.exe</phantomjs.path>
 </properties>
</profile>

Afterwards, the variable phantomjs.path contains the path to the executable. Add a property pointing to this path in the configuration of the jasmine-maven-plugin:


<webDriverCapabilities>
    <capability>
          <name>phantomjs.binary.path</name>
          <value>${phantomjs.path}</value>
    </capability>
</webDriverCapabilities>

Adding UI5 as a custom library to the setup

So far, you can test JavaScript code, but cannot use UI5 controls or rely on the declare/require statements for modularizing your code. To solve this, we must include a UI5 distribution in the test web server provided by the jasmine-maven-plugin and provide a custom Test.html.template containing bootstrap code. Note, that this code uses the UI5 version available over the content delivery network (CDN) at https://sapui5.hana.ondemand.com/. If UI5 was available at as a maven dependency, you could simply add an execution to the maven-dependency-plugin and unpack a specific version yourself.

You now have to create a custom Test.html.template to load and bootstrap UI5. Create this file, e.g. in src/test/resources and fill it with this content:


<!DOCTYPE html>
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=$sourceEncoding$">
  $if(autoRefresh)$
  <meta http-equiv="refresh" content="$autoRefreshInterval$">
  $endif$
  <title>Jasmine Spec Runner for UI5</title>
  $cssDependencies$
  $javascriptDependencies$
  <script type="text/javascript" src="https://sapui5.hana.ondemand.com/resources/sap-ui-core.js"
  id="sap-ui-bootstrap"
  data-sap-ui-libs="sap.ui.commons,sap.ui.ux3,sap.ui.table,sap.ui.layout"
  data-sap-ui-theme="sap_bluecrystal">
  </script>
  <!-- Register your module prefix paths with UI5 -->
  <script type="text/javascript">
    jQuery.sap.registerModulePath("my.prefix.name", "/src/my/prefix/name");
  </script>
  $allScriptTags$
</head>
<body>
  <script type="text/javascript">
     var specs = $specsList$;
    var executeJasmineSpecs = function(){
      window.reporter = new jasmine.$reporter$(); jasmine.getEnv().addReporter(reporter);
      if ('$reporter$' == 'HtmlReporter') {
        jasmine.getEnv().specFilter = function(spec) {
          return window.reporter.specFilter(spec);
        };
      }
  //Don't do live updates when running through HTMLUnit
      if ('$reporter$' == 'JsApiReporter'){
  jasmine.getEnv().updateInterval = Number.MAX_VALUE;
      }
      jasmine.getEnv().execute();
    };
    if (window.addEventListener) {
      addEventListener('DOMContentLoaded', executeJasmineSpecs, false);
    } else {
      attachEvent('onload', executeJasmineSpecs);
    }
  </script>
</body>
</html>

Please note that you have to Register your module prefix paths with UI5 in this template. While the resolution of declare/require statements works automatically in your application, the location of the productive JavaScript code will be different on the test server: an additional ‘/src’ will be added in front of all paths. For example, if you have declared your own library/namespace for the file SomeFile with jQuery.sap.declare(“my.own.library.SomeFile”); and have your JavaScript sources in /src/main/js, you need to configure this like in the above template. Be sure to adapt this to your project specific needs.

Afterwards, configure this template to be used in your tests. Add these lines to the configuration of the jasmine-maven-plugin:


<!-- Use a custom template to generate the Test.html. This includes UI5 bootstrapping -->
<customRunnerTemplate>${basedir}/src/test/resources/Test.html.template</customRunnerTemplate>

Get Your Hands Dirty: Download an Example Project

You can download a pre-configured example project as an extension to Jens Glander’s excellent PersonsList Project, which is available on GitHub. Just follow his detailed instructions and be sure to get Extension-003 by synching branch origin/Extension-003 of the project.

A test suite is located in personslist/personslist-web/src/test/js/. The tests are running in each maven build of the complete project. Additionally, you can execute mvn jasmine:bdd within the personslist-web folder and point your browser to http://localhost:8234 to execute your tests. The tests are re-executed every 60 seconds or when you press F5 in your browser.

When you start adding new tests, simply create a new file within the above folder or extend the already existing suite. To see changes in test code or productive code take effect, a refresh in the browser is enough!

Conclusion

In this article, I described how you can use a combination of the jasmine, jasmine-maven-plugin, and PhantomJS

  • to execute tests during development and have fast feedback cycles
  • to execute tests during local maven build
  • to execute tests on an Continuous Integration (CI) server such as jenkins

with no additional setup on a developer’s workplace or on your CI server.

The tests enable you to write your JavaScript code TDD-style and have fast-feedback cycles during development.

Now it’s your turn: Write some clean and tested code!

This blog post is part of a series, presslike’ on the overview blog post to stay tuned and get updates about more topics around software engineering with SAPUI5 and JavaScript.

To report this post you need to login first.

3 Comments

You must be Logged on to comment or reply to a post.

  1. Kais KHELIFA

    Hello Marco,

    Thank you for the great explanation 🙂

    I’m having a trouble with the jQuery.sap.registerModulePath, could you please explain that paragraph, how do i register the sapui5 library path in the test.html.template. What exactly is the sapui5 library.

    Thank you in advance,

    Kais.

    (0) 
    1. Marco Voelz Post author

      Dear Kais,

      actually, my wording in the article is not correct – I should speak of ‘modules’, not ‘libraries’ in this point. I will correct that in the original article.

      Now to your question: I assume that you put your javascript files in a unique namespace (in java you would call this a package). In the above example app that is ‘personslist-web’.

      If you would have other javascript files than just views, which are included by using jQuery.sap.require(‘personslist-web.MyFile’), UI5 would try to resolve find these files by translating ‘personslist-web.MyFile’ into the folder structure ‘personslist-web/MyFile.js’. This path will be searched for relatively to the location of your sap-ui-core.js.

      However, your JavaScript code is in a completely different location, namely src/main/webapp/personslist-web. The jQuery.sap.registerModulePath() method can be used to tell UI5 where to find JavaScript files prefixed with ‘personslist-web’ by executing registerModulePath(‘personslist-web’, src/main/webapp/personslist-web’). Using jasmine complicates this even more: The directory specified in your pom.xml with <jsSrcDir>${basedir}/src/main/webapp/personslist-web</jsSrcDir> is mounted under the folder ‘src’ in the webserver created by the jasmine-maven-plugin. Therefore, the path in your Test.html.template will be something like registerModulePath(‘personslist-web’, src).

      Unfortunate enough, my provided example application works without this registration mechanism, as views are resolved with a different mechanism, it seems.

      I hope this could clarify your question?

      Warm regards

      Marco

      (0) 

Leave a Reply