Thursday, August 13, 2015

MXJUnit: Testing Crons

MXJUnit provides the ability to unit test crons.  This means that, within a JUnit test case, it is possible to execute a cron and test what it does.  MXJunit goes one step further and allows you to execute the cron with a subclass of what's defined in Maximo.  This allows a change to change the behaviour of the cron for the test.  Normally, I use this to provide a different SQL where clause so the test operates on a smaller and/or known subset of the data.

Most MXJUnit tests will extend from MaximoTestHarness.  A test that has to execute a cron task must extend from CronTaskTestHarness.   This provides methods to execute a cron, but also performs some additional work behind the scenes that isn't normally required.

This example executes the KPIHISTORY cron which goes through the KPIHISTORY table and deletes records more than a year old.  These examples should be executable in any Maximo environment.


 public class CronTaskDemo1Test extends CronTaskTestHarness {  
      /**  
       * Test that KPIHISTORY cron will delete old KPIHISOTRY records.  
       *   
       * @throws RemoteException if an RMI problem occurs.  
       * @throws MXException if a Maximo problem occurs.  
       */  
      @Test  
      public void testKPIHistory() throws RemoteException, MXException {  
           // create a new KPI History record for testing.  
           MboRemote kpiHistory = createKpiHistory();  

           // load it and verify that it exists and is properly populated  
           kpiHistory = refresh(kpiHistory);  
           assertNotNull(kpiHistory);  
           assertEquals(pastDate(), kpiHistory.getDate("RECORDEDON"));  

           // run the cron  
           runCron("KPIHISTORY", "KPIHISTORY1YEAR");  

           // attempt to reload the KPIHISTORY and note that it is gone.   
           kpiHistory = refresh(kpiHistory);  
           assertNull(kpiHistory);  
      }

     // ... snip ...  
 }  

The full source is available in CronTaskDemo1Test.java

The runCron() method executes the cron.  It must refer to an existing definition in the Maximo cron task tables (CRONTASKDEF, CRONTASKINSTANCE).  It will execute the cron with the parameters defined for the named cron task instance.  The cron does not need to be active in Maximo for the cron to execute it.

That's all there is to running a cron in an MXJUnit test.  After the test, make sure to test that the cron performed the intended work.


 public class CronTaskDemo2Test extends CronTaskTestHarness {  
   
      public static class DemoCron extends KPIHistoryCleanupCronTask {  
           @Override  
           public void cronAction() {  
                throw new RuntimeException("Stopped!");  
           }            
      }  
        
      /**  
       * Test that KPIHISTORY cron will delete old KPIHISOTRY records.  
       *   
       * @throws RemoteException if an RMI problem occurs.  
       * @throws MXException if a Maximo problem occurs.  
       */  
      @Test  
      public void testKPIHistory() throws RemoteException, MXException {  
           try {  
                // run the cron  
                runCron(DemoCron.class, "KPIHISTORY", "KPIHISTORY1YEAR");  
                fail("Exception expected");  
           } catch (RuntimeException e) {  
                assertEquals("Stopped!", e.getMessage());  
           }       
      }            
 }  

The full source is available in CronTaskDemo2Test.java

Many crons I've written involve data transfers or data updates.  By writing the cron with a method to retrieve or generate an SQL where clause to identify the data on which to operate, it is possible to use MXJUnit to substitute a different where clause for testing.

This example replaces the cronAction() of the KPIHISTORY cron to throw an error.  A new class, DemoCron is created to extend from KPIHistoryCleanupCronTask.  The cronAction() method is overridden to perform a different action.  The cron is executed using the alternative runCron() that accepts a Class object.  It is that Class that will be instantiated instead of the one defined in the CRONTASKDEF table.


      public static class DemoCron extends PMWoGenCronTask {  
           @Override  
           public void cronAction() {  
                throw new RuntimeException("Stopped!");  
           }            
      } 

The full source is available in CronTaskDemo3Test.java

As a safety precaution,  the CronTaskTestHarness makes sure that the class passed in the first parameter is a subclass of the class defined in CRONTASKDEF.

If the DemoCron is changed to extend from PMWoGenCronTask, the test will fail with a message that "Test class must be a subclass of com.ibm.tivoli.maximo.report.kpi.KPIHistoryCleanupCronTask."  This will catch situations where the cron class has changed in the database or the developer makes a mistake in the class definition.

Dynamically Adding a Transactable

I have run into several situations where I have to work on data that could have been modified through one of several different relationships.  It's not practical to consider each possible relationship.  What I have found that works, is to dynamically add a Transactable object.

Here is an example of adding the Transactable object.

 public class CustomMeasurement implements CustomMeasurementRemote {  
   /* ... snip ... */
   @Override
   public void save() throws MXException, RemoteException {
      if (/* condition true */) {
         getMXTransaction().add(new CustomTransactable(this));
      }
      super.save();
   }

   @Override
   public void customWork() throws MXException, RemoteException {
      // perform work that
   }
}


In this example, the custom Measurement object overrides the save method.  When it is determined that the customWork() should be performed, the CustomTransactable is added to the current transaction.  In this example, it's done in the save() method, but it can be done in any method, at any time.

The CustomTransactable looks like this:

 public class CustomTransactable implements Transactable {  
   
   private CustomMeasurement mbo;  
   
   public CustomTransactable(CustomMeasurement mbo) {  
     super();  
     this.mbo = mbo;  
   }  
   
   @Override  
   public void commitTransaction(MXTransaction arg0) throws MXException, RemoteException {  
     return;  
   }  
   
   @Override  
   public void fireEventsAfterDB(MXTransaction arg0) throws MXException, RemoteException {  
     return;  
   }  
   
   @Override  
   public void fireEventsAfterDBCommit(MXTransaction arg0) throws MXException, RemoteException {  
     MboSetRemote msr = mbo.getMboServer().getMboSet("MEASUREMENT", mbo.getUserInfo());  
     CustomMeasurementRemote mr = (CustomMeasurementRemote)msr.getMboForUniqueId(mbo.getUniqueIDValue());  
     mr.customWork();  
     msr.save();  
   }  
   
   @Override  
   public void fireEventsBeforeDB(MXTransaction arg0) throws MXException, RemoteException {  
     return;  
   }  
   
   @Override  
   public void rollbackTransaction(MXTransaction arg0) throws MXException, RemoteException {  
     return;  
   }  
   
   @Override  
   public void saveTransaction(MXTransaction arg0) throws MXException, RemoteException {  
     return;  
   }  
   
   @Override  
   public void undoTransaction(MXTransaction arg0) throws MXException, RemoteException {  
     return;  
   }  
   
   @Override  
   public boolean validateTransaction(MXTransaction arg0) throws MXException, RemoteException {  
     return true;  
   }  
 }  

The constructor takes a reference to the mbo that will be updated.

All the methods, with two exceptions, have empty stubs, but they need to be defined.

The first exception is validateTransaction() returns true.

The second exception is fireEventsAfterDBCommit().  This method is called have the data has been committed to the database.  In this example, it loads a fresh copy of the mbo from the database, calls a method to act on it, and then saves the changes back to the database.

This occurs within the same thread as the call to MboSet.save().  The call will not return until after the CustomTransactable has run.

Since the code executes after the commit, the changes will appear in a separate EAudit record, if the mbo is EAudit enabled.

Getting back to my original problem.  The code in the Mbo or in the screens updates data through one of several different relationships.  It's not easy to tell through which one in the code.  By adding the Transactable object to operate on the object after the commit, the code can use whichever relationship is most appropriate to work on the data.