Testing DFC Offline Using Mock Objects

Share This Post

Introduction

In the recently published article, Unit Testing DFC Code, Bump Verde discussed common difficulties associated with unit testing classes dependent on DFC, and he introduced strategies for dealing with these pain points. In that article, he explored a variety of testing strategies including:

  • Dealing with docbase security.
  • Setting up initial test state in the docbase.
  • Tearing down intial and resultant test state from the docbase.

Using these strategies, any class that depends on DFC and docbase state can be fully tested with a moderate amount of effort.

However, every project I have worked on-large and small-has included a set of classes that are dependent only upon the data retrieved from DFC classes without relying on any of the behavioral logic (for example, link, save, or delete) of the DFC classes themselves. These custom classes are often utility classes, written to encapsulate common, system-wide behavior. For this set of classes that has a simple, read-only dependency on DFC state, I recommend considering mock objects to eliminate the need for a docbase entirely. Direct benefits of using mock objects include:

  • Faster test implementation.
  • Faster test execution.
  • Offline test execution.

These direct benefits tend to give rise to a very important indirect benefit: improved application quality. This is because anything that makes testing easier for developers typically results in greater code coverage and ultimately greater application quality.

To help you get started, let’s take a look at the PathFormat class and the unit test for this class using the open source EasyMock test framework.

PathFormat Sample Class

On a recent project, I needed to control the location of a document (the folder it’s linked into) based on certain attributes of the document. For example, the client might wish for a document with an object_type value of “bf_custom_doc” and object_name value of “DFC Testing with Mocks” to be located under /Temp/bf_custom_doc/DFC Testing with Mocks. To accomplish this, I needed a class which would take a document object, fetch the desired attributes, and return the path of the folder the document should be linked into. I planned to call this class from a DFC Typed Based Object (TBO).

When my program runs, this class will operate on a full-fledged IDfSysObject. But the only data my class needs from the IDfSysObject are the attribute values. It won’t need to call any of the IDfSysObject’s behavioral methods. This makes my class an ideal candidate for testing using Mock Objects.

Let’s take a look at the code for my class, PathFormat:


package com.ArgonDigitalgroup.util;

import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;

import com.documentum.fc.client.IDfDocument;
import com.documentum.fc.client.IDfSysObject;
import com.documentum.fc.common.DfException;

/**
* Parses a folder path pattern into a MessageFormat and an array of attribute names whose values
* should be used when formatting the MessageFormat. For example:
*
* <ul>
*  <li>  Folder path pattern:  /Temp/${object_type}/${object_name}/${r_object_id}  </li>
*  <li>  MessageFormat:        /Temp/{0}/{1}/{2}                                   </li>
*  <li>  Attribute names:      { "object_type", "object_name", "r_object_id" }     </li>
* </tr>
*
* @author smcmichael
*/
public class PathFormat {

	private String _msgFormatPattern;
	private String [] _attrParams;

	public PathFormat(String pattern) {
            // Check if we have any attributes to parse.
            if (pattern.indexOf("${") == -1) {
                // No attr to expand, so create simple MessageFormat pattern with no params.
                _msgFormatPattern = pattern;
                _attrParams = new String[0];
            } else {
                // Need to convert pattern to MessageFormat pattern and attr params.
                StringBuffer format = new StringBuffer(); // A buffer while generating the MessageFormat pattern.
                List attrs = new ArrayList(); // An ordered list of the attribute names in the pattern.
                int paramIndex = 0;

                String [] tokens = pattern.split("${");
                for (int i=0; i < tokens.length; i++) {
                    // The first token may not be an attribute.
                    if (i == 0 && tokens[i].indexOf("}") == -1) {
                        // The first token isn't an attribute, so just append and continue.
                        format.append(tokens[i]);
                        continue;
                    }
                    // Split the current token into two subtokens at the closing curly bracket.
                    String [] subTokens = tokens[i].split("}");
                    // First subtoken is the name of the attribute to expand.
                    attrs.add(subTokens[0]);
                    // Replace the attr in the MessageFormat pattern with the appropriate param placeholder.
                    format.append("{").append(paramIndex++).append("}");
                    // Check for a second subtoken and append to the MessageFormat pattern if found
                    if (subTokens.length > 1) {
                        format.append(subTokens[1]);
                    }
                }
                _msgFormatPattern = format.toString();
                _attrParams = (String[]) attrs.toArray(new String[attrs.size()]);
                }
            }

            /**
             * Formats the path using attributes from the specified {@link IDfDocument}.
             * @param document  The doc to use.
             * @return  The formatted path.
             * @throws DfException
             */
	    public String format(IDfSysObject document) throws DfException {
                String [] attrs = new String[_attrParams.length];
                for (int i=0; i < _attrParams.length; i++) {
                    attrs[i] = document.getString(_attrParams[i]);
                }
                return format(attrs);
            }

            /**
             * Formats the path using specified values.
             * @param attrs  The attribute values to use.
             * @return  The formatted path.
             */
            public String format(Object [] attrs) {
                return MessageFormat.format(_msgFormatPattern, attrs);
            }
        }
    
        

Clearly, there is some complex parsing logic within the PathFormat class that needs to be thoroughly tested if I’m to have any confidence in my implementation. Furthermore, my utility requires an instance of IDfSysObject from which to retrieve the attribute values necessary for path formatting.

However, though my class is dependent on DFC and docbase state, there is no dependency on any of the complex behavior of DFC. Therefore, the PathFormat class is a prime candidate for unit testing with mock objects. Specifically, the unit test for PathFormat will require a mock instance of a DFC persisent object with the proper attribute values set on the mock object ready for retrieval.

Mock Objects and the EasyMock Framework

Testing with mock objects is not yet a widely adopted practice; however, I certainly recognize their utility. If you aren’t familiar with mock objects, you may wish to read a couple of the resources I’ve referenced at the end of this article. Essentially, mock objects implement the same interfaces as the objects they are substituting, allowing you to specify which – and in what order – methods will be invoked on a mock object, what parameters will be passed to them, and what values will be returned. For example, in our particular case, we need a mock instance of an IDfSysObject with the getString(String attrName) method set up to return the appropriate values required by the PathFormat.format(IDfSysObject) implementation.

After reviewing several mock object frameworks, I settled on EasyMock.Within their framework, they’ve included a class of mock objects they call “nice controls.” Nice controls have greatly relaxed method call restrictions, allowing you to easily specify the desired method call return values without enforcing any particular order of the actual method calls as is customary with most mock object implementations. Since I prioritized simplicity and efficiency, the ease of use afforded by the EasyMock nice controls was a requirement for my purposes. Additionally, the use of Java Proxies was a solid design decision for the framework that simplifies the setup required for each mock instance. This makes things much simpler than some of the other frameworks I reviewed. In EasyMock, all that is required to prepare a mock instance is to:

  • Create a MockControl for your desired interface.
  • Obtain the mock object from the mock control.
  • Prepare each mock method invocation by calling the method on the mock object with the appropriate parameters.
  • Notify the mock control of the expected return value for the previous method call.
  • Signal the mock control that setup is complete.

Then our mock object will be ready for use in our test case.

PathFormatTest Unit Test

The PathFormat class has two public methods: one which takes an array of Strings for replacement in the encapsulated MessageFormat and a second which takes an IDfSysObject from which it extracts the required attribute values and then delegates to the first method for final formatting.

The PathFormatTest test case has two test methods: testFormatFromStringArray() and testFormatFromCustomDocument(). The first thoroughly tests the path formatting by constructing a variety of PathFormat forms and then calling the format(String []) method. From this first series of tests, we can fully verify the parsing logic in the constructor, entirely independent of DFC. What remains is to verify the extraction of the attribute values within the format(IDfSysObject) method. The second test method does this by using a mock object to remove the dependency on the docbase. Note how easy it is to create the mock object and prepare it for use:


public void testFormatFromCustomDocument() throws Exception {
    // Create mock object
    MockControl control = MockControl.createNiceControl(ICustomDocument.class);
    ICustomDocument doc = (ICustomDocument) control.getMock();

    // Prepare the mock object
    doc.getString("bf_custom_attr_a");
    control.setReturnValue("Custom Attr A");
    doc.getString("bf_custom_attr_b");
    control.setReturnValue("Custom Attr B");
    doc.getString("bf_custom_attr_c");
    control.setReturnValue("Custom Attr C");

    // Start the mock object
    control.replay();
    ...
}

The full test case class looks like:


package com.ArgonDigitalgroup.util;

import org.easymock.MockControl;
import com.ArgonDigitalgroup.ICustomDocument;
import junit.framework.TestCase;

/**
 * Tests {@link PathFormat}.
 *
 * @author <a href="mailto:smcmichael@argondigital.com">Steve McMichael</a>
 */
public class PathFormatTest extends TestCase {

    public void testFormatFromCustomDocument() throws Exception {
        // Create mock object
        MockRegistry registry = MockRegistry.getSingleton();
        ICustomDocument doc = (ICustomDocument) registry.createMock(ICustomDocument.class); // The mock object!!!
        MockControl control = registry.getControl(doc);

        // Prepare the mock object
        doc.getString("bf_custom_attr_a");
        control.setReturnValue("Custom Attr A");
        doc.getString("bf_custom_attr_b");
        control.setReturnValue("Custom Attr B");
        doc.getString("bf_custom_attr_c");
        control.setReturnValue("Custom Attr C");

        // Start the mock object
        control.replay();

        PathFormat pathFormat = new PathFormat("/Temp/${bf_custom_attr_a}/${bf_custom_attr_c}/${bf_custom_attr_b}");
        String path = pathFormat.format(doc);
        System.out.println(path);
        assertEquals("Paths are not equal", "/Temp/Custom Attr A/Custom Attr C/Custom Attr B", path);
    }

    public void testFormatFromStringArray() throws Exception {

        PathFormat pathFormat = new PathFormat("/Temp/${bf_custom_attr_a}/${bf_custom_attr_b}/${bf_custom_attr_c}");
        String path = pathFormat.format(new String[] {"Custom Attr A", "Custom Attr B", "Custom Attr C"});
        System.out.println(path);
        assertEquals("Paths are not equal", "/Temp/Custom Attr A/Custom Attr B/Custom Attr C", path);

        pathFormat = new PathFormat("${bf_custom_attr_a}/${bf_custom_attr_b}/${bf_custom_attr_c}");
        path = pathFormat.format(new String[] {"Custom Attr A", "Custom Attr B", "Custom Attr C"});
        System.out.println(path);
        assertEquals("Paths are not equal", "Custom Attr A/Custom Attr B/Custom Attr C", path);

        pathFormat = new PathFormat("${bf_custom_attr}");
        path = pathFormat.format(new String[] {"Custom"});
        System.out.println(path);
        assertEquals("Paths are not equal", "Custom", path);

        pathFormat = new PathFormat("/${bf_custom_attr}");
        path = pathFormat.format(new String[] {"Custom"});
        System.out.println(path);
        assertEquals("Paths are not equal", "/Custom", path);

        pathFormat = new PathFormat("${bf_custom_attr}/");
        path = pathFormat.format(new String[] {"Custom"});
        System.out.println(path);
        assertEquals("Paths are not equal", "Custom/", path);

        pathFormat = new PathFormat("/Foo");
        path = pathFormat.format(new String[] {});
        System.out.println(path);
        assertEquals("Paths are not equal", "/Foo", path);

        pathFormat = new PathFormat("/${bf_custom_attr}");
        path = pathFormat.format(new String[] {null});
        System.out.println(path);
        assertEquals("Paths are not equal", "/null", path);

    }

}

See how easy it is to use mock objects? Once set up, a mock object can act as a replacement for any heavyweight object. When it comes to unit testing DFC code, mock objects can save you a lot of time that would otherwise be spent setting up and tearing down data in the docbase. While the code is not significantly simpler either way, it will run faster and can be run in an offline setting.

Conclusion

If your unit tests require extensive docbase communication, you are probably best off following the strategy laid out in Unit Testing DFC Code. If, however, your code simply relies upon the docbase to serve up data, you should give mock objects a try. There are numerous resources available which dive deeper into some of the concepts presented here. To help you out, I’ve provided a short list of references for further reading. Enjoy!

References

More To Explore

b2b auto pay

B2B Auto Pay: Automation Use Cases

Migrating a B2B “Auto Pay” Program Companies migrating to SAP often have daunting challenges to overcome in Accounts Receivable as part of the transition. You might have different divisions running

ArgonDigital | Making Technology a Strategic Advantage