Re: Simplify (junit) testing in cayenne

From: Mike Kienenberger (mkienen..mail.com)
Date: Thu Sep 06 2007 - 18:46:34 EDT

  • Next message: Andrus Adamchik: "Apache Cayenne September Board Report"

    Well, if you're going to use the interface route, then you'll need to
    put the shared behavior in either a base superclass or in some kind of
    helper class.

    With inheritance, I haven't found the perfect solution.

    Without inheritance, it's pretty easy to use a base superclass.

    Here's one possible way (and yes, the structure gets a little
    convoluted, but most of the files are generated):

    For implementation hierarchy, it'd be this:
    BaseEntity
    _AbstractSharedEntityBehavior
    AbstractSharedEntityBehavior
    _Entity
    Entity

    You'd have a similiar hierarchy for the Interfaces.

    You'd generate both the _Entity and the Entity classes specific to
    either Cayenne or Mock implementations.

    You'd put derived behaviors common to any Entity implementation in
    AbstractSharedEntityBehavior.

    And the trick is that you'd generate _AbstractSharedEntityBehavior to
    pre-declare the generic entity accessors.

    Ie,

    abstract public Type1 getEntityAttribute1();
    abstract public void setEntityAttribute1(Type1 value);

    abstract public Type2 getEntityAttribute1();
    abstract public void setEntityAttribute1(Type2 value);

    However, as you point out, it's also possible to use a subclass of
    Entity for your MockEntity. I'm using this in my current project as
    it's been around from before I started using interfaces for entities,
    and adding interfaces into it at this point would be awkward.

    The key is to make sure that youre MockEntities override every single
    one of the generated attribute getters/setters.

    As for dealing with relationships, you can handle this by creating
    your own versions of Cayenne's setToOneTarget and
    addTo/removeFromTarget methods.

    For providing metadata, I simply generate a static array giving the
    relationship names.
    All of this is in a base entity class for my interface-based mock objects.

    I admit that I haven't dealt with the issue yet for my non-interface
    objects. The easiest solution will be to simply generate the support
    methods in each of the _MockEntity classes, I suspect.

    Each generated mock entity class would have something like this
    generated to represent object metadata. I don't see which of my
    templates generates them off-hand, but it should be easy enough for
    you to figure out how to do it. Following is what the base entity
    file looks like. I've tried to remove irrelevent code.
    Interestingly enough, this would be the same approach you could use to
    make vanilla JPA support setting both sides of a relationship.
    ======================================================
        static final String forwardRelationships[] = new String[] {
                "cycle",
                "routeStatusType",
                "serviceList",
                "***END***"
        };

        static final String reverseRelationships[] = new String[] {
                "routeList",
                "routeList",
                "route",
                "***END***"
        };

        static final Boolean reverseRelationshipIsToManys[] = new Boolean[] {
                Boolean.TRUE,
                Boolean.TRUE,
                Boolean.FALSE,
                null
        };

        protected void populateRelationshipMapsFromArrays()
        {
                reverseRelationshipsForForwardRelationships =
    convertArraysToMap(forwardRelationships, reverseRelationships);
                reverseRelationshipTypeForForwardRelationships =
    convertArraysToMap(forwardRelationships,
    reverseRelationshipIsToManys);
        }
    ======================================================

    ======================================================
    import java.util.ArrayList;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;

    abstract public class BaseEntityStub extends BaseStub implements BaseEntity {

            protected static boolean safeEquals(Object A, Object B) {
            return (A == null) ? (B == null) : A.equals(B);
        }

        transient protected Map reverseRelationshipsForForwardRelationships;
        transient protected Map reverseRelationshipTypeForForwardRelationships;

        protected Map map = new HashMap();

        protected Object primaryKey;
        protected boolean isTemporaryPrimaryKey = true;

        public Object getPrimaryKey()
        {
            return this.primaryKey;
        }
        public void setPrimaryKey(Object primaryKey)
        {
            this.primaryKey = primaryKey;
        }
        public boolean isTemporaryPrimaryKey() {
                    return isTemporaryPrimaryKey;
            }
            public void setTemporaryPrimaryKey(boolean isTemporaryPrimaryKey) {
                    this.isTemporaryPrimaryKey = isTemporaryPrimaryKey;
            }

        /**
         * Returns a value of the property identified by propName.
    Resolves faults if needed.
         * This method can safely be used instead of or in addition to the
    auto-generated
         * property accessors in subclasses of CayenneDataObject.
         */
        public Object readProperty(String propName)
        {
            Object o = map.get(propName);
            return o;
        }

        /**
         * Sets the property with the name propName to the new value val.
    Resolves faults if
         * needed This method can safely be used instead of or in addition to the
         * auto-generated property modifiers in subclasses of CayenneDataObject.
         */
        public void writeProperty(String propName, Object value)
        {
            // Oracle can't handle empty strings, and JSF generates them
    instead of empty strings.
            if ( (value instanceof String) && (0 == ((String)value).length()) )
                map.put(propName, null);
            else map.put(propName, value);
        }

        private void checkObjectType(BaseEntity val)
        {
            if (null == val)
            {
                return;
            }

                if (false == val instanceof BaseEntityStub)
                {
                        throw new RuntimeException("BaseEntity class " +
    val.getClass().getName() + " is not of type BaseEntityStub");
                }
        }

            public void setToOneTarget(String relName, BaseEntity val, boolean setReverse)
        {
            BaseEntity oldVal = (BaseEntity)map.get(relName);
            // skip initial assignment if nothing changed
            if (setReverse && safeEquals(oldVal, val))
            {
                return;
            }

            checkObjectType(val);

            if (setReverse)
            {
                if (null != oldVal)
                {
                    unsetReverseRelationship(relName, oldVal);
                }
                if (null != val)
                {
                    setReverseRelationship(relName, val);
                }
            }

            map.put(relName, val);
        }
            
        public void addToManyTarget(String relName, BaseEntity val,
    boolean setReverse)
        {
            checkObjectType(val);

            if (setReverse)
            {
                setReverseRelationship(relName, val);
            }

            List targetList = (List)map.get(relName);
            if (null == targetList)
            {
                targetList = new ArrayList();
                map.put(relName, targetList);
            }

            if (targetList.contains(val))
            {
                throw new RuntimeException("val " + val + " " +
    val.getPrimaryKey() + " is already in " + relName);
            }
            targetList.add(val);
        }

        public void removeToManyTarget(String relName, BaseEntity val,
    boolean setReverse)
        {
            checkObjectType(val);

            if (setReverse)
            {
                unsetReverseRelationship(relName, val);
            }

            List targetList = (List)map.get(relName);
            if (null == targetList)
            {
                throw new RuntimeException("Unsupported operation");
            }

            targetList.remove(val);
        }

        abstract protected void populateRelationshipMapsFromArrays();

        private void setReverseRelationship(String relName, BaseEntity val) {
            if (null == val)
            {
                throw new NullPointerException("val is null");
            }

            if (null == reverseRelationshipsForForwardRelationships)
            {
                populateRelationshipMapsFromArrays();
            }
            String reverseName =
    (String)reverseRelationshipsForForwardRelationships.get(relName);
                if (null != reverseName)
            {
                Boolean reverseToMany =
    (Boolean)reverseRelationshipTypeForForwardRelationships.get(relName);
                if (null != reverseToMany)
                {
                    if (reverseToMany.booleanValue())
                    {
                        ((BaseEntityStub)val).addToManyTarget(reverseName,
    this, false);
                    }
                    else
                    {
                        ((BaseEntityStub)val).setToOneTarget(reverseName,
    this, false);
                    }
                }
            }
            }

        private void unsetReverseRelationship(String relName, BaseEntity val) {
            if (null == val)
            {
                throw new NullPointerException("val is null");
            }

            if (null == reverseRelationshipsForForwardRelationships)
            {
                populateRelationshipMapsFromArrays();
            }
            String reverseName =
    (String)reverseRelationshipsForForwardRelationships.get(relName);
            if (null != reverseName)
            {
                Boolean reverseToMany =
    (Boolean)reverseRelationshipTypeForForwardRelationships.get(relName);
                if (null != reverseToMany)
                {
                    if (reverseToMany.booleanValue())
                    {

    ((BaseEntityStub)val).removeToManyTarget(reverseName, this, false);
                    }
                    else
                    {
                        ((BaseEntityStub)val).setToOneTarget(reverseName,
    null, false);
                    }
                }
            }
            }

        protected String nullCapableString(Object object) {
            return null == object ? "<null>" : object.toString();
        }

        protected boolean convertBooleanToPrimitive(Boolean value, boolean
    valueIfNull) {
            if (null == value) return valueIfNull;

            return value.booleanValue();
        }

    //////////////////////////////////////////////////////////////////////////////////
        /* ====================================================================
         *
         * The ObjectStyle Group Software License, version 1.1
         * ObjectStyle Group - http://objectstyle.org/
         *
         * Copyright (c) 2002-2005, Andrei (Andrus) Adamchik and individual authors
         * of the software. All rights reserved.
         *
         * Redistribution and use in source and binary forms, with or without
         * modification, are permitted provided that the following conditions
         * are met:
         *
         * 1. Redistributions of source code must retain the above copyright
         * notice, this list of conditions and the following disclaimer.
         *
         * 2. Redistributions in binary form must reproduce the above copyright
         * notice, this list of conditions and the following disclaimer in
         * the documentation and/or other materials provided with the
         * distribution.
         *
         * 3. The end-user documentation included with the redistribution, if any,
         * must include the following acknowlegement:
         * "This product includes software developed by independent contributors
         * and hosted on ObjectStyle Group web site (http://objectstyle.org/)."
         * Alternately, this acknowlegement may appear in the software itself,
         * if and wherever such third-party acknowlegements normally appear.
         *
         * 4. The names "ObjectStyle Group" and "Cayenne" must not be used
    to endorse
         * or promote products derived from this software without prior written
         * permission. For written permission, email
         * "andrus at objectstyle dot org".
         *
         * 5. Products derived from this software may not be called "ObjectStyle"
         * or "Cayenne", nor may "ObjectStyle" or "Cayenne" appear in their
         * names without prior written permission.
         *
         * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
         * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
         * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
         * DISCLAIMED. IN NO EVENT SHALL THE OBJECTSTYLE GROUP OR
         * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
         * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
         * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
         * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
         * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
         * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
         * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
         * SUCH DAMAGE.
         * ====================================================================
         *
         * This software consists of voluntary contributions made by many
         * individuals and hosted on ObjectStyle Group web site. For more
         * information on the ObjectStyle Group, please see
         * <http://objectstyle.org/>.
         */
        /**
         * Creates a mutable map out of two arrays with keys and values.
         *
         *..ince 1.2
         */
        public static Map convertArraysToMap(Object[] keys, Object[] values) {
            int keysSize = (keys != null) ? keys.length : 0;
            int valuesSize = (values != null) ? values.length : 0;

            if (keysSize == 0 && valuesSize == 0) {
                // return mutable map
                return new HashMap();
            }

            if (keysSize != valuesSize) {
                throw new IllegalArgumentException(
                        "The number of keys doesn't match the number of values.");
            }

            Map map = new HashMap();
            for (int i = 0; i < keysSize; i++) {
                if (null != values[i])
                    map.put(keys[i], values[i]);
            }

            return map;
        }

    }
    ======================================================

    On 9/6/07, Marcin Skladaniec <marci..sh.com.au> wrote:
    > Hi
    > Thank you for hints, they are useful. I'll try to use some of them,
    > but my main concern is to test the client in ROP setup. As Mike and
    > Tore mentioned, there are two levels of testing junit and integration
    > test. Testing ROP application brings another dimension, with need to
    > test client and server separately and together.
    > Right now it is impossible to test client without running a server,
    > and in our case if we want the client to connect we need to fake
    > login process, start some side services etc. We have succeeded with
    > those tests, but they are ugly and very limited.
    >
    > About testing MockDAO, or server side: Mike you mention that you use
    > interfaces to define entities, and than implement that interface in
    > MockEntities. I don't know if I get you right, but than you are not
    > testing code in your Entity, but the implementation in the mocked-up
    > one. I cannot see how this might work for testing lifecycle events or
    > complex validation.
    >
    > In my current setup i subclass the entity with mockupentity and just
    > by overriding getObjectId I have an almost fully working entity. The
    > problem I have is that I cannot make them able to set relationships.
    > I get a nullpointer in setReverseRelationship. I was trying to mock
    > the entityresolver based on cayenne junit tests, but those tests
    > subclass private classes, so I cannot use this approach. How could I
    > overcome that ?
    >
    > I still think that cayenne could include a simplified junit testing
    > API for different deployment options ("classic cayenne", ROP, web)
    >
    > Thanks
    > Marcin
    >
    >
    > On 07/09/2007, at 5:58 AM, Mike Kienenberger wrote:
    >
    > > Hmm. I got rid of the cleanup threads easily enough, but it didn't
    > > really affect the memory usage. That's what I get for not using a
    > > profiler. At least things are easier to read in the debugger
    > > without all of those extra threads :-)
    > >
    > > On 9/6/07, Mike Kienenberger <mkienen..mail.com> wrote:
    > >>> Why do you want all of that mocking stuff?
    > >>
    > >> Because what you described is an integration test, not a unit
    > >> test :-)
    > >>
    > >> I have both -- the integration tests running against an in-memory
    > >> hsqldb instance and unit tests running without needing to hit a
    > >> database. However, running my integration tests still takes around
    > >> 15 minutes.
    > >>
    > >> Configuring efficient integration tests is much more tricky. You
    > >> either recreate everything which makes things take another order of
    > >> magnitude to run, or you try to "clean up" the important record
    > >> tables
    > >> in setup.
    > >>
    > >> Configuring a unit test using mock objects is much faster -- you just
    > >> configure your MockDAO to respond to expected method calls and
    > >> poll it
    > >> afterward to see if the expected method calls were called correctly.
    > >>
    > >> My other issue with integration tests is that I'm using Cayenne
    > >> 1.1.4,
    > >> and every setup() call to initialize Configuration creates a new
    > >> PoolManagerCleanup thread which won't time out for 60 seconds. That
    > >> makes my integration tests memory intensive -- currently about 1.4Gb
    > >> of memory is required to run some 650+ tests. I just spent a couple
    > >> of hours trying to figure out a nicer way to deal with this, but I
    > >> haven't done so yet. I probably will need to subclass
    > >> DriverDataSourceFactory and PoolManager.
    > >>
    > >> On 9/6/07, Tore Halset <halse..vv.ntnu.no> wrote:
    > >>> Hello.
    > >>>
    > >>> My junit setup creates a database with all its tables and basic
    > >>> schema and then all of the cayenne-related tests operate on that
    > >>> temporary db. It is pretty much like cayenne junit tests before the
    > >>> move to maven, but a bit simpler. The junit tests are started when a
    > >>> developer wants to and periodically on our development server. It
    > >>> tests everything with both PostgreSQL and Derby.
    > >>>
    > >>> Why do you want all of that mocking stuff?
    > >>>
    > >>> - Tore.
    > >>>
    > >>> On Sep 5, 2007, at 17:10, Mike Kienenberger wrote:
    > >>>
    > >>>> There's a few different ways to look at this.
    > >>>>
    > >>>> It's true that Cayenne doesn't easily support application unit
    > >>>> testing.
    > >>>>
    > >>>> However, I'm not sure it's entirely appropriate to do so.
    > >>>>
    > >>>> What I do is use a DAO pattern for my database operations. Then I
    > >>>> mock up a DAO rather than the entire database layer. It's far
    > >>>> easier
    > >>>> to mock up "myTestingDAO.findUserByUserName()" than to mock up
    > >>>> SelectQuery, DataNode, DataMap, DataContext, etc.
    > >>>>
    > >>>> I haven't quite reached this point in all of my projects, but my
    > >>>> goal
    > >>>> is to generate Interfaces for each of my Entities. If I have a
    > >>>> User
    > >>>> entity, then I create a User interface and use that exclusively
    > >>>> outside of my DAO. The DAO returns User interface objects rather
    > >>>> than User data objects.
    > >>>>
    > >>>> This then allows me to create a MockUser simply by implementing the
    > >>>> User interface. For projects where I don't have entity
    > >>>> interfaces,
    > >>>> I subclass the User DataObject instead. This isn't quite as
    > >>>> clean or
    > >>>> workable, but it does help so long as you override every method.
    > >>>>
    > >>>> For creating Mock objects, I use the cayenne code generator the
    > >>>> same
    > >>>> way I use it for the DataObjects.
    > >>>>
    > >>>> I'm finding that there are still some places where integration
    > >>>> testing
    > >>>> is necessary to catch problems. In Cayenne 1.1.4, I've had an
    > >>>> issue
    > >>>> where I tried to create a local copy of a modified or a transient
    > >>>> object and then commit an object with a relationship to it -- those
    > >>>> kinds of problems can only be detected when using the real database
    > >>>> layer unless your mock layer knows the quirks.
    > >>>>
    > >>>>
    > >>>>
    > >>>> On 9/4/07, Marcin Skladaniec (JIRA) <de..ayenne.apache.org> wrote:
    > >>>>> Simplify (junit) testing in cayenne
    > >>>>> -----------------------------------
    > >>>>>
    > >>>>> Key: CAY-862
    > >>>>> URL: https://issues.apache.org/cayenne/browse/
    > >>>>> CAY-862
    > >>>>> Project: Cayenne
    > >>>>> Issue Type: New Feature
    > >>>>> Components: Cayenne Core Library
    > >>>>> Affects Versions: 3.0
    > >>>>> Environment: All
    > >>>>> Reporter: Marcin Skladaniec
    > >>>>> Assignee: Andrus Adamchik
    > >>>>>
    > >>>>>
    > >>>>> Junit tests are becoming very important once the project reaches a
    > >>>>> certain point. Cayenne has dozens of junit tests but writing a
    > >>>>> junit test for cayenne based application is not easy at all.
    > >>>>>
    > >>>>> For me the main trouble is when there is no need to fetch or
    > >>>>> commit something (like testing GUI or lifecycle events). I tried
    > >>>>> to reproduce the tests found in cayenne,but always ended up with
    > >>>>> problems with mocking up the context, datachannel,
    > >>>>> entityResolver, altering the configuration to point to different
    > >>>>> db etc.
    > >>>>>
    > >>>>> To solve that my idea was that one might specify a package in the
    > >>>>> CayenneModeler, this package will than be populated with generated
    > >>>>> a set of _MockupXXX extends XXX (like _MockupArtist extends
    > >>>>> Artist, _MockupPainting extends Painting etc.) and a
    > >>>>> MockupDataContext etc. There could be a second set of
    > >>>>> _MockupEntities for ROP client.
    > >>>>>
    > >>>>> Another thing is to specify the testing environment with ease. I
    > >>>>> think there should be also a possibility to create a "testing"
    > >>>>> DataNode pointing to a different database than deployment, and for
    > >>>>> the DataMap could be related to the real or testing DataNode at
    > >>>>> the same time. To choose the testing environment a system param
    > >>>>> like -Dcayenne.testing=TRUE could be utilised.
    > >>>>> I might have missed something here: is there a simply way of
    > >>>>> having two DataNodes for one DataMap ?
    > >>>>>
    > >>>>> I think that simplified testcase writing feature would be a great
    > >>>>> advantage for Cayenne over any other ORM.
    > >>>>>
    > >>>>> --
    > >>>>> This message is automatically generated by JIRA.
    > >>>>> -
    > >>>>> You can reply to this email to add a comment to the issue online.
    > >>>>>
    > >>>>>
    > >>>>
    > >>>
    > >>>
    > >>
    >
    >



    This archive was generated by hypermail 2.0.0 : Thu Sep 06 2007 - 18:47:11 EDT