Introduction
There are few test frameworks for the JSF applications and components development. The most popular is the SHALE Test Framework http://shale.apache.org/shale-test/index.html, which uses the Mock object approach, and JSFUnit http://labs.jboss.com/jsfunit/, which is based on Apache Cactus http://jakarta.apache.org/cactus/index.html and runs tests inside a real server container.
In the RichFaces Wiki Home Pagewe have more then one unit testing framework, which uses a different approach. Though mock objects are ideal for the low-level, fine grained white box testing, they do not allow testing interaction with particular JSF implementations or third-party extensions. On the other hand, running tests in the full-featured container requires a lot of time, because you have to run a new container instance for each test, and less flexible as well. The full-featured container tests are more similar to the integration or functional testing, not to a JSF framework or component testing.
RichFaces jsf-test project uses an intermediate strategy. It runs a real JSF framework in the lightweight 'staging' web container. Advantages of this approach are:
- Testing has a minimal startup time, because no configuration files are processed and no resource pools or other heavy objects are created.
- It is extremely flexible. This server uses a 'virtual' web application content that can be built from any objects: files, java resources, even dynamic classes, which generate content 'on the fly'. Every test is able to use individual application content.
- Not only content, but any other configuration aspects, like servlets, filters, and configuration parameters can be changed from the test code. You don't have to create a lot of configuration files for each case.
- Test runs as a web server clients in the same JVM and thread as a server-side code does. It allows inspecting any objects on the both sides from the same code.
- It is possible to make test runs "inside" web request. For example, only one JSF lifecycle phase can be tested.
- At the same time, tests run in the real JSF environment. Any implementations can be tested, from JSF 1.1 to upcoming JSF 2.0.
The project source code is available from the svn repository http://anonsvn.jboss.org/repos/richfaces/trunk/framework/jsf-test/ , and nightly builds from the Jboss Maven snapshots repository http://snapshots.jboss.org/maven2/org/richfaces/framework/jsf-test/3.3.0-SNAPSHOT/. Java API documentation is also available at http://www.jboss.org/file-access/default/members/jbossrichfaces/freezone/docs/apidoc_jsf_test/index.html.
Getting started with RichFaces jsf-test
Because the entire RichFaces project uses the Maven build system, these samples based on the Maven too. But there is no limitations to use test framework with Apache Ant or any IDE.
Because the new test framework has not yet included in any stable release, it is only available as snapshots only. See MavenSettingshow to configure Maven to use the Jboss snapshots repository. Many of the JEE libraries are not published into the default maven repository, so we also have to configure java.net Maven repository https://maven-repository.dev.java.net/.
First step, create a simple Ajax repeater application
RichFaces project has the Maven archetype for a simple JSF project. To create a new project, use the next command in the directory where you want to create a project:
mvn -s ~/.m2t/settings.xml archetype:generate -DarchetypeGroupId=org.richfaces.cdk \ -DarchetypeArtifactId=maven-archetype-jsfwebapp \ -DarchetypeVersion=3.3.0-SNAPSHOT -Dversion=0.0.1-SNAPSHOT -DgroupId=foo.bar -DartifactId=test \ -DarchetypeRepository=http://snapshots.jboss.org/maven2
Note: this is a UNIX style, here the character '\' means "continue the command on the next line". In the MS Windows environment, put it all on the single line.
Second step, Configure tests.
To use RichFaces test framework, we should add it as the dependency to the project. Just open the pom.xml and find Junit dependency configuration:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>foo.bar</groupId> <artifactId>test</artifactId> <packaging>war</packaging> <name>test Maven Webapp</name> <version>0.0.1-SNAPSHOT</version> <build> <finalName>test</finalName> <plugins> <plugin> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.5</source> <target>1.5</target> </configuration> </plugin> </plugins> </build> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>3.8.1</version> <scope>test</scope> </dependency> <dependency> <groupId>org.richfaces.ui</groupId> <artifactId>richfaces-ui</artifactId> <version>3.3.0-SNAPSHOT</version> </dependency> </dependencies> </project>
Replace it with the jsf-test artifact:
............ <dependency> <groupId>org.richfaces.framework</groupId> <artifactId>jsf-test</artifactId> <version>3.3.0-SNAPSHOT</version> <scope>test</scope> </dependency> .............
Is it all ? Not yet. The jsf-test project does not depend from any JSF libraries by default. This allows using the test framework with different JSF implementations. For a first step, we append a Sun reference implementation only, as well as facelets library:
......... <dependency> <groupId>com.sun.facelets</groupId> <artifactId>jsf-facelets</artifactId> <version>1.1.14</version> </dependency> <dependency> <groupId>javax.faces</groupId> <artifactId>jsf-api</artifactId> <version>1.2_10</version> </dependency> <dependency> <groupId>javax.faces</groupId> <artifactId>jsf-impl</artifactId> <version>1.2_10</version> </dependency> ..........
Note: There is default dependencies scope for the JSF libraries, so they will be included into a final web application too. This is fine for a simple servlet container (like Tomcat or Jetty), but you have to set scope to 'provided' if you are going to deploy your application into a full JEE5 container.
To make the test project independent from the location in the file system, one more configuration step is required:
........ <plugins> ................. <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <configuration> <systemProperties> <property> <name>webapp</name> <value>${basedir}/src/main/webapp</value> </property> </systemProperties> </configuration> </plugin> </plugins> </build> ...............
This section sets the system property 'webapp' to the current location of the web application content. We are going to use this property to configure content of the "virtual" web server from the real one.
Create unit tests
Do you remember, this article title is "Test driven JSF development"? Let's suppose, for example, that we have to develop a simple JSF AJAX-enabled application. This application should display one input field and one output text. On any character inserted into the input it should display an entered string as converted to the upper case.
Before writing any application code, we are going to create tests for the application use-case. Create RepeaterTest.java in the src/test/java/foo/bar directory (see the attached file). This is a Junit4 test case inherited from the org.richfaces.test.AbstractFacesTest class. There are two methods, setupWebContent and testRepeater(). Have a look for the setupWebContent method body:
@Override protected void setupWebContent() { String webappDirectory = System.getProperty("webapp"); if (null == webappDirectory) { throw new TestException("The 'webapp' system property does not set"); } File webFile = new File(webappDirectory); facesServer.addResourcesFromDirectory("/", webFile); }
This method overrides a template method from the super class. Though the base class prepares and initializes staging web server, every test can has its own virtual web application content. Test server does not use a real directory, but a virtual one with references to the real files, java resources or even java classes that generates file content in the code. To reduce a size of the test code, we create the virtual content from the real web application. If test requires, we could replace any object in the virtual directory with something different. For example, replace production faces-config.xml with special test config from the src/test/resources directory:
............ facesServer.addResourcesFromDirectory("/", webFile); facesServer.addResource("/WEB-INF/faces-config.xml", "test-faces-config.xml"); }
Next one, testRepeater() is the application test itself:
@Test public void testRepeater() throws Exception { WebClient webClient = new LocalWebClient(facesServer);// (1) HtmlPage page = webClient.getPage("http://localhost/repeater.jsf");// (2) HtmlInput htmlInput = (HtmlInput) page.getElementById("ajaxForm:text");// (3) assertNotNull(htmlInput);// (4) htmlInput.type("foo");// (5) Element element = page.getElementById("ajaxForm:out");// (6) assertEquals("FOO", element.getTextContent().trim());// (7) }
What is this code doing? Let me describe it line by line:
- In the first line we create a special implementation of the HtmlUnit (http://htmlunit.sourceforge.net/) WebClient class. In difference with the original implementation, it is local staging server client only.
- In the second line, we call a test server to load JSF page content.
- Get reference to the input element. Note: It would be better to use a some implementation-independent lookup, as it's done by a real user: look for an input control by the type or access key. I've used known element ID just for a simplified code.
- Check that such input element exists.
- Simulate user input. Application should perform an AJAX request to the server and update the output element content.
- Lookup the output element on the page.
- Check output element content for the expected value.
It's all! No we can go to create and test application what meets given requirement.
Create JSF Repeater application.
Any JSF application requires proper configuration. There is content of the web.xml file in the src/main/webapp/WEB-INF folder that defines Faces Servlet, RichFaces AJAX filter and parameters necessary for the Facelets:
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5"> <display-name>repeater</display-name> <description>ajax simple repeater</description> <context-param> <param-name>javax.faces.STATE_SAVING_METHOD</param-name> <param-value>server</param-value> </context-param> <context-param> <param-name>javax.faces.DEFAULT_SUFFIX</param-name> <param-value>.xhtml</param-value> </context-param> <context-param> <param-name>facelets.VIEW_MAPPINGS</param-name> <param-value>*.xhtml</param-value> </context-param> <filter> <display-name>Ajax4jsf Filter</display-name> <filter-name>ajax4jsf</filter-name> <filter-class>org.ajax4jsf.Filter</filter-class> </filter> <filter-mapping> <filter-name>ajax4jsf</filter-name> <servlet-name>Faces Servlet</servlet-name> <dispatcher>REQUEST</dispatcher> <dispatcher>FORWARD</dispatcher> <dispatcher>INCLUDE</dispatcher> <dispatcher>ERROR</dispatcher> </filter-mapping> <servlet> <servlet-name>Faces Servlet</servlet-name> <servlet-class>javax.faces.webapp.FacesServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>Faces Servlet</servlet-name> <url-pattern>*.jsf</url-pattern> </servlet-mapping> </web-app>
We also have to configure FaceletsViewHandler in the faces-config.xml (that also is placed in the src/main/webapp/WEB-INF folder):
<?xml version="1.0" encoding="UTF-8"?> <faces-config version="1.2" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_1_2.xsd"> <application> <view-handler>com.sun.facelets.FaceletViewHandler</view-handler> </application> </faces-config>
Now, we create a very simple Java Bean which will play role application 'data model':
package foo.bar; import java.io.Serializable; public class Bean implements Serializable { private String text; public Bean() {} public String getText() { return text;} public void setText(String name) { this.text = name; } public String getResult() { if(null != text ){ return text.toUpperCase(); } else { return null; } } }
This bean has one read-write bean property "text", as well as one read-only propety "result", which returns upper-case
value of the "text" property.
The last file in this test project is the facelets view page "repeater.xhtml":
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:a4j="http://richfaces.org/a4j" xmlns:f="http://java.sun.com/jsf/core" xmlns:h="http://java.sun.com/jsf/html"> <head><title>Simple repeater in seam</title></head> <body> <h:form id="ajaxForm"> <h:inputText id="text" value="#{bean.text}"> <a4j:support reRender="out" event="onkeyup"></a4j:support> </h:inputText> <h:outputText id="out" value="#{bean.result}"></h:outputText> </h:form> </body> </html>
There is only one form that contains input text element for the bean "text" property and one output element for the "result". On any key pressed in the input property, <a4j:support> component sends AJAX request to the server, what updates content of the output field with new value.
P.S. Project source code is attached to the article as 'test.zip'.
Conclusion
This test framework allows using a test-driven software development for JSF-based web applications. Although staging web server is used to run tests has a very limited functionality, it runs faster than any real Java web-container and much more flexible. Of course, this framework does not replace full integration tests in the real environment, but helps to test application blocks and model more effective. In the next article I'm going to demonstrate how to extend this simple test to check our application with different frameworks and configurations. Also, we are going to create one custom component and test it from different points of view.
Comments