Mindoo Blog - Cutting edge technologies - About Java, Lotus Notes and iPhone

  • XPages series #10: Running JUnit tests on XPages code

    Karsten Lehmann  7 February 2011 09:03:00
    The 10th article of the XPages series deals with the first sample I demo'ed for the Lotusphere session BP212 - Deep Dive into IBM XPage Expression Language Syntax:
    It shows how you can develop and test most of your XPages code outside of DDE.

    So why should you development code outside of DDE?

    1. Workaround for classloader issues

    Well, you can experience a very nasty caching behaviour when you develop Java code in the Java perspective of DDE and test it live in an XPages application on the client. This might result in ClassCastExceptions like "com.company.packagename.MyClass cannot be cast to com.company.packagename.MyClass", which is caused by the JSF servlet: It still has a cached instance of the first bean in memory, created with one classloader while it tries to run the XPages application after code changes with a new classloader which is incompatible with the first one used.

    This behaviour is discussed in an article in the Lotus Notes and Domino Application Development Wiki and IBM recommends to restart the http task in this case (only a viable option if you develop on a local Domino server), because this shuts down and restarts the JVM used to load the classes. Unfortunately this is not an option for development in the Notes Client, since you really don't want to restart your Notes Client/DDE every time you change code.
    In addition to classloader issues, the JSF runtime also seems to cache Java classes and XPages intermittently, a problem that I faced just recently when I worked on the demos for BP212 and tested them in the client.

    2. Version control for source code

    Another reason why you should develop code outside of DDE is that you can use version control systems like Subversion or CVS. With 8.5.2 and an additional source control plugin from OpenNTF this aspect is less important than before, but I still prefer to have my code outside of a virtual file system that is stored in an NSF. I just don't trust that stuff and don't want to lose my code, e.g. if DDE does a rebuild on two developer machines and then they replicate with the same db instance on a server.

    3. Use code in other Non-XPages projects

    We do not only develop XPages apps, we develop libraries to be used in agents, standalone Eclipse RCP/Swing applications, server-side OSGi plugins and for many other areas. That's why we want to share most of the code between multiple development projects.

    4. Run unit tests on the code for quality checks

    To ensure that our code still produces the expected results after a code change, we use JUnit test cases on a manual or scheduled basis.

    And that is exactly what this sample is about.

    Workspace content

    The download archive for this blog article contains two directories: a sample XPages application and an exported Eclipse workspace.

    The XPages application consists of a simple XPage:

    Image:XPages series #10: Running JUnit tests on XPages code



    Here is the source code:

    <?xml version="1.0" encoding="UTF-8"?>
    <xp:view xmlns:xp="http://www.ibm.com/xsp/core">
           <xp:span style="font-size:20pt">Current User Information</xp:span>
           <xp:br></xp:br><xp:br></xp:br>
           <xp:span style="font-size:16pt">
                   Common Name:
                   <xp:br></xp:br>
                   <xp:text escape="true" id="commonName" value="#{currentUser.commonName}"
                           style="color:rgb(0,128,255)"></xp:text>
                   <xp:br></xp:br>
                   <xp:br></xp:br>
                   <xp:span style="font-size:16pt">Abbreviated Username:</xp:span>
                   <xp:br></xp:br>
                   <xp:text escape="true" id="abbrName" value="#{currentUser.abbreviatedName}"
                           style="color:rgb(0,128,255)"></xp:text>
           </xp:span>
    </xp:view>


    All it does is read the name information of the current user from a managed bean "currentUser" and display it on screen.
    The managed bean class is not directly part of the NSF, but has been added in a "lib" folder below the WebContent/WEB-INF folder as a JAR file and then added to the classpath of the NSF project in DDE (after that, it's not visible in the "lib" folder anymore, but below "Referenced Libraries").

    After you import the projects of the "workspace" directory into your personal Eclipse workspace, it should look like this:

    Image:XPages series #10: Running JUnit tests on XPages code


    Please note that you might get compile errors and need to change the path of the referenced "Notes.jar" file in the com.ls11.dominohelper project to fit your Notes configuration.

    There are four projects in the workspace:

    com.ls11.dominohelper
    • The project contains helper classes to execute a piece of code in the context of a Notes session (see below for details)
    com.ls11.externalcodesample
    • This project contains the bean implementation to read the current username
    com.ls11.externalcodesample.build
    • We use the ANT script of this project to create the JAR file with the necessary content to work in an XPages app
    com.ls11.externalcodesample.test
    • Finally this project contains a JUnit test case to check that our bean is working correctly

    Running the same code standalone and from an XPages request

    Let's dive into the class CurrentUserInfo.java. The method .getCommonName() looks like this:

    /**
    * Returns the common name of the current user
    *
    * @return common name
    */
    public String getCommonName() {
           if (m_commonName==null) {
                   IDominoCallable<String> callable=new IDominoCallable<String>() {

                           @Override
                           public String call(Session session) throws NotesException {
                                   Name currName=session.createName(session.getUserName());
                                   String commonName=currName.getCommon();
                                   currName.recycle();
                                           
                                   return commonName;
                           }
                   };
                           
                   try {
                           m_commonName=DominoExecution.run(callable).get();
                   } catch (InterruptedException e) {
                           throw new RuntimeException("Could not read username info", e);
                   } catch (ExecutionException e) {
                           throw new RuntimeException("Could not read username info", e);
                   }        
           }
           return m_commonName;
    }


    Now that looks interesting: We are using an IDominoCallable implementation to retrieve the common name part of the username.
    We do this in order to run the same code from an XPages environment and from a standalone application. In the DominoExecution there's some magic that detects whether it gets executed from XPages code or not.

    In standalone mode, DominoExecution currently launches a new NotesThread for every call, but that could easily be changed to have a permanent thread and just feed it with the IDominoCallable's one after another.

    The run method of DominoExecution returns a Future object of Java's concurrency framework. Calling its .get() method let's the executing code wait for the result to be computed.
    In XPages mode, the IDominoCallable is executed synchronously, because the code is already running in a Domino enabled thread.

    Running JUnit in Eclipse

    The project com.ls11.externalcodesample.test contains a JUnit test case that checks whether the methods of the bean are working correctly: For example it ensures that abbreviated name and common name are both not null and compares the first part of the abbreviated name with the common name.

    /**
    * Check that common and abbreviated name
    */
    public void testCommonNameAndAbbrNameConsistent() {
           String commonName=m_currUserInfo.getCommonName();
           String abbrName=m_currUserInfo.getAbbreviatedName();
           
           //the values cannot be null
           assertNotNull(commonName);
           assertNotNull(abbrName);
                   
           int iPos=abbrName.indexOf("/");
           assertTrue((iPos ! = - 1) && (iPos ! = 0) );
                   
           String firstAbbrNamePart=abbrName.substring(0, iPos);
           assertEquals(commonName, firstAbbrNamePart);
    }


    You can launch the test case by first creating a JUnit run configuration:

    Image:XPages series #10: Running JUnit tests on XPages code


    On the Environment tab, please add a variable "PATH" that contains the path to your Lotus Notes program directory:

    Image:XPages series #10: Running JUnit tests on XPages code


    Now you can execute the test case and get the following result:

    Image:XPages series #10: Running JUnit tests on XPages code


    The method testCommonNameAndAbbrNameConsistent could be executed without errors, but calling testOrganisation led to an error.
    It checks that CurrentUserInfoTest.getOrganisation() is not null, but unfortunately, it seems like we forgot the method's implementation:

    public String getOrganisation() {

           // TODO Auto-generated method stub

           return null;

    }


    After fixing that, the JUnit succeeds:

    Image:XPages series #10: Running JUnit tests on XPages code



    Now that we know that the bean works, we can package everything by right clicking on the ANT script of project com.ls11.externalcodesample.build, choose "Run As/Ant Build", then do a refresh on the "lib" folder and replace the JAR in the XPages application database with the new one.

    That's it!

    Here is the download link for the archive:
    LS11-BP212-JUnit.zip
    Comments

    1Nick Goddard  16.02.2011 12:18:59  XPages series #10: Running JUnit tests on XPages code

    The link to the samples doesn't seem to be working. Thanks for the article though it is very interesting :)

    2Karsten Lehmann  16.02.2011 12:32:37  XPages series #10: Running JUnit tests on XPages code

    Weird... I couldn't find the attachment anymore in the database. Uploaded it a second time, should work now.

    3Nick Goddard  16.02.2011 13:07:41  XPages series #10: Running JUnit tests on XPages code

    Thanks :)

    4Andrew Magerman  21.04.2011 11:12:47  XPages series #10: Running JUnit tests on XPages code

    There is another reason for out of DDE code development: Java debugging.

    5Howard Pflugh  24.01.2013 19:09:25  XPages series #10: Running JUnit tests on XPages code

    Thanks for taking the time to document this. It was a great article to get me to dig into JUNIT and ANT. However, when I implemented the bean in my xpage, it produced only the server's name for both common and abbreviated. Any ideas on what I could be doing wrong?

    6Howard Pflugh  24.01.2013 21:19:47  XPages series #10: Running JUnit tests on XPages code

    I should note that this works correctly when previewed in the Notes Client. But even after changing the lines that reference .getUsername to getEffectiveUsername it still fails. I am hoping it is somekind of caching problem on the server.

    7Karsten Lehmann  24.01.2013 23:33:38  XPages series #10: Running JUnit tests on XPages code

    @Howard:

    Did you login with your username/password on the Domino server before calling the XPage?

    I would expect that session.getEffectiveUserName() returns the server name instead of the user name, if the XPage is called with anonymous access.