6 Replies Latest reply on Jul 14, 2006 12:13 PM by gavin.king

    Possible to write a rollback SeamTest?

    robjellinghaus

      OK, so pardon my lack of clue here. This is kind of a minor conundrum, but maybe someone knows the answer off the top of their head.

      I am writing a base class for versioned objects. These versioned objects can make (dynamically) immutable copies of themselves. I call these copies "snapshots".

      To ensure that my code doesn't modify the supposed-to-be-immutable snapshots, I have a @PreUpdate method in the base class which checks to see if the object is a snapshot (which is true iff its "replicatedChangeset" field is non-null). If it is, the @PreUpdate throws a SnapshotModifiedException.

      @Entity
      @Inheritance(strategy=InheritanceType.JOINED)
      public abstract class RepObject {
      ...
       private Changeset replicatedChangeset;
      ...
       @PreUpdate
       private void checkUpdate () {
       log.debug("Calling checkUpdate on " + this + "...");
       if (replicatedChangeset != null) {
       throw new SnapshotModifiedException(this);
       }
       }
      ...
      }

      SnapshotModifiedException:
      @ApplicationException(rollback=true)
      public class SnapshotModifiedException
       extends RuntimeException
      {
       RepObject snapshot;
      
       public SnapshotModifiedException(RepObject snapshot) {
       this.snapshot = snapshot;
       assert snapshot.getReplicatedChangeset() != null;
       }
      ...
      }


      This all works fine, actually.

      So now, naturally enough, I want to write a SeamTest that tries to modify a snapshot object, and then fails if the modification is successful, and passes if the modification blows up as expected. Here's my attempt:

      @Override
       protected void updateModelValues()
       {
       entityManager = (EntityManager) Component.getInstance("entityManager", true);
       changesetLog = (ChangesetLog) Contexts.getApplicationContext().get("changesetLog");
       }
      
       @Override
       protected void invokeApplication()
       {
       // ... get a list of snapshot objects in the List priorBlogPosts ...
       try {
       BlogPost prior1 = priorBlogPosts.get(0);
       prior1.setTitle("changedTitle"); // shouldn't change a snapshot!
      
       // trigger the @PreUpdate check
       entityManager.flush();
      
       assert false; // plan to die if we get this far; should get exception from the flush
       } catch (RuntimeException e) {
       log.info("Got expected exception from attempted snapshot update", e);
      
       // rethrow e... since it's marked as @ApplicationException(rollback=true), should roll back peacefully?!
       throw e;
       }
       }


      Now, this obviously doesn't work, because I rethrow the exception, so the test dies. (Note that the exception is not a SnapshotModifiedException, it's an InvocationTargetException wrapping a SnapshotModifiedException. Oh well.)

      But what should I do instead? If I swallow the exception, the test will still die, because it will try to flush again and fail. I think I want to do setRollbackOnly. But I don't actually know how to *do* setRollbackOnly from inside a SeamTest. The only syntax I see for it is (from the dvd example):
      @Resource
      SessionContext ctx;
      ...
      ctx.setRollbackOnly();


      But this can't be done from a SeamTest. If I just change my test to:
      @Resource
       SessionContext ctx;
      ...
       } catch (RuntimeException e) {
       log.info("Got expected exception from attempted snapshot update", e);
      
       ctx.setRollbackOnly();
       }

      Then I get a NullPointerException because SeamTests don't seem to support @Resource injection, so ctx is null. So how do I get a hold of a javax.ejb.SessionContext in my SeamTest to make it roll back peacefully?

      Basically I'm between a rock and a hard place:
      1) If I swallow the exception, then the test blows up when it tries to commit, because it flushes again and hits the exception again.
      2) If I rethrow the exception, then the test blows up because an exception thrown out a test causes a test failure.
      3) I can't do "ctx.setRollbackOnly();" in my catch clause because I don't know how to inject or otherwise obtain a javax.ejb.SessionContext in my test.

      Obviously I could just punt on this whole test, but this is really important functionality in my little framework and I want a test for it :-)

      Clues, anyone? There's probably some dead simple way to get the SessionContext from inside a SeamTest... I just can't imagine what it is!

      Thanks!
      Cheers,
      Rob

        • 1. Re: Possible to write a rollback SeamTest?
          robjellinghaus

          I hate to bump my own post, but... well... I'm pretty sure Gavin could answer this in 0.005 seconds, so I'm just giving it another shot :-)

          • 2. Re: Possible to write a rollback SeamTest?
            gavin.king

            I guess the problem is that E-EJB is not supporting injection of the SessionContext. Well, that is a bug/limitation of E-EJB3 that needs to be fixed, so hassle Bill ;-)

            Alternatively, you can try calling Transactions.getTransactionManager().setRollbackOnly(), but that is a hack, obviously...

            • 3. Re: Possible to write a rollback SeamTest?
              robjellinghaus

              Hm. No, actually, that's not the problem. I mean, I was sticking that @Resource tag into a SeamTest instance. Why should E-EJB inject into a SeamTest? Nothing else does....

              What I wanted was the equivalent of "Component.getInstance("sessionContext")" for the EJB session context, that I could use from within the SeamTest. That would seem cleaner than your getTransactionManager() suggestion. But lacking that, I did this instead:

              @Name("testAction")
              @Scope(ScopeType.CONVERSATION)
              public class TestAction implements Serializable
              {
               private static final Logger log = Logger.getLogger(TestAction.class);
              
               @In(create=true)
               private transient EntityManager entityManager;
              
               @Resource
               private SessionContext ctx;
              
               /**
               * Alter the passed-in BlogPost, then flush. If failure, then setRollbackOnly and
               * rethrow.
               */
               public void alterBlogPost (BlogPost bp) {
               try {
               bp.setTitle("newTestTitle");
              
               entityManager.flush();
               } catch (RuntimeException e) {
               log.debug("Could not flush changed blog post", e);
               ctx.setRollbackOnly(); // this line turns out to be optional!!!
               throw e;
               }
               }
              }


              Then changed my test to:
              testAction = (TestAction)Component.getInstance("testAction", true);
               BlogPost prior1 = priorBlogPosts.get(0);
              
               try {
               testAction.alterBlogPost(prior1);
              
               // should throw a RuntimeException, so shouldn't get here
               assert false;
               } catch (RuntimeException e) {
               log.debug("Caught expected exception", e);
               }


              Works great :-) The sessionContext gets @Resource-injected into my TestAction, which flushes, blows up, and does setRollbackOnly; then it rethrows to my test case, which catches and ignores the exception.

              Result: my test case passes just fine, and I avoid having to use your hacky workaround. But thanks for mentioning it anyway, you never know when you might need something like that :-D

              The WEIRD thing, though, is that even if you leave out the ctx.setRollbackOnly() line, the action still rolls back when the runtime exception gets thrown out of it! Is this expected? It surprised me, but actually it's kind of cool. Is this standard Seam behavior or standard EJB3 behavior, or something else?

              You never know what a test will teach you....
              Cheers!
              Rob

              • 4. Re: Possible to write a rollback SeamTest?
                gavin.king

                There is no EJB sessioncontext outside the scope of an EJB ;-)

                • 5. Re: Possible to write a rollback SeamTest?
                  robjellinghaus

                  Yer, once I realized that, the TestAction strategy was obvious :-)

                  What about the auto-rollback? Is that standard EJB3?

                  • 6. Re: Possible to write a rollback SeamTest?
                    gavin.king

                    Of course. Any runtime exceptions not annotated as an @ApplicationException is defined as a "system exception" by the EJB spec and rolls back the transaction when it propagates out of the bean.