org.openmrs.ObsTest.java Source code

Java tutorial

Introduction

Here is the source code for org.openmrs.ObsTest.java

Source

/**
 * This Source Code Form is subject to the terms of the Mozilla Public License,
 * v. 2.0. If a copy of the MPL was not distributed with this file, You can
 * obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under
 * the terms of the Healthcare Disclaimer located at http://openmrs.org/license.
 *
 * Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS
 * graphic logo is a trademark of OpenMRS Inc.
 */
package org.openmrs;

import static org.hamcrest.core.Is.is;
import static org.hamcrest.core.IsNot.not;
import static org.hamcrest.core.IsNull.nullValue;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.lang.reflect.Field;
import java.sql.Timestamp;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;

import org.apache.commons.beanutils.BeanUtils;
import org.junit.Assert;
import org.junit.Test;
import org.openmrs.api.APIException;
import org.openmrs.obs.ComplexData;
import org.openmrs.util.Reflect;

/**
 * This class tests all methods that are not getter or setters in the Obs java object TODO: finish
 * this test class for Obs
 * 
 * @see Obs
 */
public class ObsTest {

    private static final String VERO = "Vero";

    private static final String FORM_NAMESPACE_PATH_SEPARATOR = "^";

    //ignore these fields, groupMembers and formNamespaceAndPath field are taken care of by other tests
    private static final List<String> IGNORED_FIELDS = Arrays.asList("dirty", "log", "serialVersionUID",
            "DATE_TIME_PATTERN", "TIME_PATTERN", "DATE_PATTERN", "FORM_NAMESPACE_PATH_SEPARATOR",
            "FORM_NAMESPACE_PATH_MAX_LENGTH", "obsId", "groupMembers", "uuid", "changedBy", "dateChanged", "voided",
            "voidedBy", "voidReason", "dateVoided", "formNamespaceAndPath", "$jacocoData");

    private void resetObs(Obs obs) throws Exception {
        Field field = Obs.class.getDeclaredField("dirty");
        field.setAccessible(true);
        try {
            field.set(obs, false);
        } finally {
            field.setAccessible(false);
        }
        assertFalse(obs.isDirty());
    }

    private Obs createObs(Integer id) throws Exception {
        Obs obs = new Obs(id);
        List<Field> fields = Reflect.getAllFields(Obs.class);
        for (Field field : fields) {
            if (IGNORED_FIELDS.contains(field.getName())) {
                continue;
            }
            setFieldValue(obs, field, false);
        }
        assertFalse(obs.isDirty());
        return obs;
    }

    private void setFieldValue(Obs obs, Field field, boolean setAlternateValue) throws Exception {
        final boolean accessible = field.isAccessible();
        if (!accessible) {
            field.setAccessible(true);
        }
        try {
            Object oldFieldValue = field.get(obs);
            Object newFieldValue = generateValue(field, setAlternateValue);
            //sanity check
            if (setAlternateValue) {
                assertNotEquals("The old and new values should be different for field: Obs." + field.getName(),
                        oldFieldValue, newFieldValue);
            }

            field.set(obs, newFieldValue);
        } finally {
            field.setAccessible(accessible);
        }
    }

    private Object generateValue(Field field, boolean setAlternateValue) throws Exception {
        Object fieldValue;
        if (field.getType().equals(Boolean.class)) {
            fieldValue = setAlternateValue ? true : false;
        } else if (field.getType().equals(Integer.class)) {
            fieldValue = setAlternateValue ? 10 : 17;
        } else if (field.getType().equals(Double.class)) {
            fieldValue = setAlternateValue ? 5.0 : 7.0;
        } else if (field.getType().equals(Date.class)) {
            fieldValue = new Date();
            if (setAlternateValue) {
                Calendar c = Calendar.getInstance();
                c.add(Calendar.MINUTE, 2);
                fieldValue = c.getTime();
            }
        } else if (field.getType().equals(String.class)) {
            fieldValue = setAlternateValue ? "old" : "new";
        } else if (field.getType().equals(Person.class)) {
            //setPerson updates the personId, so we want the personIds to match for the tests to be valid
            fieldValue = new Person(setAlternateValue ? 10 : 17);
        } else if (field.getType().equals(ComplexData.class)) {
            fieldValue = new ComplexData(setAlternateValue ? "some complex data" : "Some other value",
                    new Object());
        } else if (field.getType().equals(Obs.Interpretation.class)) {
            fieldValue = setAlternateValue ? Obs.Interpretation.ABNORMAL : Obs.Interpretation.CRITICALLY_ABNORMAL;
        } else if (field.getType().equals(Obs.Status.class)) {
            fieldValue = setAlternateValue ? Obs.Status.AMENDED : Obs.Status.PRELIMINARY;
        } else {
            fieldValue = field.getType().newInstance();
        }
        assertNotNull("Failed to generate a value for field: Obs." + field.getName());

        return fieldValue;
    }

    /**
     * Tests the addToGroup method in ObsGroup
     * 
     * @throws Exception
     */
    @Test
    public void shouldAddandRemoveObsToGroup() throws Exception {

        Obs obs = new Obs(1);

        Obs obsGroup = new Obs(755);

        // These methods should not fail even with null attributes on the obs
        assertFalse(obsGroup.isObsGrouping());
        assertFalse(obsGroup.hasGroupMembers(false));
        assertFalse(obsGroup.hasGroupMembers(true)); // Check both flags for false

        // adding an obs when the obs group has no other obs
        // should not throw an error
        obsGroup.addGroupMember(obs);
        assertEquals(1, obsGroup.getGroupMembers().size());

        // check duplicate add. should only be one
        obsGroup.addGroupMember(obs);
        assertTrue(obsGroup.hasGroupMembers(false));
        assertEquals("Duplicate add should not increase the grouped obs size", 1,
                obsGroup.getGroupMembers().size());

        Obs obs2 = new Obs(2);

        obsGroup.removeGroupMember(obs2);
        assertTrue(obsGroup.hasGroupMembers(false));
        assertEquals("Removing a non existent obs should not decrease the number of grouped obs", 1,
                obsGroup.getGroupMembers().size());

        // testing removing an obs from a group that has a null obs list
        new Obs().removeGroupMember(obs2);

        obsGroup.removeGroupMember(obs);

        assertEquals(0, obsGroup.getGroupMembers().size());

        // try to add an obs group to itself
        try {
            obsGroup.addGroupMember(obsGroup);
            fail("An APIException about adding an obsGroup should have been thrown");
        } catch (APIException e) {
            // this exception is expected
        }
    }

    /**
     * tests the getRelatedObservations method:
     */
    @Test
    public void shouldGetRelatedObservations() throws Exception {
        // create a child Obs
        Obs o = new Obs();
        o.setDateCreated(new Date());
        o.setLocation(new Location(1));
        o.setObsDatetime(new Date());
        o.setPerson(new Patient(2));
        o.setValueText("childObs");

        // create its sibling
        Obs oSibling = new Obs();
        oSibling.setDateCreated(new Date());
        oSibling.setLocation(new Location(1));
        oSibling.setObsDatetime(new Date());
        oSibling.setValueText("childObs2");
        oSibling.setPerson(new Patient(2));

        // create a parent Obs
        Obs oParent = new Obs();
        oParent.setDateCreated(new Date());
        oParent.setLocation(new Location(1));
        oParent.setObsDatetime(new Date());
        oSibling.setValueText("parentObs");
        oParent.setPerson(new Patient(2));

        // create a grandparent obs
        Obs oGrandparent = new Obs();
        oGrandparent.setDateCreated(new Date());
        oGrandparent.setLocation(new Location(1));
        oGrandparent.setObsDatetime(new Date());
        oGrandparent.setPerson(new Patient(2));
        oSibling.setValueText("grandParentObs");

        oParent.addGroupMember(o);
        oParent.addGroupMember(oSibling);
        oGrandparent.addGroupMember(oParent);

        // create a leaf observation at the grandparent level
        Obs o2 = new Obs();
        o2.setDateCreated(new Date());
        o2.setLocation(new Location(1));
        o2.setObsDatetime(new Date());
        o2.setPerson(new Patient(2));
        o2.setValueText("grandparentLeafObs");

        oGrandparent.addGroupMember(o2);

        /**
         * test to make sure that if the original child obs calls getRelatedObservations, it returns
         * itself and its siblings: original obs is one of two groupMembers, so relatedObservations
         * should return a size of set 2 then, make sure that if oParent calls
         * getRelatedObservations, it returns its own children as well as the leaf obs attached to
         * the grandparentObs oParent has two members, and one leaf ancestor -- so a set of size 3
         * should be returned.
         */
        assertEquals(o.getRelatedObservations().size(), 2);
        assertEquals(oParent.getRelatedObservations().size(), 3);

        // create a great-grandparent obs
        Obs oGGP = new Obs();
        oGGP.setDateCreated(new Date());
        oGGP.setLocation(new Location(1));
        oGGP.setObsDatetime(new Date());
        oGGP.setPerson(new Patient(2));
        oGGP.setValueText("grandParentObs");
        oGGP.addGroupMember(oGrandparent);

        // create a leaf great-grandparent obs
        Obs oGGPleaf = new Obs();
        oGGPleaf.setDateCreated(new Date());
        oGGPleaf.setLocation(new Location(1));
        oGGPleaf.setObsDatetime(new Date());
        oGGPleaf.setPerson(new Patient(2));
        oGGPleaf.setValueText("grandParentObs");
        oGGP.addGroupMember(oGGPleaf);

        /**
         * now run the previous assertions again. this time there are two ancestor leaf obs, so the
         * first assertion should still return a set of size 2, but the second assertion sould
         * return a set of size 4.
         */
        assertEquals(o.getRelatedObservations().size(), 2);
        assertEquals(oParent.getRelatedObservations().size(), 4);

        // remove the grandparent leaf observation:

        oGrandparent.removeGroupMember(o2);

        // now the there is only one ancestor leaf obs:
        assertEquals(o.getRelatedObservations().size(), 2);
        assertEquals(oParent.getRelatedObservations().size(), 3);

        /**
         * finally, test a non-obsGroup and non-member Obs to the function Obs o2 is now not
         * connected to our heirarchy: an empty set should be returned:
         */

        assertNotNull(o2.getRelatedObservations());
        assertEquals(o2.getRelatedObservations().size(), 0);

    }

    /**
     * @see Obs#isComplex()
     */
    @Test
    public void isComplex_shouldReturnTrueIfTheConceptIsComplex() throws Exception {
        ConceptDatatype cd = new ConceptDatatype();
        cd.setName("Complex");
        cd.setHl7Abbreviation("ED");

        ConceptComplex complexConcept = new ConceptComplex();
        complexConcept.setDatatype(cd);

        Obs obs = new Obs();
        obs.setConcept(complexConcept);

        Assert.assertTrue(obs.isComplex());
    }

    /**
     * @see Obs#setValueAsString(String)
     */
    @Test(expected = RuntimeException.class)
    public void setValueAsString_shouldFailIfTheValueOfTheStringIsEmpty() throws Exception {
        Obs obs = new Obs();
        obs.setValueAsString("");
    }

    /**
     * @see Obs#setValueAsString(String)
     */
    @Test(expected = RuntimeException.class)
    public void setValueAsString_shouldFailIfTheValueOfTheStringIsNull() throws Exception {
        Obs obs = new Obs();
        obs.setValueAsString(null);
    }

    /**
     * @see Obs#getValueAsBoolean()
     */
    @Test
    public void getValueAsBoolean_shouldReturnFalseForValue_numericConceptsIfValueIs0() throws Exception {
        Obs obs = new Obs();
        obs.setValueNumeric(0.0);
        Assert.assertEquals(false, obs.getValueAsBoolean());
    }

    /**
     * @see Obs#getValueAsBoolean()
     */
    @Test
    public void getValueAsBoolean_shouldReturnNullForValue_numericConceptsIfValueIsNeither1Nor0() throws Exception {
        Obs obs = new Obs();
        obs.setValueNumeric(24.8);
        Assert.assertNull(obs.getValueAsBoolean());
    }

    @Test
    public void getValueAsString_shouldReturnNonPreciseValuesForNumericConcepts() throws Exception {
        Obs obs = new Obs();
        obs.setValueNumeric(25.125);
        ConceptNumeric cn = new ConceptNumeric();
        ConceptDatatype cdt = new ConceptDatatype();
        cdt.setHl7Abbreviation("NM");
        cn.setDatatype(cdt);
        cn.setAllowDecimal(false);
        obs.setConcept(cn);
        String str = "25";
        Assert.assertEquals(str, obs.getValueAsString(Locale.US));
    }

    @Test
    public void getValueAsString_shouldNotReturnLongDecimalNumbersAsScientificNotation() throws Exception {
        Obs obs = new Obs();
        obs.setValueNumeric(123456789.0);
        String str = "123456789.0";
        Assert.assertEquals(str, obs.getValueAsString(Locale.US));
    }

    @Test
    public void getValueAsString_shouldReturnDateInCorrectFormat() throws Exception {
        Obs obs = new Obs();
        obs.setValueDatetime(new Date());
        Concept cn = new Concept();
        ConceptDatatype cdt = new ConceptDatatype();
        cdt.setHl7Abbreviation("DT");
        cn.setDatatype(cdt);
        obs.setConcept(cn);

        Date utilDate = new Date();
        DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        String dateString = dateFormat.format(utilDate);
        Assert.assertEquals(dateString, obs.getValueAsString(Locale.US));
    }

    /**
     * @see Obs#getValueAsBoolean()
     */
    @Test
    public void getValueAsBoolean_shouldReturnTrueForValue_numericConceptsIfValueIs1() throws Exception {
        Obs obs = new Obs();
        obs.setValueNumeric(1.0);
        Assert.assertEquals(true, obs.getValueAsBoolean());
    }

    /**
     * @see Obs#getGroupMembers(boolean)
     */
    @Test
    public void getGroupMembers_shouldGetAllGroupMembersIfPassedTrueAndNonvoidedIfPassedFalse() throws Exception {
        Obs parent = new Obs(1);
        Set<Obs> members = new HashSet<>();
        members.add(new Obs(101));
        members.add(new Obs(103));
        Obs voided = new Obs(99);
        voided.setVoided(true);
        members.add(voided);
        parent.setGroupMembers(members);
        members = parent.getGroupMembers(true);
        assertEquals("set of all members should have length of 3", 3, members.size());
        members = parent.getGroupMembers(false);
        assertEquals("set of non-voided should have length of 2", 2, members.size());
        members = parent.getGroupMembers(); // should be same as false
        assertEquals("default should return non-voided with length of 2", 2, members.size());
    }

    /**
     * @see Obs#hasGroupMembers(boolean)
     */
    @Test
    public void hasGroupMembers_shouldReturnTrueIfThisObsHasGroupMembersBasedOnParameter() throws Exception {
        Obs parent = new Obs(5);
        Obs child = new Obs(33);
        child.setVoided(true);
        parent.addGroupMember(child); // Only contains 1 voided child
        assertTrue("When checking for all members, should return true", parent.hasGroupMembers(true));
        assertFalse("When checking for non-voided, should return false", parent.hasGroupMembers(false));
        assertFalse("Default should check for non-voided", parent.hasGroupMembers());
    }

    /**
     * @see Obs#isObsGrouping()
     */
    @Test
    public void isObsGrouping_shouldIncludeVoidedObs() throws Exception {
        Obs parent = new Obs(5);
        Obs child = new Obs(33);
        child.setVoided(true);
        parent.addGroupMember(child);
        assertTrue("When checking for Obs grouping, should include voided Obs", parent.isObsGrouping());
    }

    /**
     * @see Obs#getValueAsString(Locale)
     */
    @Test
    public void getValueAsString_shouldUseCommasOrDecimalPlacesDependingOnLocale() throws Exception {
        Obs obs = new Obs();
        obs.setValueNumeric(123456789.3);
        String str = "123456789,3";
        Assert.assertEquals(str, obs.getValueAsString(Locale.GERMAN));
    }

    /**
     * @see Obs#getValueAsString(Locale)
     */
    @Test
    public void getValueAsString_shouldNotUseThousandSeparator() throws Exception {
        Obs obs = new Obs();
        obs.setValueNumeric(123456789.0);
        String str = "123456789.0";
        Assert.assertEquals(str, obs.getValueAsString(Locale.ENGLISH));
    }

    /**
     * @see Obs#getValueAsString(Locale)
     */
    @Test
    public void getValueAsString_shouldReturnRegularNumberForSizeOfZeroToOrGreaterThanTenDigits() throws Exception {
        Obs obs = new Obs();
        obs.setValueNumeric(1234567890.0);
        String str = "1234567890.0";
        Assert.assertEquals(str, obs.getValueAsString(Locale.ENGLISH));
    }

    /**
     * @see Obs#getValueAsString(Locale)
     */
    @Test
    public void getValueAsString_shouldReturnRegularNumberIfDecimalPlacesAreAsHighAsSix() throws Exception {
        Obs obs = new Obs();
        obs.setValueNumeric(123456789.012345);
        String str = "123456789.012345";
        Assert.assertEquals(str, obs.getValueAsString(Locale.ENGLISH));
    }

    @Test
    public void getValueAsString_shouldReturnLocalizedCodedConcept() throws Exception {
        ConceptDatatype cdt = new ConceptDatatype();
        cdt.setHl7Abbreviation("CWE");

        Concept cn = new Concept();
        cn.setDatatype(cdt);
        cn.addName(new ConceptName(VERO, Locale.ITALIAN));

        Obs obs = new Obs();
        obs.setValueCoded(cn);
        obs.setConcept(cn);
        obs.setValueCodedName(new ConceptName("True", Locale.US));

        Assert.assertEquals(VERO, obs.getValueAsString(Locale.ITALIAN));
    }

    /**
     * @see Obs#setFormField(String,String)
     */
    @Test
    public void setFormField_shouldSetTheUnderlyingFormNamespaceAndPathInTheCorrectPattern() throws Exception {
        final String ns = "my ns";
        final String path = "my path";
        Obs obs = new Obs();
        obs.setFormField(ns, path);
        java.lang.reflect.Field formNamespaceAndPathProperty = Obs.class.getDeclaredField("formNamespaceAndPath");
        formNamespaceAndPathProperty.setAccessible(true);
        Assert.assertEquals(ns + FORM_NAMESPACE_PATH_SEPARATOR + path, formNamespaceAndPathProperty.get(obs));
    }

    /**
     * @see Obs#getFormFieldNamespace()
     */
    @Test
    public void getFormFieldNamespace_shouldReturnNullIfTheNamespaceIsNotSpecified() throws Exception {
        Obs obs = new Obs();
        obs.setFormField("", "my path");
        Assert.assertNull(obs.getFormFieldNamespace());
    }

    /**
     * @see Obs#getFormFieldNamespace()
     */
    @Test
    public void getFormFieldNamespace_shouldReturnTheCorrectNamespaceForAFormFieldWithAPath() throws Exception {
        final String ns = "my ns";
        final String path = "my path";
        Obs obs = new Obs();
        obs.setFormField(ns, path);
        Assert.assertEquals(ns, obs.getFormFieldNamespace());
    }

    /**
     * @see Obs#getFormFieldNamespace()
     */
    @Test
    public void getFormFieldNamespace_shouldReturnTheNamespaceForAFormFieldThatHasNoPath() throws Exception {
        final String ns = "my ns";
        Obs obs = new Obs();
        obs.setFormField(ns, null);
        Assert.assertEquals(ns, obs.getFormFieldNamespace());
    }

    /**
     * @see Obs#getFormFieldPath()
     */
    @Test
    public void getFormFieldPath_shouldReturnNullIfThePathIsNotSpecified() throws Exception {
        Obs obs = new Obs();
        obs.setFormField("my ns", "");
        Assert.assertNull(obs.getFormFieldPath());
    }

    /**
     * @see Obs#getFormFieldPath()
     */
    @Test
    public void getFormFieldPath_shouldReturnTheCorrectPathForAFormFieldWithANamespace() throws Exception {
        final String ns = "my ns";
        final String path = "my path";
        Obs obs = new Obs();
        obs.setFormField(ns, path);
        Assert.assertEquals(path, obs.getFormFieldPath());
    }

    /**
     * @see Obs#getFormFieldPath()
     */
    @Test
    public void getFormFieldPath_shouldReturnThePathForAFormFieldThatHasNoNamespace() throws Exception {
        final String path = "my path";
        Obs obs = new Obs();
        obs.setFormField("", path);
        Assert.assertEquals(path, obs.getFormFieldPath());
    }

    /**
     * @see Obs#setFormField(String,String)
     */
    @Test(expected = APIException.class)
    public void setFormField_shouldRejectANamepaceAndPathCombinationLongerThanTheMaxLength() throws Exception {
        StringBuffer nsBuffer = new StringBuffer(125);
        for (int i = 0; i < 125; i++) {
            nsBuffer.append("n");
        }
        StringBuffer pathBuffer = new StringBuffer(130);
        for (int i = 0; i < 130; i++) {
            nsBuffer.append("p");
        }

        final String ns = nsBuffer.toString();
        final String path = pathBuffer.toString();
        Obs obs = new Obs();
        obs.setFormField(ns, path);
    }

    /**
     * @see Obs#setFormField(String,String)
     */
    @Test(expected = APIException.class)
    public void setFormField_shouldRejectANamepaceContainingTheSeparator() throws Exception {
        final String ns = "my ns" + FORM_NAMESPACE_PATH_SEPARATOR;
        Obs obs = new Obs();
        obs.setFormField(ns, "");
    }

    /**
     * @see Obs#setFormField(String,String)
     */
    @Test(expected = APIException.class)
    public void setFormField_shouldRejectAPathContainingTheSeparator() throws Exception {
        final String path = FORM_NAMESPACE_PATH_SEPARATOR + "my path";
        Obs obs = new Obs();
        obs.setFormField("", path);
    }

    /**
     * @see Obs#isDirty()
     */
    @Test
    public void isDirty_shouldReturnFalseWhenNoChangeHasBeenMade() throws Exception {
        assertFalse(new Obs().isDirty());

        //Should also work if setters are called with same values as the original
        Obs obs = createObs(2);
        obs.setGroupMembers(new LinkedHashSet<>());
        obs.getConcept().setDatatype(new ConceptDatatype());
        assertFalse(obs.isDirty());
        BeanUtils.copyProperties(obs, BeanUtils.cloneBean(obs));
        assertFalse(obs.isDirty());

        obs = createObs(null);
        obs.setGroupMembers(new LinkedHashSet<>());
        obs.getConcept().setDatatype(new ConceptDatatype());
        assertFalse(obs.isDirty());
        BeanUtils.copyProperties(obs, BeanUtils.cloneBean(obs));
        assertFalse(obs.isDirty());
    }

    /**
     * @see Obs#isDirty()
     */
    @Test
    public void isDirty_shouldReturnTrueWhenAnyImmutableFieldHasBeenChangedForEditedObs() throws Exception {
        Obs obs = createObs(2);
        assertFalse(obs.isDirty());
        updateImmutableFieldsAndAssert(obs, true);
    }

    /**
     * @see Obs#isDirty()
     */
    @Test
    public void isDirty_shouldReturnFalseWhenAnyImmutableFieldHasBeenChangedForNewObs() throws Exception {
        Obs obs = createObs(null);
        assertFalse(obs.isDirty());
        updateImmutableFieldsAndAssert(obs, false);
    }

    private void updateImmutableFieldsAndAssert(Obs obs, boolean assertion) throws Exception {
        //Set all fields to some random values via reflection
        List<Field> fields = Reflect.getAllFields(Obs.class);

        final Integer originalPersonId = obs.getPersonId();
        //call each setter and check that dirty has been set to true for each
        for (Field field : fields) {
            String fieldName = field.getName();
            if (IGNORED_FIELDS.contains(fieldName)) {
                continue;
            }

            if ("personId".equals(fieldName)) {
                //call setPersonId because it is protected so BeanUtils.setProperty won't work
                obs.setPersonId((Integer) generateValue(field, true));
            } else {
                BeanUtils.setProperty(obs, fieldName, generateValue(field, true));
            }
            assertEquals("Obs was not marked as dirty after changing: " + fieldName, obs.isDirty(), assertion);
            if ("person".equals(fieldName)) {
                //Because setPerson updates the personId we need to reset personId to its original value 
                //that matches that of person otherwise the test will fail for the personId field
                obs.setPersonId(originalPersonId);
            }

            //reset for next field
            resetObs(obs);
        }
    }

    /**
     * @see Obs#isDirty()
     */
    @Test
    public void isDirty_shouldReturnFalseWhenOnlyMutableFieldsAreChanged() throws Exception {
        Obs obs = new Obs();
        obs.setVoided(true);
        obs.setVoidedBy(new User(1000));
        obs.setVoidReason("some other reason");
        obs.setDateVoided(new Date());
        assertFalse(obs.isDirty());

        Obs obsEdited = new Obs(5);
        obsEdited.setVoided(true);
        obsEdited.setVoidedBy(new User(1000));
        obsEdited.setVoidReason("some other reason");
        obsEdited.setDateVoided(new Date());
        assertFalse(obsEdited.isDirty());
    }

    /**
     * @see Obs#isDirty()
     */
    @Test
    public void isDirty_shouldReturnTrueWhenAnImmutableFieldIsChangedFromANonNullToANullValueForEditedObs()
            throws Exception {
        Obs obs = createObs(2);
        assertNotNull(obs.getComment());
        obs.setComment(null);
        assertTrue(obs.isDirty());
    }

    /**
     * @see Obs#isDirty()
     */
    @Test
    public void isDirty_shouldReturnFalsWhenAnImmutableFieldIsChangedFromANonNullToANullValueForNewObs()
            throws Exception {
        Obs obs = createObs(null);
        assertNotNull(obs.getComment());
        obs.setComment(null);
        assertFalse(obs.isDirty());
    }

    /**
     * @see Obs#isDirty()
     */
    @Test
    public void isDirty_shouldReturnTrueWhenAnImmutableFieldIsChangedFromANullToANonNullValueInExistingObs()
            throws Exception {
        Obs obs = new Obs(5);
        assertNull(obs.getComment());
        obs.setComment("some non null value");
        assertTrue(obs.isDirty());
    }

    /**
     * @see Obs#isDirty()
     */
    @Test
    public void isDirty_shouldReturnFalseWhenAnImmutableFieldIsChangedFromANullToANonNullValueInNewObs()
            throws Exception {
        Obs obs = new Obs();
        assertNull(obs.getComment());
        obs.setComment("some non null value");
        assertFalse(obs.isDirty());
    }

    /**
     * @see Obs#setFormField(String,String)
     */
    @Test
    public void setFormField_shouldNotMarkTheObsAsDirtyWhenTheValueHasNotBeenChanged() throws Exception {
        Obs obs = createObs(3);
        obs.setFormField(obs.getFormFieldNamespace(), obs.getFormFieldPath());
        assertFalse(obs.isDirty());
    }

    /**
     * @see Obs#setFormField(String,String)
     */
    @Test
    public void setFormField_shouldMarkTheObsAsDirtyWhenTheValueHasBeenChanged() throws Exception {
        Obs obs = createObs(5);
        final String newNameSpace = "someNameSpace";
        final String newPath = "somePath";
        assertNotEquals(newPath, obs.getFormFieldNamespace());
        assertNotEquals(newNameSpace, obs.getFormFieldPath());
        obs.setFormField(newNameSpace, newPath);
        assertTrue(obs.isDirty());
    }

    /**
     * @see Obs#setFormField(String,String)
     */
    @Test
    public void setFormField_shouldMarkTheObsAsDirtyWhenTheValueIsChangedFromANonNullToANullValue()
            throws Exception {
        Obs obs = new Obs(2);
        obs.setFormField("someNameSpace", "somePath");
        resetObs(obs);
        assertFalse(obs.isDirty());
        assertNotNull(obs.getFormFieldNamespace());
        assertNotNull(obs.getFormFieldPath());
        obs.setFormField(null, null);
        assertTrue(obs.isDirty());
    }

    /**
     * @see Obs#setFormField(String,String)
     */
    @Test
    public void setFormField_shouldMarkTheObsAsDirtyWhenTheValueIsChangedFromANullToANonNullValue()
            throws Exception {
        Obs obs = new Obs(5);
        assertNull(obs.getFormFieldNamespace());
        assertNull(obs.getFormFieldPath());
        obs.setFormField("someNameSpace", "somePath");
        assertTrue(obs.isDirty());
    }

    /**
     * @see Obs#addGroupMember(Obs)
     */
    @Test
    public void addGroupMember_shouldReturnFalseWhenADuplicateObsIsAddedAsAMember() throws Exception {
        Obs obs = new Obs(2);
        Obs member = new Obs();
        obs.addGroupMember(member);
        assertFalse(obs.isDirty());
        resetObs(obs);
        obs.addGroupMember(member);
        assertFalse(obs.isDirty());
    }

    /**
     * @see Obs#addGroupMember(Obs)
     */
    @Test
    public void addGroupMember_shouldReturnFalseWhenADuplicateObsIsAddedAsAMemberToNewObs() throws Exception {
        Obs obs = new Obs();
        Obs member = new Obs();
        obs.addGroupMember(member);
        assertFalse(obs.isDirty());
        resetObs(obs);
        obs.addGroupMember(member);
        assertFalse(obs.isDirty());
    }

    /**
     * @see Obs#addGroupMember(Obs)
     */
    @Test
    public void addGroupMember_shouldReturnFalseWhenANewObsIsAddedAsAMember() throws Exception {
        Obs obs = new Obs(2);
        Obs member1 = new Obs();
        obs.addGroupMember(member1);
        assertFalse(obs.isDirty());
        resetObs(obs);
        Obs member2 = new Obs();
        obs.addGroupMember(member2);
        assertFalse(obs.isDirty());
    }

    /**
     * @see Obs#removeGroupMember(Obs)
     */
    @Test
    public void removeGroupMember_shouldReturnFalseWhenANonExistentObsIsRemoved() throws Exception {
        Obs obs = new Obs();
        obs.removeGroupMember(new Obs());
        assertFalse(obs.isDirty());
    }

    /**
     * @see Obs#removeGroupMember(Obs)
     */
    @Test
    public void removeGroupMember_shouldReturnDirtyFalseWhenAnObsIsRemoved() throws Exception {
        Obs obs = new Obs(2);
        Obs member = new Obs();
        obs.addGroupMember(member);
        assertFalse(obs.isDirty());
        resetObs(obs);
        obs.removeGroupMember(member);
        assertFalse(obs.isDirty());
    }

    /**
     * @see Obs#removeGroupMember(Obs)
     */
    @Test
    public void removeGroupMember_shouldReturnFalseForDirtyFlagWhenAnObsIsRemovedFromGroup() throws Exception {
        Obs obs = new Obs();
        Obs member = new Obs();
        obs.addGroupMember(member);
        assertFalse(obs.isDirty());
        resetObs(obs);
        obs.removeGroupMember(member);
        assertFalse(obs.isDirty());
    }

    /**
     * @see Obs#setGroupMembers(Set)
     */
    @Test
    public void setGroupMembers_shouldNotMarkTheExistingObsAsDirtyWhenTheSetIsChangedFromNullToANonEmptyOne()
            throws Exception {
        Obs obs = new Obs(5);
        assertNull(Obs.class.getDeclaredField("groupMembers").get(obs));
        Set<Obs> members = new HashSet<>();
        members.add(new Obs());
        obs.setGroupMembers(members);
        assertFalse(obs.isDirty());
    }

    /**
     * @see Obs#setGroupMembers(Set)
     */
    @Test
    public void setGroupMembers_shouldNotMarkNewObsAsDirtyWhenTheSetIsChangedFromNullToANonEmptyOne()
            throws Exception {
        Obs obs = new Obs();
        assertNull(Obs.class.getDeclaredField("groupMembers").get(obs));
        Set<Obs> members = new HashSet<>();
        members.add(new Obs());
        obs.setGroupMembers(members);
        assertFalse(obs.isDirty());
    }

    /**
     * @see Obs#setGroupMembers(Set)
     */
    @Test
    public void setGroupMembers_shouldNotMarkTheExistingObsAsDirtyWhenTheSetIsReplacedWithAnotherWithDifferentMembers()
            throws Exception {
        Obs obs = new Obs(2);
        Set<Obs> members1 = new HashSet<>();
        members1.add(new Obs());
        obs.setGroupMembers(members1);
        resetObs(obs);
        Set<Obs> members2 = new HashSet<>();
        members2.add(new Obs());
        obs.setGroupMembers(members2);
        assertFalse(obs.isDirty());
    }

    /**
     * @see Obs#setGroupMembers(Set)
     */
    @Test
    public void setGroupMembers_shouldNotMarkTheNewObsAsDirtyWhenTheSetIsReplacedWithAnotherWithDifferentMembers()
            throws Exception {
        Obs obs = new Obs();
        Set<Obs> members1 = new HashSet<>();
        members1.add(new Obs());
        obs.setGroupMembers(members1);
        assertFalse(obs.isDirty());
        Set<Obs> members2 = new HashSet<>();
        members2.add(new Obs());
        obs.setGroupMembers(members2);
        assertFalse(obs.isDirty());
    }

    /**
     * @see Obs#setGroupMembers(Set)
     */
    @Test
    public void setGroupMembers_shouldNotMarkTheObsAsDirtyWhenTheSetIsChangedFromNullToAnEmptyOne()
            throws Exception {
        Obs obs = new Obs();
        assertNull(Obs.class.getDeclaredField("groupMembers").get(obs));
        obs.setGroupMembers(new HashSet<>());
        assertFalse(obs.isDirty());
    }

    /**
     * @see Obs#setGroupMembers(Set)
     */
    @Test
    public void setGroupMembers_shouldNotMarkTheObsAsDirtyWhenTheSetIsReplacedWithAnotherWithSameMembers()
            throws Exception {
        Obs obs = new Obs();
        Obs o = new Obs();
        Set<Obs> members1 = new HashSet<>();
        members1.add(o);
        obs.setGroupMembers(members1);
        resetObs(obs);
        Set<Obs> members2 = new HashSet<>();
        members2.add(o);
        obs.setGroupMembers(members2);
        assertFalse(obs.isDirty());
    }

    /**
     * @see Obs#setObsDatetime(Date)
     */
    @Test
    public void setObsDateTime_shouldNotMarkTheObsAsDirtyWhenDateIsNotChangedAndExistingValueIsOfTimeStampType() {
        Obs obs = new Obs();
        Date date = new Date();
        Timestamp timestamp = new Timestamp(date.getTime());
        obs.setObsDatetime(timestamp);
        obs.setId(1);
        assertFalse(obs.isDirty());

        obs.setObsDatetime(date);

        assertFalse(obs.isDirty());
    }

    @Test
    public void shouldSetFinalStatusOnNewObsByDefault() throws Exception {
        Obs obs = new Obs();
        assertThat(obs.getStatus(), is(Obs.Status.FINAL));
    }

    @Test
    public void newInstance_shouldCopyMostFields() throws Exception {
        Obs obs = new Obs();
        obs.setStatus(Obs.Status.PRELIMINARY);
        obs.setInterpretation(Obs.Interpretation.LOW);
        obs.setConcept(new Concept());
        obs.setValueNumeric(1.2);

        Obs copy = Obs.newInstance(obs);

        // these fields are not copied
        assertThat(copy.getObsId(), nullValue());
        assertThat(copy.getUuid(), not(obs.getUuid()));

        // other fields are copied
        assertThat(copy.getConcept(), is(obs.getConcept()));
        assertThat(copy.getValueNumeric(), is(obs.getValueNumeric()));
        assertThat(copy.getStatus(), is(obs.getStatus()));
        assertThat(copy.getInterpretation(), is(obs.getInterpretation()));
        // TODO test that the rest of the fields are set
    }

    @Test
    public void shouldSupportInterpretationProperty() throws Exception {
        Obs obs = new Obs();
        assertThat(obs.getInterpretation(), nullValue());

        obs.setInterpretation(Obs.Interpretation.NORMAL);
        assertThat(obs.getInterpretation(), is(Obs.Interpretation.NORMAL));
    }
}