Portlet Unit Testing with Liferay 6

>> Tuesday, June 8, 2010

My Test Obsession
I must admit, I am a bit obsessed with automated tests. Whenever I approach the development of a new project with a new system, one of my main concerns is how can we implement and run unit tests? So when I tackled the development of a Liferay portlet I assumed that a unit testing framework or practice was a given, Surely someone (if not the Liferay folks) had mastered and documented this aspect of portlet development. Well, Googling and searching for this revealed that no one seemed to have documented a simple and elegant solution to this need. It is easy to test the base Liferay portlets that can be found in the Liferay SDK and source. However, running Unit tests for a portlet in a separate Eclipse project poses a few challenges as mentioned in the following Liferay forum article: http://www.liferay.com/community/forums/-/message_boards/message/3827878.

The solution proposed in that article did not satisfy me and appeared too complex. There had to be a better and simpler way. After some tinkering and reverse engineering using the Eclipse debugger and changing the log4j settings, I can now run a suite of unit tests for my Liferay service including persistence. The following describes how to achieve this and the general approach. You will probably need to adapt the example to your own situation/portlet but the general approach should be applicable. I am still somewhat green when it comes to Liferay and my understanding of the inner workings of the solution is far from complete so there may be better ways of doing this. Please let me know if you find something missing or incorrect so I can improve this documentation.

The main difficulty with portlet unit testing, from what I observed, is due to the fact that portlets are meant to run inside (or alongside) a portal which provides a number of resources and contexts that are not de facto present when running unit tests. Fortunately, Liferay is based on Spring and the Liferay folks did a good job of making it easy for us to extend or override the context in which code is executed.

In the following example, we have a relatively simple portlet in which Java code was essentially generated with the "build-service" Ant script based on a WEB-INF/service.xml file. What we basically need to do to run the unit tests is create a new source folder for the project that will contain the test resources. This folder contains resources such as Java classes, property and Spring context files that are used exclusively for unit testing and must not be included in the WAR bundle to be deployed in the portal container.

My JUnit test class extends com.liferay.portal.service.persistence.BasePersistenceTestCase and implements the following methods:

public abstract void testCreate() throws Exception;
public abstract void testRemove() throws Exception;
public abstract void testUpdateNew() throws Exception;
public abstract void testUpdateExisting() throws Exception;
public abstract void testFindByPrimaryKeyExisting() throws Exception;
public abstract void testFindByPrimaryKeyMissing() throws Exception;
public abstract void testFetchByPrimaryKeyExisting() throws Exception;
public abstract void testFetchByPrimaryKeyMissing() throws Exception;
public abstract void testDynamicQueryByPrimaryKeyExisting()
      throws Exception;
public abstract void testDynamicQueryByPrimaryKeyMissing() throws Exception;

I am a lazy type so I simply copied an existing subclass of BasePersistenceTestCase and changed the implementation so it refers to my own services.

In the root of the source test folder, put a file called portal-test.properties containing the following (substitute $TOKEN$ to reflect your context):

jdbc.default.driverClassName=$DRIVER_CLASS_NAME$
jdbc.default.url=$DB_URL$
jdbc.default.username=$USER_NAME$
jdbc.default.password=$USER_PASSWORD$

# Disable the scheduler for Unit testing
ehcache.portal.cache.manager.jmx.enabled=false

value.object.listener.com.liferay.portal.model.LayoutSet=

resource.repositories.root=$RESOURCE_REPOSITORIES_ROOT$

# Disable the scheduler for Unit testing
scheduler.enabled=false

hibernate.configs=\
META-INF/mail-hbm.xml,\
META-INF/portal-hbm.xml,\
META-INF/ext-hbm.xml,\
META-INF/portlet-hbm.xml

One of the basic principles in Liferay is that you can override base properties and Spring configs with your own. In the above file, we define properties that override the base ones that come from the portal project.

In the same fashion, I created a "ext-spring.xml" file in the META-INF subfolder of the source test folder that overrides the bean definitions found in the portlet or portal spring contexts. As an example, the content of the "ext-spring.xml" file I use is the following (substitute type names to reflect your service/portlet):

<beans default-destroy-method="destroy" default-init-method="afterPropertiesSet"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.springframework.org/schema/beans"
 xsi:schemalocation="http://www.springframework.org/schema/beans 
        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

 <bean
  class="org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy"
  id="liferayDataSource">
  <property name="targetDataSource">
   <bean class="com.liferay.portal.dao.jdbc.util.DataSourceFactoryBean">
    <property name="propertyPrefix" value="jdbc.default.">
    </property>
   </bean>
  </property>
 </bean>

 <bean class="com.opnworks.portlet.iamhere.service.impl.MyLocalServiceImpl"
  id="com.opnworks.portlet.service.MyLocalService">
  <bean class="com.opnworks.portlet.service.MyLocalServiceUtil" id="com.opnworks.portlet.service.MyLocalServiceUtil">
   <property name="service"
    ref="com.opnworks.portlet.service.GeoCampaignLocalService">
   </property>
  </bean>
  <bean
   class="com.opnworks.portlet.iamhere.service.persistence.MyEntityPersistenceImpl"
   id="com.opnworks.portlet.service.persistence.MyEntityPersistence"
   parent="basePersistence">

   <bean
    class="com.liferay.portal.spring.transaction.TransactionManagerFactory"
    factory-method="createTransactionManager" id="liferayTransactionManager">
    <constructor-arg ref="liferayDataSource">
     <constructor-arg ref="liferayHibernateSessionFactory">
     </constructor-arg>
    </constructor-arg>
   </bean>
   <bean
    class="com.opnworks.portlet.iamhere.service.persistence.PortletHibernateTestConfiguration"
    id="liferayHibernateSessionFactory">
    <property name="dataSource" ref="liferayDataSource">
    </property>
   </bean>
   <bean class="com.liferay.portal.kernel.dao.orm.DynamicQueryFactoryUtil"
    id="com.liferay.portal.kernel.dao.orm.DynamicQueryFactoryUtil">
    <property name="dynamicQueryFactory">
     <bean
      class="com.liferay.portal.dao.orm.hibernate.DynamicQueryFactoryImpl">
     </bean>
    </property>
   </bean>
   <bean class="com.liferay.portal.kernel.dao.orm.RestrictionsFactoryUtil"
    id="com.liferay.portal.kernel.dao.orm.RestrictionsFactoryUtil">
    <property name="restrictionsFactory">
     <bean
      class="com.liferay.portal.dao.orm.hibernate.RestrictionsFactoryImpl">
     </bean>
    </property>
   </bean>
   <bean class="com.liferay.portal.kernel.util.InfrastructureUtil"
    id="com.liferay.portal.kernel.util.InfrastructureUtil">
    <property name="dataSource" ref="liferayDataSource">
     <property name="transactionManager" ref="liferayTransactionManager">
     </property>
    </property>
   </bean>
  </bean>
 </bean>
</beans> 
                                                     

In the above file, the main trick that I pulled is to redefine the bean called liferayHibernateSessionFactory and to specify the following class implementation:

import com.liferay.portal.spring.hibernate.PortletHibernateConfiguration;

public class PortletHibernateTestConfiguration extends
  PortletHibernateConfiguration {
 protected ClassLoader getConfigurationClassLoader() {
  return this.getClass().getClassLoader();
 }
}

This was needed because the PortletHibernateConfiguration implementation throws a NPE on the getConfigurationClassLoader() method invocation due to the fact that we are not running inside a container. I also put the above class definition in my test source folder. If anybody can suggest a more elegant solution, I would be delighted to hear about it.

Another useful trick is to put a copy of portal-log4j.xml in the META-INF subfolder of your test source folder. That way, you can change the logging strategy used for unit testing.

If using Eclipse, you can simply run your unit test classes or suite as a JUnit configuration.

Have fun in the Liferay lane...

20 commentaires:

Anonymous,  10 August, 2010 09:59  

Hello,
I can't find the resources needed to do the tests.

I follow your link
http://tencompetence.cvs.sourceforge.net/viewvc/tencompetence/portal
but I can't find the class:
com.liferay.portal.service.persistence.BasePersistenceTestCase
Could you help me to find all resources needed?
thank you.

Anonymous,  18 October, 2010 05:04  

Thanks for this, it's been very helpful.

Jeff - jeffpower78@gmail.com,  23 October, 2010 19:28  

Thanks for this wonderful approach..

Liked the idea : redefining the liferayHibernateSessionFactory bean

Wanna to give a try. Appreciate if you can share code base for test folder for quick reference.

it's great to see more liferay lane

Jakub,  08 December, 2010 09:04  

Hey, thanks for sharing this way of unit testing. How did you build the liferay test code such as BasePersistenceTestCase etc. it is not built into portal-impl.jar and there is no ant target except for compile-test which only compiles the test sources and test, which runs junit

Jakub,  08 December, 2010 15:52  

It all works fine except for one thing, the spring context cannot be initialized due to

java.lang.NoClassDefFoundError: Could not initialize class com.liferay.portal.util.PropsUtil

I have portal-impl.jar on classpath for testing, don't understand what can be possibly wrong

Jakub,  08 December, 2010 18:18  

You didn't define basePersistence parent bean, liferaySessionFactory is not defined...not sure how to do that properly...

Anonymous,  28 December, 2010 15:05  

It would be really great if you could provide a sample (maybe eclipse project) because i cannot clearly follow your tutorial...

thanks c.

Rose 03 January, 2011 01:07  

I am looking for such type of informative news and i get through this blog so i am very much thankful to you for sharing such a great information.
- liferay customization

Laurent Gauthier 25 January, 2011 21:39  

I had the chance to work on unit testing of Liferay portlets again Today. We are currently working on a fully Mavenized project with Hudson-based continuous integration and we need to fully automate builds including unit testing. Following my own guidelines worked pretty well. One thing I had to do is generate a Maven artifact (jar) for the portal-impl test classes. I actually wrote a POM to do that and now I can add a test dependency in my Maven projects to this artifact. I could post this POM if anyone is interested.

The other thing we wanted is the ability to use a in-memory, HSQLDB database for unit testing. That turned out relatively easy but we had to write special setup code in our TestCase class to create the database objects before running the unit tests. Here is what we did in a nutshell:

## portal-test.properties ##

# HSQLDB-MEM
jdbc.default.driverClassName=org.hsqldb.jdbcDriver
jdbc.default.url=jdbc:hsqldb:mem:lportal
jdbc.default.username=sa
jdbc.default.password=

// Code in TestCase
private static boolean initialized = false;

public void setUp() throws Exception {
super.setUp();

if (!initialized ) {
ClassLoader classLoader = Struts2TestImplTest.class.getClassLoader();
final String path = "src/main/webapp/WEB-INF/sql/";
String tablesSQL = loadSqlScript(path, "tables.sql");
String sequencesSQL = loadSqlScript(path, "sequences.sql");
String indexesSQL = loadSqlScript(path, "indexes.sql");
ServiceComponentLocalServiceUtil.upgradeDB(classLoader, "transco", 1l,
true, null, tablesSQL, sequencesSQL, indexesSQL);
initialized = true;
}
}

protected static String loadSqlScript(String path,
String scriptName) throws IOException {

StringBuffer buf = new StringBuffer();
File file = new File(path + scriptName);
InputStream is = new FileInputStream(file);
String line = null;
if (is != null) {
try {
BufferedReader bufferedReader =
new BufferedReader(
new InputStreamReader(is));
while ((line = bufferedReader.readLine()) !=
null) {
buf
.append(line)
.append("\n");
}
} finally {
is.close();
}
return buf.toString();
}

slayercon66 05 February, 2011 08:34  

Thanks for your great work ... It would be even better if you could provide us some Sample Source Code of your implementation...

regards c

useche 26 February, 2011 16:18  

Thanks Laurent, but I could not find the class BasePersistenceTestCase in any jar of liferay 6, this info is for Liferay 5.2?

Andrius 12 April, 2011 02:52  

Nice.
Could you please post this pom here?
Also, loading SQL files from src path does not seem right. It'a pity they are not in classpath...

Laurent Gauthier 13 April, 2011 09:58  

Here is the POM file to deploy the Liferay util-test.jar to a repository.

<?xml version="1.0" encoding="UTF-8"?>

<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>com.liferay.portal</groupId>
<artifactId>util-test</artifactId>
<name>Liferay Portal Test Utils</name>
<version>6.0.5</version>
<description>Contains implementation for the portal test utils.</description>
<url>http://www.liferay.com</url>
<licenses>
<license>
<name>GNU Lesser General Public License</name>
<url>http://www.gnu.org/licenses/lgpl-2.1.html</url>
<distribution>repo</distribution>
</license>
</licenses>
<properties>
<spring.version>3.0.3.RELEASE</spring.version>
</properties>

<distributionManagement>
<repository>
<id>releases</id>
<name>Releases</name>
<url>http://path.to.repository....</url>
</repository>
</distributionManagement>

<dependencies>
<dependency>
<groupId>com.liferay.portal</groupId>
<artifactId>portal-impl</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.liferay.portal</groupId>
<artifactId>portal-client</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.liferay.portal</groupId>
<artifactId>portal-service</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.liferay.portal</groupId>
<artifactId>util-bridges</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.liferay.portal</groupId>
<artifactId>util-java</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.4</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.5</version>
</dependency>
<dependency>
<groupId>org.apache.abdera</groupId>
<artifactId>abdera-core</artifactId>
<version>1.1</version>
</dependency>
<dependency>
<groupId>axis</groupId>
<artifactId>axis</artifactId>
<version>1.4</version>
</dependency>
<dependency>

...etc...

Peter Ma 01 June, 2011 05:44  

I think in PortletHibernateTestConfiguration, you miss sth. Here is the missing code:
protected String[] getConfigurationResources() {
String[] configs = PropsUtil.getArray(PropsKeys.HIBERNATE_CONFIGS);
for(String config : configs){
System.out.println(config);
}
return configs;
}

Laurent Gauthier 01 June, 2011 08:26  

I have not had the chance to test Peter's proposed addition (override getConfigurationResources() method) but if anybody can comment on it's impact/utility. Peter maybe?

Anonymous,  14 July, 2011 06:19  

Hi All,

I had try and it is OK but just work only entity reference package exist from liferay.
ex: my service.xml

1. Work well




2. No Work







anyone idea?

Fournel 16 November, 2011 12:10  

Hi Laurent,

Thank you very much for your unit test setup.
But did you get interested to setup a mockup object instead of real Spring context instances ?
It is better ?

look at : http://blog.ippon.fr/2011/04/26/tests-unitaires-dans-liferay/

Anonymous,  18 November, 2011 10:08  

I tried this tutorial, but don't manage to ru nmy tests because of a "perm gem space error" :

I tested with value like 256/384 and 512...but never work. Why this error ?

[DEBUG] Adding to surefire booter test classpath: C:\Users\Francois\.m2\repository\org\apache\maven\surefire\surefire-api\2.7.1\surefire-api-2.7.1.jar Scope: compile
Forking command line: cmd.exe /X /C ""C:\Program Files\Java\jdk1.6.0_29\jre\bin\java" -jar D:\helios\workspace-liferay606\first.maven.portlet\target\surefire\surefirebooter715269634778104074.jar D:\helios\workspace-liferay606\first.maven.portlet\target\surefire\surefire3487838244268613545tmp D:\helios\workspace-liferay606\first.maven.portlet\target\surefire\surefire823374070065890259tmp"

-------------------------------------------------------
T E S T S
-------------------------------------------------------
Running com.commsen.liferay.examples.portlet.servicebuilder.service.base.PlayerLocalServiceImplTest
Loading jar:file:/C:/Users/Francois/.m2/repository/liferay-portal-community-606/portal-impl/6.0.6/portal-impl-6.0.6.jar!/system.properties
Loading jar:file:/C:/Users/Francois/.m2/repository/liferay-portal-community-606/portal-impl/6.0.6/portal-impl-6.0.6.jar!/portal.properties
Loading file:/D:/helios/workspace-liferay606/first.maven.portlet/target/test-classes/portal-test.properties
META-INF/mail-hbm.xml
META-INF/portal-hbm.xml
META-INF/ext-hbm.xml
META-INF/portlet-hbm.xml
15:03:25,761 INFO [DialectDetector:69] Determining dialect for MySQL 5
15:03:25,867 INFO [DialectDetector:49] Using dialect org.hibernate.dialect.MySQLDialect
15:03:25,875 INFO [TestPortletHibernateConfiguration:777] Building new Hibernate SessionFactory
15:03:29,250 INFO [PortalImpl:278] Global lib directory /C:/Users/Francois/.m2/repository/liferay-portal-community-606/portal-service/6.0.6/
15:03:29,252 INFO [PortalImpl:298] Portal lib directory /C:/Users/Francois/.m2/repository/liferay-portal-community-606/util-java/6.0.6/util-java-6.0.6.jar!/
java.lang.OutOfMemoryError: PermGen space
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------

Anonymous,  18 November, 2011 10:18  

Hey !

I needed to configure permsize at surefire plugin level , like explain here : http://www.gitshah.com/2010/08/how-to-fix-outofmemoryerror-permgen.html

Thx guys !

podnov 31 December, 2013 12:22  

We've been using this method to for our test for quite some time. We're now trying to upgrade from Liferay 6.0.6 to 6.1.2 and are having trouble with the new way the liferay tests are designed.

Do you have any insight into migrating tests to newer versions of liferay?

Thanks!

Post a Comment

  © Blogger template Webnolia by Ourblogtemplates.com 2009

Back to TOP