Easier mocks with data

I have a love/hate relationship with mocks in unit tests. They are wonderful to mock out services, but building mocks or objects with just enough data for testing is often a pain. I end up writing code like:

Person person = new Person();
person.setName("John Doe");
person.setLocation("Brussels");
Organization organization = new Organization();
organizaion.setName("Company Ltd");
person.setOrganization(organization);

I believe this is too verbose to be nice. As such test objects are often needed this code may be moved to a separate class to build test mother objects. This arguably makes things worse by hiding the values used and more or less forcing all tests to use the same field values.

When I need this object to be a Mockito mock it becomes something like this:

Person person = Mockito.mock(Person.class);
when(person.getName()).thenReturn("John Doe");
when(person.getLocation()).thenReturn("Brussels");
Organization organization = mock(Organization.class);
when(organizaion.getName()).thenReturn("Company Ltd");
when(person.getOrganization()).thenReturn(organization);

This is not very readable.

Taking inspiration from faker gems in Ruby, I created a new library to make this more legible.

Person person = FakerMock.withFields(Person.class,
        "name", "John Doe",
        "location", "Brussels",
        "organization", FakerMock.withFields(Organization.class, 
                "name", "Company Ltd"));

This is now available as jFaker.

This is a reasonable simple start, but I would like to extend this to also support javabeans (same syntax but using FakerBean instead of FakerMock) and also te be able to have values read from a yaml file. In that case, I would probably use a syntax like

FakerBuilder faker = new FakerBuilder().
        withYaml("my/yaml/file/with/general/defaults.yml");
        withYaml("my/yaml/file/with/defaults/for/this/test.yml");
Person person = faker.withFields(Person.class,
        "name", "John Doe",
        "location", "Brussels",
        "organization", faker.withFields(Organization.class, 
                "name", "Company Ltd"));

Would this be useful for you?

OwnCloud, fixing the WebDAV warning

I had a fight with my system when trying to to a OwnCloud setup.

It was constantly saying (after a long wait when trying to open the admin page)

isWebDAVWorking: NO - Reason: [CURL] Error while making request: Connection timed out after 10001 milliseconds (error code: 28) (Sabre_DAV_Exception)

After figuring out the proper URL (https://owncloud.installation.domain/remote.php/webdav/) it dawned to me that the server me not be able to reach itself.
Indeed, adding an entry in /etc/hosts/ to assure that the domain points to 127.0.0.1 instead of the real IP address worked.

Storing Activiti process variables as JSON text

By default Activiti will use Java serialization to store non-primitive process variables in the database.
This is nice and mostly works but has a few disadvantages:

  • You have to be careful about your serialVersionUID fields on the object (you need it).
  • It is not refactor friendly. Change a class or field name creates havoc.
  • You cannot use a database query to search on property value.

Here is some code to use JSON as serialization format. The result is put in the text_ field to allow database searches. However, this does limit the maximum length of the string (you can increase the field length if you want – by default Activiti defines it as 4000 characters).

You have to create a VariableType implementation and pass that to your configuration. When initializing using Spring, this can be done using something like:

<bean id="processEngineConfiguration">
    <!-- your usual configuration -->
 
    <property name="customPreVariableTypes">
        <list>
            <bean class="mypackage.SampleAsJsontype" />
            <bean class="mypackage.SampleListAsJsontype" />
            <bean class="mypackage.SerializeAsJsonType" />
        </list>
    </property>
 
</bean>

VariableType for a specific object type.

public class SampleAsJsontype implements VariableType {
 
    private static final Logger LOG = LoggerFactory.getLogger(SampleAsJsontype.class);
 
    private DcJsonMapper mapper = new DcJsonMapper();
 
    @Override
    public String getTypeName() {
        return "sample";
    }
 
    @Override
    public boolean isCachable() {
        return true;
    }
 
    @Override
    public Object getValue(ValueFields valueFields) {
        try {
            return mapper.readValue(valueFields.getTextValue(), Sample.class);
        } catch (IOException ioe) {
            LOG.error("Kan object niet converteren naar Sample: " + valueFields.getTextValue(), ioe);
            return null;
        }
    }
 
    @Override
    public void setValue(Object value, ValueFields valueFields) {
        if (null == value) {
            valueFields.setTextValue(""); // needed to allow removing variables
        } else {
            try {
                valueFields.setTextValue(mapper.writeValueAsString(value));
            } catch (IOException ioe) {
                LOG.error("Kan Sample niet converteren o(JSON) string: " + value, ioe);
            }
        }
    }
 
    @Override
    public boolean isAbleToStore(Object value) {
        return value instanceof Sample;
    }
 
}

When it is a list of this type you can use something like:

public class SampleListAsJsontype implements VariableType {
 
    private static final Logger LOG = LoggerFactory.getLogger(SampleListAsJsontype.class);
 
    private DcJsonMapper mapper = new DcJsonMapper();
 
    @Override
    public String getTypeName() {
        return "sampleList";
    }
 
    @Override
    public boolean isCachable() {
        return true;
    }
 
    @Override
    public Object getValue(ValueFields valueFields) {
        try {
            return mapper.readValue(valueFields.getTextValue(), new TypeReference<List<Sample>>() { });
        } catch (IOException ioe) {
            LOG.error("Kan object niet converteren naar Sample: " + valueFields.getTextValue(), ioe);
            return null;
        }
    }
 
    @Override
    public void setValue(Object value, ValueFields valueFields) {
        if (null == value) {
            valueFields.setTextValue(""); // needed to allow removing variables
        } else {
            try {
                valueFields.setTextValue(mapper.writeValueAsString(value));
            } catch (IOException ioe) {
                LOG.error("Kan Sample niet converteren o(JSON) string: " + value, ioe);
            }
        }
    }
 
    @Override
    public boolean isAbleToStore(Object value) {
        return value instanceof Collection && containsAllLijnLocatie((Collection) value);
    }
 
    private boolean containsAllLijnLocatie(Collection collection) {
        for (Object object : collection) {
            if (!(object instanceof Sample)) {
                return false;
            }
        }
        return true;
    }
 
}

Much more practical is a generic solution which converts many object. Here is the marker interface to trigger the serialization.

public interface SerializeAsJson {
}

In this case, the JSON data is prepended with the Java type to assure the JSON mapper knows what to do. This code also assures the length of the string fits the field.

public class SerializeAsJsonType implements VariableType {
 
    private static final Logger LOG = LoggerFactory.getLogger(SerializeAsJsonType.class);
 
    private static final String SEPARATOR = "=>";
    private static final int SERIALIZED_MAX_LENGTH = 44000; // Activiti default field length
 
    private DcJsonMapper mapper = new DcJsonMapper();
 
    @Override
    public String getTypeName() {
        return "serializeAsJson";
    }
 
    @Override
    public boolean isCachable() {
        return true;
    }
 
    @Override
    public Object getValue(ValueFields valueFields) {
        try {
            String value = valueFields.getTextValue();
            int pos = value.indexOf(SEPARATOR);
            if (pos < 1) {
                throw new IllegalArgumentException("Value for serializeAsJson does not contain type indicator.");
            }
            String className = value.substring(0, pos);
            String json = value.substring(pos + SEPARATOR.length());
            ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
            if (null == classLoader) {
                classLoader = this.getClass().getClassLoader();
            }
            try {
                Class clazz = classLoader.loadClass(className);
                return mapper.readValue(json, clazz);
            } catch (ClassNotFoundException cnfe) {
                throw new IllegalArgumentException("Cannot find class  " + className + ".", cnfe);
            }
        } catch (IOException ioe) {
            LOG.error("Cannot convert JSON to object: " + valueFields.getTextValue(), ioe);
            return null;
        }
    }
 
    @Override
    public void setValue(Object value, ValueFields valueFields) {
        if (null == value) {
            valueFields.setTextValue(""); // needed to allow removing variables
        } else {
            try {
                String serialized = value.getClass().getName() + SEPARATOR + mapper.writeValueAsString(value);
                if (serialized.length() > SERIALIZED_MAX_LENGTH) {
                    throw new IllegalArgumentException("Serialized value for object of type " +
                            value.getClass().getName() + " is too long to store as JSON object.");
                }
                valueFields.setTextValue(serialized);
            } catch (IOException ioe) {
                LOG.error("Cannot convert " + value.getClass().getName() + " to (JSON) string: " + value, ioe);
            }
        }
    }
 
    @Override
    public boolean isAbleToStore(Object value) {
        return value instanceof SerializeAsJson;
    }
 
}

If you want the JSON length limit to be increased you can alter the SERIALIZED_MAX_LENGTH constant in the code above and increase the field size in your database using something like (PostgreSQL sample):

ALTER TABLE act_ru_variable ALTER text_ TYPE VARCHAR(16000);
ALTER TABLE act_hi_varinst ALTER text_ TYPE VARCHAR(16000);
ALTER TABLE act_hi_detail ALTER text_ TYPE VARCHAR(16000);

If you already have process variables in your database which did not use these serializers, then you will need some migration code.
The bg trick is to delete the process variable before storing it again. If you don’t delete it first Activiti will try to store the updated value using the same serializer (if that serializer can store the updated value).

This code handles the migration when the system starts. It is implemented as a Spring service.

@Component
public class MigrateProcessVariables {
 
    @Autowired
    private TaskService taskService;
 
    /**
     * Migrate existing variables to store again using the JSON serializers.
     */
    @PostConstruct
    public void fixSerializedVariables() {
        List<Task> tasks = taskService.createTaskQuery().list();
        for (Task task : tasks) {
            Map<String, Object> vars = taskService.getVariables(task.getId());
            for (Map.Entry<String, Object> entry : vars.entrySet()) {
                Object value = entry.getValue();
                if (value instanceof Sample || value instanceof SerializeAsJson) {
                    taskService.removeVariable(task.getId(), entry.getKey());
                    taskService.setVariable(task.getId(), entry.getKey(), value);
                }
                if (value instanceof List) {
                    // convert possible List<Sample> objects
                    List list = (List) value;
                    boolean changed = list.size() > 0;
                    for (int i = 0; i < list.size(); i++) {
                        Object item = list.get(i);
                        if (!(item instanceof Sample)) {
                            changed = false;
                        }
                    }
                    if (changed) {
                        taskService.removeVariable(task.getId(), entry.getKey());
                        taskService.setVariable(task.getId(), entry.getKey(), list);
                    }
                }
            }
        }
    }
 
}