In the last post we used a demo application to discuss the development of maintainable Rich Web Applications with AngularJS. We introduced the most important concepts: The Model View Controller pattern and its application, the extension of HTML using directives and the routing concept to define the navigation between views.
This post explains how to integrate AngularJS into a build process based on Maven and shows how to develop automated unit tests and end-to-end tests for AngularJS applications.
Introduction
Maven build process
Maven is an established build tool in the Java world to automatically build, test, package and deliver applications. The resulting automated build process is repeatable on different machines and enables using continuous integration systems to build and test the application on dedicated servers. Thus, commits from different developers can be integrated and tested immediately, allowing early detection of problems.
With Maven, you can automatically build and test JavaScript projects as well. This allows to integrate the JavaScript client parts of complex server projects into the surrounding build process without the complexity of adding another build tool. We have already discussed how to “mavenize” JavaScript projects in a previous post. In this post, we focus on AngularJS projects, particularly the integration of tests in AngularJS applications.
Testing of AngularJS applications
Software tests can be classified into tests of isolated system components and tests of system component interaction. In this context, we can differentiate between three groups of tests: unit, integration and end-to-end tests.
Unit tests examine isolated source code units of a system, e.g. single classes or packages. Integration tests deal with the integration of a system or parts of it with third party systems, e.g. a database or a Java-EE container, in order to see if these interact correctly. In unit and integration tests, dependencies to system parts that are not under test (if existing) must be replaced by mocks. In the context of Rich Web Applications, a common requirement is to test the client independently from the server.
End-to-end tests (or acceptance tests) test a system in all its depth, as a black box, including the user interface and the server. The test acts from the perspective of a potential user by controlling the system’s user interface in order to test a certain functionality. Technically speaking, the test fires UI events in an automated browser and defines expectations on the resulting behavior of the user interface.
Unit-Tests
In AngularJS applications, unit tests can be implemented using Jasmine, a JavaScript test framework. The framework does not require an HTML-DOM, allowing to define unit tests that act independent from the HTML in the UI. AngularJS provides specific Jasmine extensions to test AngularJS apps.
The following code example shows a test of the BlogPostService
of the demo application. The test checks if the method fetchBlogPosts()
attempts to send a request with expected characteristics to the server (line 15). Note that, by using mocks, we avoid the necessity of a running server during test execution.
describe('BlogPostService test suite', function() { beforeEach(module('Services')); // ... describe('BlogPostList tests', function() { var data, responseData, requestMethod, requestUrl; it('should send get request to "rest/blog"', inject( function($httpBackend, BlogPostService) { data = getJSONFixture('blog-post-list.json'); $httpBackend.expectGET('rest/blog').respond(data); BlogPostService.fetchBlogPosts(). success(function(data, status, headers, config) { responseData = data; requestMethod = config.method; requestUrl = config.url; }); $httpBackend.flush(); expect(responseData).not.toBe(undefined); expect(requestMethod).toEqual("GET"); expect(requestUrl).toEqual("rest/blog"); } )); }); // ... });
Jasmine tests are based on the concept of fluent interface: The code is similar to natural language and therefore easier to read.
In the given example, we first define a test suite. For this, we call the global Jasmine function describe()
using two parameters: A string that defines the title of the test suite and a function that contains the implementation of the suite. A test suite can contain multiple specifications, each implemented using the function it()
. This function also expects the two mentioned parameters. Within a test specification, the test expectations are defined via the expect()
function.
First, in order to access the BlogPostService
from the test, the enclosing module must be registered (line 2). Fo this, we use the module()
function, an AngularJS extension of the Jasmine framework. Once the module is registered, the service under test can be injected into a test specification using the inject()
function (line 10). In this example, we inject the predefined mock service $httpBackend
as well as the service under test, BlogPostService
.
The test specification verifies whether the function fetchBlogPosts()
sends an HTTP GET
request to a particular URL. For this, we configure the HTTP Mock service with the expected request method and the expected request URL (line 13). Also, we define the mock data (data
) that will be returned by the mock service. To ease maintainability, these mock data are defined in a separate JSON file (blog-post-list.json).
Now, all expectations are defined and the fetchBlogPosts()
function can be called (line 15). As this function is asynchronous, we need to define a callback (success
) in order to verify the result (line 16-20). Next, the function flush()
of the HTTP mock service is called. It blocks execution until all waiting requests haven been processed. Finally, all expectations are checked (lines 23-25).
Our newly defined unit tests can now be executed, as explained later in the Maven integration section.
End-to-end tests
In order to implement end-to-end tests of AngularJS applications, we use the API of the AngularJS Scenario Runner. It enables the test to remotely control the application within a browser. The following test interacts with the user interface in order to create a user account in the blog application.
describe('E2E Tests', function() { describe('Registration test', function() { it('should redirect to "/login" after successful registration', function() { browser().navigateTo('/blog/#/register'); input('user.username').enter('jd'); input('user.password').enter('pwd'); input('user.firstname').enter('John'); input('user.surname').enter('Doe'); input('user.email').enter('john@doe.com'); element( '#registerSubmitBtn', 'registration form submit button' ).click(); expect(browser().location().url()).toBe('/login'); } ); }); });
A test scenario is defined in a syntax that is similar to Jasmine. The user interface can be controlled by predefined functions, e.g. navigateTo()
in order to access the registration page (line 5). Check this link for an overview of all functions.
Once the registration page is loaded, the form data is entered (line 6-10). Afterwards, it is submitted by firing a click event on the submit button. Finally, all expectations are checked, in this case a redirect to the login page.
The following section will explain how to execute end-to-end tests.
Maven integration
Unit tests
To integrate our unit tests into the Maven build process, we need the jasmine-maven-plugin, configured as follows:
com.github.searls jasmine-maven-plugin 1.2.0.0 true test ${project.basedir}/src/main/webapp/js ${project.basedir}/src/test/webapp/js/spec <!-- libraries and frameworks here --> <!-- AngularJS components here -->
By defining the Maven goal test
(line 9), our unit test execution is now linked to the Maven test phase. Thus, they are executed automatically during build, including builds on CI servers.
To quickly test changes during development, the Maven build may be too slow. To reduce turnaround, we can start a dedicated server environment capable of executing the Jasmine Spec Runner in order to run tests manually in a browser. This allows to quickly test development changes for possible errors. To start this environment, we use the jasmine-maven-plugin as follows:
$ mvn jasmine:bdd
As we defined our mock data in a separate JSON file, we now need to explicitly set the path to the mock file. For this, we create a JavaScript file with the following content:
jasmine.getJSONFixtures().fixturesPath = 'src/test/webapp/js/spec/javascripts/fixtures/json';
To restrict this path change to the Jasmine server environment, we define a Maven profile in the pom.xml as follows:
jasmine-spec-runner com.github.searls jasmine-maven-plugin ${project.basedir}/src/test/webapp/js/lib/jasmine-jquery-settings/jasmine_spec_runner_fixtures_path.js
By adding the created JavaScript file to the preloadSources
tag (line 10), it is loaded only if our profile is active. To avoid overriding the jasmine-maven-plugin settings, we use combine-children="append"
(line 9), so that the file does not replace our other sources.
Now, the profile has to be specified when starting the Maven goal bdd
.
$ mvn jasmine:bdd -Pjasmine-spec-runner
This starts the test server environment and the tests can be executed by calling the specified URL. We get the following test results in our example:
An error would lead to an output like this:
End-to-end tests
In our example, the integration of the end-to-end tests into the build process follows three steps:
- automatically start the web server
- start a remote controlled browser
- execute the AngularJS end-to-end tests
In order to start the web server automatically (step 1) and stop it later, we use Arquillian, a popular framework for tests against an embedded server (primarily in Java EE contexts). For this, we define a separate Maven module (angularjs-blog-e2e-tests
) with a single Arquillian test:
@RunWith(Arquillian.class) public class E2ETest { @Deployment(testable = false) public static WebArchive accessDeployment() { File war = MavenDependencyResolver.resolve( "de.akquinet.angularjs", "angularjs-blog-web", "1.0-SNAPSHOT", "e2etest", "war" ); return ShrinkWrap.createFromZipFile(WebArchive.class, war); } @Test public void runE2ETest() { // ... } }
The web application is defined using the return value of the method accessDeployment()
(line 5). In our example, we use a self defined helper class MavenDependencyResolver
which references and resolves our blog application using its Maven coordinates.
The test class contains one single test that starts a remote controlled browser (step 2). This test looks as follows:
// ... @Test public void runE2ETest() { WebDriver driver = new FirefoxDriver(); driver.get("http://localhost:8180/blog/angularjs-scenario-runner/runner.html"); ExpectedCondition e = new ExpectedCondition() { public Boolean apply(WebDriver d) { return !d.findElement(By.id("application")) .isDisplayed(); } }; Wait w = new WebDriverWait(driver, 20); w.until(e); // ... Assert.assertEquals(error.getText(), "0 Errors"); Assert.assertEquals(failure.getText(), "0 Failures"); driver.close(); } }
Using the test framework Selenium and its WebDriver API we start the embedded browser and run the AngularJS Scenario Runner (step 3) (lines 4, 5).
To let the Maven build fail on a test failure, we define two assertions (lines 18, 19). In order to delay checking these assertions until all tests have been executed, the WebDriver
has to wait until a certain event occurs. As the Scenario Runner loads the application within an iFrame and removes it after all tests have been completed, the verification of the visibility of the iFrame is a suitable criterion. It makes sure that all tests have been completed and the analysis of the results can begin. The delay logic is defined in lines 7-14.
Done! Now, the end-to-end tests are executed during the maven build process.
Conclusion
In our first post, we presented a demo application in order to show how to develop Rich Web Applications with AngularJS. We introduced the most important concepts of AngularJS: The extension of HTML by directives, the two-way data binding, the application of the Model View Controller pattern and the routing concept. In this second post we have shown how to implement unit and end-to-end test as well as how to integrate those into a Maven-based build process of a server application.
Being a relatively new framework on the field, integrating AngularJS into enterprise applications is more risky than relying on established and standardized solutions. At the same time, the W3C draft “Web Components” submitted by Google is similar to the directives concept and already points in a possible direction of standardization.
AngularJS eases certain tasks, e.g. by allowing two-way data binding and the injection of dependencies. While definitely simplifying JavaScript logic, it might complicate troubleshooting in complex projects.
The concept of directives leads to a distinct separation of static UI description and dynamic UI logic. Views are defined in plain HTML and extended with dynamics using directives, thus achieving a clean separation of presentation and functionality. The two-way data binding renders DOM manipulation code as well as code for monitoring user interactions unnecessary.
AngularJS offers a modularization concept to encapsulate related components like controllers and services. Dependencies between modules are resolved using dependency injection, leading to a loose coupling.
All these aspects allow for well-testable AngularJS applications. Using Jasmine and the Scenario Runner of AngularJS, we can define both unit tests and end-to-end tests. As shown, the integration of those tests into a Maven-based build process is also possible. While the integration of unit tests via the jasmine-maven-plugin is very easy, embedding the end-to-end-tests is initially a bit more time-consuming.
AngularJS applications can be hooked up to arbitrarily complex server backends via a REST interface in a convenient manner, by using predefined AngularJS services.
In conclusion, we consider AngularJS to be a very impressive and promising solution.
Feel free to contact us via email for any questions and remarks:
philipp.kumar [at] akquinet.de
till.hermsen [at] akquinet.de
Hi,
This is a great series of posts.
The jasmine unit tests run fine. The e2e, tho are not what I expected. I was looking more for running the e2e tests using the Karma runner.
Any suggestions on how e2e tests using the Karma runner can be run via the maven-jasmine-plugin?
Thanks!
Nice post. How about using Maven to resolve javascript dependencies?
hi,
Thank you for submitting,
i suggest the part 3 : “integrate a osgi architecture” ;-).
Hi Guys,
there are more Arquillian goodies you could use:
1/ Arquillian Drone to inject WebDriver instance, e.g.
@Drone WebDriver driver;
https://github.com/arquillian/arquillian-extension-drone
https://docs.jboss.org/author/display/ARQ/Drone
2/ Arquillian QUnit (labs) which is our JavaScript JUnit runner nicely wrapping QUnit results, reporting js lines, etc. all from IDE/JUnit.
https://github.com/qa/arquillian-qunit
(here is 1.0.0 baseline) https://github.com/tolis-e/arquillian-qunit
How test looks like:
https://github.com/tolis-e/arquillian-qunit/blob/master/arquillian-qunit-ftest/src/test/java/org/jboss/arquillian/qunit/junit/ftest/QUnitRunnerTestCase.java
If you dig especially into 2/, changing QUnit to Jasmine might be really easy, so any comments, bugs reports, feature requests, pull requests are very welcomed
Thanks,
Karel
Hi Karel,
thanks for the input! Especially the runner looks promising, will check it out.
Thanks
Philipp
Hi Guys,
there are more Arquillian goodies you could use:
1/ Arquillian Drone to inject WebDriver instance, e.g.
@Drone WebDriver driver;
https://github.com/arquillian/arquillian-extension-drone
https://docs.jboss.org/author/display/ARQ/Drone
2/ Arquillian QUnit (labs) which is our JavaScript JUnit runner nicely wrapping QUnit results, reporting js lines, etc. all from IDE/JUnit.
https://github.com/qa/arquillian-qunit
(here is 1.0.0 baseline) https://github.com/tolis-e/arquillian-qunit
How test looks like:
https://github.com/tolis-e/arquillian-qunit/blob/master/arquillian-qunit-ftest/src/test/java/org/jboss/arquillian/qunit/junit/ftest/QUnitRunnerTestCase.java
If you dig especially into 2/, changing QUnit to Jasmine might be really easy, so any comments, bugs reports, feature requests, pull requests are very welcomed 🙂
Thanks,
Karel