Sonntag, 5. Juni 2011

Unit-Testing EJB 3.1 with Netbeans, Maven and embedded Glassfish

When trying out unit-testing support in Netbeans 7.0 with the EJB 3.1 embedded container from glassfish 3.1 I ran across a few problems for which I want to share my solutions. I'm working through chapter 6 of "Beginning Java EE 6 with GlassFish 3" at the moment. It's a good book but the code examples are not quite complete in some cases.

I'm in an Maven EJB Module-Project in Netbeans using:
  • Netbeans 7.0
  • GlassFish Server Open Source Edition 3.1 (43)
  • maven 3.0.3 (bundled)

The module is just a scaffold consisting of:
  • A JPA entity bean (provider: Eclipselink 2.0.2, jdbc: Derby 10.6.2.1 client)
  • An EJB implementing CRUD on this bean
  • A remote interface for the EJB
  • A web service

My goal is to test the EJB with the EJB 3.1 embedded container

EJBContainer.createEJBContainer()


First of all, when you generate an unit test for the EJB in Netbeans and use the provided quick fix for missing EJBContainer, it modifies the pom.xml to use the so-called Glassfish static shell for the embedded container.

<dependency>
  <groupId>org.glassfish.extras</groupId>
  <artifactId>glassfish-embedded-static-shell</artifactId>
  <version>3.1</version>
  <scope>system</scope>
  <systemPath>${glassfish.embedded-static-shell.jar}</systemPath>
</dependency>


It then sets the property ${glassfish.embedded-static-shell.jar} to a Glassfish installation of your choice, for example the bundled one. The jar file contains nothing but classpath entries for the jars needed for Glassfish embedded. This is suboptimal, as the build is now dependent on a local installation in the right version, and is not portable due to the hard-code path in the property. When using Hudson or similar CI, having to have a Glassfish installation on your build server is sometimes not preferable.

Alternatively, you can specify to download an embedded glassfish installation with the following pom.xml snippet:

<dependency>
  <groupId>org.glassfish.extras</groupId>
  <artifactId>glassfish-embedded-all</artifactId>
  <version>3.1</version>
  <scope>test</scope>
</dependency>


Warning: This is a big download, around 70 MB.

It works out of the box for simple EJB, but I still had a few issues in my case:
  1. As soon as you put a remote interface in your bean, the embedded glassfish container starts a CORBA IIOP listener on default port 3700, resulting in a BindException: Address already in use if there is another instance of glassfish running
  2. As soon as you put a JAX-WS web service in your bean, the embedded container throws a java.lang.NullPointerException at java.util.PropertyResourceBundle.handleGetObject(PropertyResourceBundle.java:136)
  3. The embedded container can't find your JTA jdbc resources referenced in persistence.xml. I also wanted to use a different persistence.xml using an embedded Derby database and using drop-and-create for eclipselink.ddl-generation
To solve issue 1, I needed to use a different domain.xml than the default one for the embedded container to change the port numbers. Pointing the embedded container to a different domain.xml by putting the property org.glassfish.ejb.embedded.glassfish.configuration.file in a map and passing it to EJBContainer.createEJBContainer() as indicated by the Oracle GlassFish Server 3.1 Embedded Server Guide didn't work for me. Instead i had to use the org.glassfish.ejb.embedded.glassfish.instance.root, and point it to a glassfish domain directory. I have created it under src/test/resources/testing-domain. The following files are needed:
  • config/admin-keyfile
  • config/cacerts.jks
  • config/domain.xml
  • config/keyfile
  • config/keystore.jks
  • config/login.conf
  • config/server.policy
I pulled the files from the glassfish-embedded-all-3.1.jar and changed to domain.xml to use different port. I also activated the http and https listeners (putting them on ports 28080 and 28181, respectiveley).

To solve issue 2, I had to put the property org.glassfish.ejb.embedded.glassfish.web.http.port when calling EJBContainer.createEJBContainer(props);
The name of the property is a bit misleading, as the value is ignored completely and can be an empty String. It just means that the embedded glassfish instance should start the http listeners (just putting them as enabled="true" in domain.xml wasn't enough).

To solve issue 3, I created an additional persistence.xml and put it to src/test/resources/META-INF:

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
  <persistence-unit name="bookstore-ejb" transaction-type="JTA">
    <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
    <exclude-unlisted-classes>false</exclude-unlisted-classes>
    <properties>
      <property
        name="eclipselink.target-database"
        value="DERBY" />
      <property
        name="eclipselink.ddl-generation"
        value="drop-and-create-tables" />
      <property
        name="eclipselink.logging.level"
        value="FINE" />
      <property
        name="javax.persistence.jdbc.driver"
        value="org.apache.derby.jdbc.EmbeddedDriver" />
      <property
        name="javax.persistence.jdbc.url"
        value="jdbc:derby:memory:bookstore-ejb;create=true;" />
      <property
        name="javax.persistence.jdbc.user"
        value="APP" />
      <property
        name="javax.persistence.jdbc.password"
        value="APP" />
    </properties>
  </persistence-unit>
</persistence>


Next, I copy target/classes to target/embedded-classes and afterwards copy target/test-classes to target/embedded-classes to overwrite my production META-INF/persistence.xml with the maven-resources-plugin:

<plugin>
    <artifactId>maven-resources-plugin</artifactId>
    <version>2.5</version>
    <executions>
        <execution>
            <id>copy-classes-to-embedded</id>
            <phase>compile</phase>
            <goals>
                <goal>copy-resources</goal>
            </goals>
            <configuration>
                <outputDirectory>target/embedded-classes</outputDirectory>
                <overwrite>true</overwrite>
                <resources>
                    <resource>
                        <directory>target/classes</directory>
                    </resource>
                    <resource>
                        <directory>src/test/resources</directory>
                    </resource>
                </resources>
            </configuration>
        </execution>

    </executions>
</plugin>

</plugins>


Then, I point the EJBContainer.MODULES property to target/embedded-classes.

We can now write our unit-test like this:

public class BookEJBTest {

    private static EJBContainer ec=null;
    private static Context ctx=null;

    public BookEJBTest() {
    }

    @BeforeClass
    public static void initContainer() throws Exception {
        Map<String, Object> props=new HashMap<String, Object>();
        props.put(EJBContainer.MODULES, new File("target/embedded-classes"));
        props.put("org.glassfish.ejb.embedded.glassfish.instance.root","./src/test/testing-domain");
        props.put("org.glassfish.ejb.embedded.glassfish.web.http.port","");
        ec = EJBContainer.createEJBContainer(props);
        ctx = ec.getContext();
    }


    @AfterClass
    public static void closeContainer() throws Exception {
        if(ctx!=null)
            ctx.close();
        if(ec!=null)
            ec.close();
    }

    @Test
    public void shouldCreateABook() throws Exception {

        Book book = new Book();
        book.setTitle("The Hitchhiker's Guide to the Galaxy");
        book.setPrice(12.5F);
        book.setDescription("Scifi book created by Douglas Adams");
        book.setIsbn("1-84023-742-2");
        book.setNbOfPages(354);
        book.setIllustrations(false);

        BookEJB bookEJB = (BookEJB) ctx.lookup("java:global/embedded-classes/BookEJB!de.familienservice.bookstoreejb.BookEJB");

        book = bookEJB.createBook(book);
        assertNotNull("ID should not be null", book.getId());
    }

}


The test runs with mvn test as well as with the embedded test runner in Netbeans 7.0. The build is portable und should run in my Hudson now.
Note that I didn't create a test for the webservice yet. The URL the webservice binds to is always http://localhost:28080/... though, so this should be an easy task.

FYR, I have put up the code for this project on github.

If you have any questions or tips for me, feel free to comment :-)

Edit: You have to have the glassfish maven repository in addition to the eclipselink repository in your pom.xml
   <repositories>  
     <repository>  
       <url>http://ftp.ing.umu.se/mirror/eclipse/rt/eclipselink/maven.repo</url>  
       <id>eclipselink</id>  
       <layout>default</layout>  
       <name>Repository for library Library[eclipselink]</name>  
     </repository>  
     <repository>  
       <id>glassfish-repo</id>  
       <url>http://download.java.net/maven/glassfish/</url>  
       <name>Repository for glassfish artifacts</name>  
     </repository>  
   </repositories>  

Kommentare:

  1. I'm having this problem when i try to build the project downloaded from the page you supplied. Would you help me please?

    Failed to execute goal on project bookstore-ejb: Could not resolve dependencies for project de.familienservice:bookstore-ejb:ejb:1.0-SNAPSHOT: The following artifacts could not be resolved: org.glassfish.extras:glassfish-embedded-all:jar:3.1, org.glassfish:javax.ejb:jar:3.1: Could not find artifact org.glassfish.extras:glassfish-embedded-all:jar:3.1 in eclipselink (http://ftp.ing.umu.se/mirror/eclipse/rt/eclipselink/maven.repo) -> [Help 1]

    To see the full stack trace of the errors, re-run Maven with the -e switch.
    Re-run Maven using the -X switch to enable full debug logging.

    For more information about the errors and possible solutions, please read the following articles:
    [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/DependencyResolutionException

    AntwortenLöschen
  2. Thanks for the hint! I had some extra repositories in my ~/.m2/settings.xml. You need to enable the glassfish maven repository in your pom in adition to the eclipselink repo. I have edited the blog posting.
    I also have updated the github project accordingly.

    AntwortenLöschen
  3. Hi

    really nice post. I am out of my desk now so i had no chance to check out your code but can you provide me any reference that clearly states that when starting EjbContainer (by EJBContainer.createEJBContainer) HTTP listener handling SOAP requests is started on port 28080.

    AntwortenLöschen
  4. According to embedded glassfishes log the listener is started:
    INFO: Grizzly Framework 1.9.31 started in: 148ms - bound to [0.0.0.0:28080]
    and my test web service is also started:
    INFO: WS00019: EJB Endpoint deployed
    embedded-classes listening at address at http://Euklid:28080/NewWebService/NewWebService
    I didn't test with actual HTTP request though, not doing anything currently with EJBs :-)

    AntwortenLöschen
  5. Hi folks,
    this example I find up to now the only one that really works with embedded Glassfish ver. 3.
    DeepZajac

    AntwortenLöschen
  6. Hi Paul,

    Thanks for the example and the sources on github.
    I was extending the example with CDI, but when adding the beans.xml, I got an "java.lang.OutOfMemoryError: PermGen space".

    Any idea's to get CDI working?
    ps, I upgraded to 3.1.1.
    Thanks,
    Duncan

    AntwortenLöschen
  7. Hi Paul,
    thanks for your nice post. Unfortunately I still face with the problem point 2#. Setting property to empty string I get: "javax.ejb.EJBException: org.glassfish.embeddable.GlassFishException: PlainTextActionReporterFAILURENo configuration found for server.network-config.network-listeners.network-listener.http-listener". Setting it to "8080" outputs "using 8080" with same exception above.
    I use glassfish-embedded-all-3.1.1-b11.jar.
    Any idea?
    Thanks
    Vins

    AntwortenLöschen
  8. @Duncan: Try to start maven with increased permanent geneneration size.
    If you run from command line, append -XX:MaxPermSize=256m like in
    mvn -XX:MaxPermSize=256m test
    If you run from eclipse, I think the best way is to set MAVEN_OPTS env variable.
    In OSX, for example, run
    export MAVEN_OPTS="-XX:MaxPermSize=256m" && open /Applications/eclipse/Eclipse.app

    For windows, there is somewhere a setting for environment var, don't ask me where :-)

    AntwortenLöschen
  9. Sorry, just realized you can't add JVM options to mvn like this. Try
    export MAVEN_OPTS="-XX:MaxPermSize=256m" && mvn test.

    AntwortenLöschen
  10. Paul,

    really appreciate your article,

    It's a nb bug that the editor doesnt recognize ${glassfish.embedded-static-shell.jar} if you set it up in MAVEN_OPTS. The editor and nb project complains with 'missconfigured project' but if you run the tests from nb or the command line it does pick up the gf location from the MAVEN_OPTS propertye, this would make the first approach installation independent.

    I've opened a NB bug for this:

    http://netbeans.org/bugzilla/show_bug.cgi?id=202202

    I am going to try the embedded-all now.

    AntwortenLöschen
  11. I think this doesn't work in 3.1.1?

    AntwortenLöschen
  12. >> I think this doesn't work in 3.1.1?
    Was having issues with ddl for entities not being generated with 3.1.1, sequence table was created but nothing else.

    Great post btw.

    AntwortenLöschen
  13. Thanks, was toooo helpful.

    If this works for someone:

    I was having the same error about the iiop issue over and over again when I was trying the 3701 port. Then I changed it to 23700 and it worked just fine.

    AntwortenLöschen
  14. Just FYI, I'm using the embedded container glassfish-embedded-all 3.1.1 and the value for the property org.glassfish.ejb.embedded.glassfish.web.http.port can be changed there now. I had it changed in the domain.xml but in the logs, it would always say "Using 8080" until I added the port number to the map.

    AntwortenLöschen
  15. Is there any way to run the tests as an authenticated user? Is it possible to use ProgrammaticLogin? A lot of my code depends on the session user.

    AntwortenLöschen