Pages

Tuesday, January 3, 2017

Unit testing Documentum client application using mock IDfSysObjects or serialized IDfSysObjects detached from documentum session

There are two easy options to test the methods with IDfSysObject as arguments without creating connection to the docbase:

The former is the quickest but the mock objects can hardly mimic real documentum objects carrying many values. Nevertheless, if the tested method does not try to access attribute values using IDfSysObject methods such as getString this can be the best option. The latter option is particularly suitable if the tests require objects prefilled with some values so that the tested method can freely manipulate all the attributes. Below I describe two practical variations of this option:

In the first case, the target IDfSysObjects should be loaded from the docbase, saved to the filesystem, and then loaded from disk before the test execution. In the second case, the type should be loaded from the docbase, serialized and then deserialized before creating objects. The generated disconnected objects preserve all behaviors not requiring interaction with the docbase, e.g. the cannot be saved by calling method save.

Creating mock objects implementing DFC interfaces

First create a simple mock class implementing the interface used by the tested methods:

public class IDfSysObjectMock implements IDfSysObject {
}

Then in you IDE click an option "Implement all abstract methods". A class implementing all methods will be generated.

public class IDfSysObjectMock implements IDfSysObject {
  @Override
  public IDfId saveAsNew(boolean shareContent) throws DfException {
    throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
  }

  @Override
  public boolean areAttributesModifiable() throws DfException {
    throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
  // …
 }

Now if a method in your application has IDfSysObject as an argument but invokes only few IDfSysObject methods you can use a modified IDfSysObjectMock instances as arguments. In the instantiated IDfSysObjectMock objects the IDfSysObject methods invoked inside the tested methods should be overridden as appropriate For example:

new IDfSysObjectMock() {
 @Override
 public String getString(String attributeName) throws DfException {
   return "test";
 }
});

This technique is particularly useful when you need to test the exception handling in you methods. Even though normally exceptions do not occur, try-catch blocks must be there. If they are left not tested, it will be reflected in the weakened test coverage report. The following mock object throws DfException when its method save is invoked.

new IDfSysObjectMock() {
 @Override
 public String getLogEntry() throws DfException {
   return "test";
 }

 @Override
 public void setString(String attributeName, String value) throws DfException {
 }

 @Override
 public void save() throws DfException {
      throw new DfException("test");
 }      
};

The same technique can be used with any DFC types. For example mock classes implementing IDfSession, IDfId or IDfType can be used for tests as well. Below is an example of a method producing mock IDfSession instance:

public IDfSession getTargetSession() {
  return new IDfSessionMock() {
    @Override
    public IDfType getType(String typeName) throws DfException {
      return new IDfTypeMock() {
        @Override
        public boolean isSubTypeOf(String typeName) throws DfException {
          return !typeName.equals("dm_sysobject");
        }
      };
    }
  };
}
Generating copies of once serialized IDfSysObjects stored in the docbase

Essentially, first we save any object filled with values to the disk, then we loaded it every time before executing the test. The generated objects will have exactly the same values as the original object. Object serialization and deserialization is illustrated in ObjectSerialization class.

public class ObjectSerialization {

  MyObjectStream os = new MyObjectStream();

  void saveObject(IDfSysObject objFromDocbase, String fileName) throws DfException, IOException, ClassNotFoundException {
    // save all the object data
    ITypedData originalData = ((ITypedObject) objFromDocbase).getData(false);
    os.write(originalData, fileName);
  }
   
  // load and instantiate a serialized IDfSysObject. note documentum session is irrelevant
  IDfSysObject loadObject(String fileName) throws DfException, IOException, ClassNotFoundException {

    ITypedData loadedData = (ITypedData) os.read(fileName);
    DfSysObject serializedObject = new DetachedDfSysObject();
    serializedObject.initialize(null, loadedData, null, null, true);
    return serializedObject;
  }
  // …
}

A simple auxiliary class encoding serialization:

public class MyObjectStream {

  public void write(Object o, String fileName) throws IOException {
    try (ObjectOutputStream out = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(fileName)))) {
      out.writeObject(o);
    }
  }

  public Object read(String fileName) throws IOException, ClassNotFoundException {
    try (ObjectInputStream in = new ObjectInputStream(new BufferedInputStream(new FileInputStream(fileName)))) {
      return in.readObject();
    }
  }
}

In the mock DfSysObject subclass several methods, namely initialize, init, setDirty, have to be overrriden so that the class can be instantiated. Optionally you can override any other methods invoked in you tested methods, e.g save or link, so that they do not fail as the instances are disconnected from the docbase.

public class DetachedDfSysObject extends DfSysObject {

  @Override
  public void save() throws DfException {
    System.out.println("fake save");
  }

  @Override
  public void destroy() throws DfException {
    System.out.println("fake destroy");
  }

  @Override
  public void link(String folderSpec) throws DfException {
    System.out.println("fake link");
  }

  @Override
  public void initialize(final IPersistentObjectFactory factory, final ITypedData data, final ISession session, final ISession originalSession, final boolean isNew) throws DfException {
    this.initData(data);
    this.init();
  }

  @Override
  protected void init() throws DfException {
  }

  @Override
  public synchronized void setDirty(final boolean dirty) throws DfException {
  }
}

A junit test proving that the original and deserialized object contain the same data:

public class ObjectSerializationTest {

  ObjectSerialization i = new ObjectSerialization();
  IDfSession session;

  @Before
  public void setUp() throws DfException {
    session = ConnectionFactory.getSession();
  }

  @After
  public void tearDown() throws DfException {
    session.disconnect();
  }

  @Test
  public void testSerializingAndLoadingDfSysObject() throws Exception {
    // load an object from the docbase
    String existingObjectId = "090f4241800c2507";
    IDfSysObject existingObj = (IDfSysObject) session.getObject(new DfId(existingObjectId));

    String fileName = "Serizalized" + existingObjectId;

    // save all the object data
    i.saveObject(existingObj, fileName);

    // now load serialized IDfSysObject
    IDfSysObject loadedSerializedObject = i.loadObject(fileName);
    compareAllAttributeValues(existingObj, loadedSerializedObject);
  }

  void compareAllAttributeValues(IDfSysObject obj1, IDfSysObject obj2) throws DfException {
    for (int i = 0; i < obj1.getAttrCount(); i++) {
      IDfAttr attr = obj1.getAttr(i);
      String attrName = attr.getName();
      String attrValue1, attrValue2;

      if (attr.isRepeating()) {
        attrValue1 = obj1.getAllRepeatingStrings(attrName, "|");
        attrValue2 = obj2.getAllRepeatingStrings(attrName, "|");
      } else {
        attrValue1 = obj1.getString(attrName);
        attrValue2 = obj2.getString(attrName);
      }
      System.out.println(">>> " + attrName);
      System.out.println("\t" + attrValue1);
      System.out.println("\t" + attrValue2);
      assertEquals(attrValue1, attrValue2);
    }
  }
}
Saving a type definition to filesystem and generating empty IDfSysObject objects of this type

Essentially, first we load the target type definition from the docbase, save it, and then before tests load it and generate detached but valid empty IDfSysObjects.

public class ObjectSerialization {

  MyObjectStream os = new MyObjectStream();
  // methods shown above …

  // serialize a type definition
  void saveType(IDfSession session, String typeName, String fileName) throws DfException, IOException, ClassNotFoundException {
    // load the type definition from the docbase
    ILiteType liteType = ((ISession) session).getDocbaseConnection().getLiteType(typeName);
    // serialize the type definition
    os.write(liteType, fileName);
  }

  // create an empty object of a particular type without using documentum session
  IDfSysObject loadTypeAndCreateEmptyObject(String fileName) throws DfException, IOException, ClassNotFoundException {
    // load the serialized type definition     
    ILiteType type = (ILiteType) os.read(fileName);

    // create data container
    final ITypedData typedData = new TypedData(type, null);

    // now instantiate an empty IDfSysObject 
    final DfSysObject emptyObject = new DetachedDfSysObject();
    emptyObject.initialize(null, typedData, null, null, true);
    return emptyObject;
  }
}

The following junit test asserts that a generated object has all the attributes of the target type and all the attributes are empty.

public class ObjectSerializationTest {

  // declarations shown above …

  @Test
  public void testSerializingTypeAndGeneratingEmptyDfSysObject() throws Exception {
    // load an object from the docbase
    String typeName = "testtype";
    String fileName = "Serizalized" + typeName;

    // serialize the type definition
    i.saveType(session, typeName, fileName);

    // now create an empty IDfSysObject of the serialized type
    IDfSysObject generatedObject = i.loadTypeAndCreateEmptyObject(fileName);
    assertThatObjectHasAllAttributesAndTheyAreEmpty(typeName, generatedObject);
  }

  void assertThatObjectHasAllAttributesAndTheyAreEmpty(String typeName, IDfSysObject obj) throws DfException {
    IDfType existingType = session.getType(typeName);
    for (int i = 0; i < existingType.getTypeAttrCount(); i++) {
      IDfAttr attr = existingType.getTypeAttr(i);
      String attrName = attr.getName();
      String attrValue;
      int attrType = attr.getDataType();
      if (attr.isRepeating()) {
        attrValue = obj.getAllRepeatingStrings(attrName, "|");
      } else {
        attrValue = obj.getString(attrName);
      }

      System.out.println(">>> " + attrName + "; " + attrType + "; " + attrValue);
      switch (attr.getDataType()) {
        case IDfAttr.DM_TIME:
          assertTrue(attrValue.equals("nulldate") || attrValue.isEmpty());
          break;
        case IDfAttr.DM_BOOLEAN:
          assertTrue(attrValue.equals("F") || attrValue.isEmpty());
          break;
        case IDfAttr.DM_INTEGER:
          assertTrue(attrValue.equals("0") || attrValue.isEmpty());
          break;
        case IDfAttr.DM_DOUBLE:
          assertTrue(attrValue.equals("0") || attrValue.isEmpty());
          break;
        case IDfAttr.DM_ID:
          assertTrue(attrValue.equals(DfId.DF_NULLID_STR) || attrValue.isEmpty());
          break;
        default:
          assertTrue(attrValue.isEmpty());
      }
    }
  }
}

No comments:

Post a Comment