diff --git a/org.springframework.aspects/.classpath b/org.springframework.aspects/.classpath index 031c0a9a59d..0157912a92e 100644 --- a/org.springframework.aspects/.classpath +++ b/org.springframework.aspects/.classpath @@ -1,19 +1,20 @@ - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + diff --git a/org.springframework.aspects/ivy.xml b/org.springframework.aspects/ivy.xml index 415b3dc3dd2..30108ea69c8 100644 --- a/org.springframework.aspects/ivy.xml +++ b/org.springframework.aspects/ivy.xml @@ -29,7 +29,8 @@ - + + diff --git a/org.springframework.aspects/src/main/java/org/springframework/mock/static_mock/AbstractMethodMockingControl.aj b/org.springframework.aspects/src/main/java/org/springframework/mock/static_mock/AbstractMethodMockingControl.aj new file mode 100644 index 00000000000..ffdaa6b047a --- /dev/null +++ b/org.springframework.aspects/src/main/java/org/springframework/mock/static_mock/AbstractMethodMockingControl.aj @@ -0,0 +1,182 @@ +package org.springframework.mock.static_mock; + +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; + +/** + * Abstract aspect to enable mocking of methods picked out by a pointcut. + * Sub-aspects must define the mockStaticsTestMethod() pointcut to + * indicate call stacks when mocking should be triggered, and the + * methodToMock() pointcut to pick out a method invocations to mock. + * + * @author Rod Johnson + * @author Ramnivas Laddad + * + */ +public abstract aspect AbstractMethodMockingControl percflow(mockStaticsTestMethod()) { + + protected abstract pointcut mockStaticsTestMethod(); + + protected abstract pointcut methodToMock(); + + private boolean recording = true; + + static enum CallResponse { nothing, return_, throw_ }; + + // Represents a list of expected calls to static entity methods + // Public to allow inserted code to access: is this normal?? + public class Expectations { + + // Represents an expected call to a static entity method + private class Call { + private final String signature; + private final Object[] args; + + private Object responseObject; // return value or throwable + private CallResponse responseType = CallResponse.nothing; + + public Call(String name, Object[] args) { + this.signature = name; + this.args = args; + } + + public boolean hasResponseSpecified() { + return responseType != CallResponse.nothing; + } + + public void setReturnVal(Object retVal) { + this.responseObject = retVal; + responseType = CallResponse.return_; + } + + public void setThrow(Throwable throwable) { + this.responseObject = throwable; + responseType = CallResponse.throw_; + } + + public Object returnValue(String lastSig, Object[] args) { + checkSignature(lastSig, args); + return responseObject; + } + + public Object throwException(String lastSig, Object[] args) { + checkSignature(lastSig, args); + throw (RuntimeException)responseObject; + } + + private void checkSignature(String lastSig, Object[] args) { + if (!signature.equals(lastSig)) { + throw new IllegalArgumentException("Signature doesn't match"); + } + if (!Arrays.equals(this.args, args)) { + throw new IllegalArgumentException("Arguments don't match"); + } + } + } + + private List calls = new LinkedList(); + + // Calls already verified + private int verified; + + public void verify() { + if (verified != calls.size()) { + throw new IllegalStateException("Expected " + calls.size() + + " calls, received " + verified); + } + } + + /** + * Validate the call and provide the expected return value + * @param lastSig + * @param args + * @return + */ + public Object respond(String lastSig, Object[] args) { + Call call = nextCall(); + CallResponse responseType = call.responseType; + if (responseType == CallResponse.return_) { + return call.returnValue(lastSig, args); + } else if(responseType == CallResponse.throw_) { + return (RuntimeException)call.throwException(lastSig, args); + } else if(responseType == CallResponse.nothing) { + // do nothing + } + throw new IllegalStateException("Behavior of " + call + " not specified"); + } + + private Call nextCall() { + if (verified > calls.size() - 1) { + throw new IllegalStateException("Expected " + calls.size() + + " calls, received " + verified); + } + return calls.get(verified++); + } + + public void expectCall(String lastSig, Object lastArgs[]) { + Call call = new Call(lastSig, lastArgs); + calls.add(call); + } + + public boolean hasCalls() { + return !calls.isEmpty(); + } + + public void expectReturn(Object retVal) { + Call call = calls.get(calls.size() - 1); + if (call.hasResponseSpecified()) { + throw new IllegalStateException("No static method invoked before setting return value"); + } + call.setReturnVal(retVal); + } + + public void expectThrow(Throwable throwable) { + Call call = calls.get(calls.size() - 1); + if (call.hasResponseSpecified()) { + throw new IllegalStateException("No static method invoked before setting throwable"); + } + call.setThrow(throwable); + } + } + + private Expectations expectations = new Expectations(); + + after() returning : mockStaticsTestMethod() { + if (recording && (expectations.hasCalls())) { + throw new IllegalStateException( + "Calls recorded, yet playback state never reached: Create expectations then call " + + this.getClass().getSimpleName() + ".playback()"); + } + expectations.verify(); + } + + Object around() : methodToMock() { + if (recording) { + expectations.expectCall(thisJoinPointStaticPart.toLongString(), thisJoinPoint.getArgs()); + // Return value doesn't matter + return null; + } else { + return expectations.respond(thisJoinPointStaticPart.toLongString(), thisJoinPoint.getArgs()); + } + } + + public void expectReturnInternal(Object retVal) { + if (!recording) { + throw new IllegalStateException("Not recording: Cannot set return value"); + } + expectations.expectReturn(retVal); + } + + public void expectThrowInternal(Throwable throwable) { + if (!recording) { + throw new IllegalStateException("Not recording: Cannot set throwable value"); + } + expectations.expectThrow(throwable); + } + + public void playbackInternal() { + recording = false; + } + +} diff --git a/org.springframework.aspects/src/main/java/org/springframework/mock/static_mock/JUnitStaticEntityMockingControl.aj b/org.springframework.aspects/src/main/java/org/springframework/mock/static_mock/JUnitStaticEntityMockingControl.aj new file mode 100644 index 00000000000..c732d7800be --- /dev/null +++ b/org.springframework.aspects/src/main/java/org/springframework/mock/static_mock/JUnitStaticEntityMockingControl.aj @@ -0,0 +1,57 @@ +package org.springframework.mock.static_mock; + +import javax.persistence.Entity; +import org.junit.Test; + +/** + * JUnit-specific aspect to use in test build to enable mocking static methods + * on Entity classes, as used by Roo for finders. + *
+ * Mocking will occur in JUnit tests where the Test class is annotated with the + * @MockStaticEntityMethods annotation, in the call stack of each + * JUnit @Test method. + *
+ * Also provides static methods to simplify the programming model for + * entering playback mode and setting expected return values. + *
+ * Usage:
    + *
  1. Annotate a JUnit test class with @MockStaticEntityMethods. + *
  2. In each @Test method, JUnitMockControl will begin in recording mode. + * Invoke static methods on Entity classes, with each recording-mode invocation + * being followed by an invocation to the static expectReturn() or expectThrow() + * method on JUnitMockControl. + *
  3. Invoke the static JUnitMockControl.playback() method. + *
  4. Call the code you wish to test that uses the static methods. Verification will + * occur automatically. + *
+ * + * @see MockStaticEntityMethods + * + * @author Rod Johnson + * @author Ramnivas Laddad + * + */ +public aspect JUnitStaticEntityMockingControl extends AbstractMethodMockingControl { + + /** + * Stop recording mock calls and enter playback state + */ + public static void playback() { + JUnitStaticEntityMockingControl.aspectOf().playbackInternal(); + } + + public static void expectReturn(Object retVal) { + JUnitStaticEntityMockingControl.aspectOf().expectReturnInternal(retVal); + } + + public static void expectThrow(Throwable throwable) { + JUnitStaticEntityMockingControl.aspectOf().expectThrowInternal(throwable); + } + + // Only matches directly annotated @Test methods, to allow methods in + // @MockStatics classes to invoke each other without resetting the mocking environment + protected pointcut mockStaticsTestMethod() : execution(@Test public * (@MockStaticEntityMethods *).*(..)); + + protected pointcut methodToMock() : execution(public static * (@Entity *).*(..)); + +} diff --git a/org.springframework.aspects/src/main/java/org/springframework/mock/static_mock/MockStaticEntityMethods.java b/org.springframework.aspects/src/main/java/org/springframework/mock/static_mock/MockStaticEntityMethods.java new file mode 100644 index 00000000000..b2efe668d2a --- /dev/null +++ b/org.springframework.aspects/src/main/java/org/springframework/mock/static_mock/MockStaticEntityMethods.java @@ -0,0 +1,21 @@ +package org.springframework.mock.static_mock; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to indicate a test class for whose @Test methods + * static methods on Entity classes should be mocked. + * + * @see AbstractMethodMockingControl + * + * @author Rod Johnson + * + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface MockStaticEntityMethods { + +} diff --git a/org.springframework.aspects/src/test/java/org/springframework/mock/static_mock/JUnitStaticEntityMockingControlTest.java b/org.springframework.aspects/src/test/java/org/springframework/mock/static_mock/JUnitStaticEntityMockingControlTest.java new file mode 100644 index 00000000000..34fa82b7fcf --- /dev/null +++ b/org.springframework.aspects/src/test/java/org/springframework/mock/static_mock/JUnitStaticEntityMockingControlTest.java @@ -0,0 +1,209 @@ +/* + * Copyright 2009 SpringSource Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.mock.static_mock; + +import java.rmi.RemoteException; + +import javax.persistence.PersistenceException; + +import junit.framework.Assert; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.springframework.remoting.RemoteAccessException; + + +/** + * Test for static entity mocking framework. + * @author Rod Johnson + * @author Ramnivas Laddad + * + */ +@MockStaticEntityMethods +@RunWith(JUnit4.class) +public class JUnitStaticEntityMockingControlTest { + + @Test + public void testNoArgIntReturn() { + int expectedCount = 13; + Person.countPeople(); + JUnitStaticEntityMockingControl.expectReturn(expectedCount); + JUnitStaticEntityMockingControl.playback(); + Assert.assertEquals(expectedCount, Person.countPeople()); + } + + @Test(expected=PersistenceException.class) + public void testNoArgThrows() { + Person.countPeople(); + JUnitStaticEntityMockingControl.expectThrow(new PersistenceException()); + JUnitStaticEntityMockingControl.playback(); + Person.countPeople(); + } + + @Test + public void testArgMethodMatches() { + long id = 13; + Person found = new Person(); + Person.findPerson(id); + JUnitStaticEntityMockingControl.expectReturn(found); + JUnitStaticEntityMockingControl.playback(); + Assert.assertEquals(found, Person.findPerson(id)); + } + + + @Test + public void testLongSeriesOfCalls() { + long id1 = 13; + long id2 = 24; + Person found1 = new Person(); + Person.findPerson(id1); + JUnitStaticEntityMockingControl.expectReturn(found1); + Person found2 = new Person(); + Person.findPerson(id2); + JUnitStaticEntityMockingControl.expectReturn(found2); + Person.findPerson(id1); + JUnitStaticEntityMockingControl.expectReturn(found1); + Person.countPeople(); + JUnitStaticEntityMockingControl.expectReturn(0); + JUnitStaticEntityMockingControl.playback(); + + Assert.assertEquals(found1, Person.findPerson(id1)); + Assert.assertEquals(found2, Person.findPerson(id2)); + Assert.assertEquals(found1, Person.findPerson(id1)); + Assert.assertEquals(0, Person.countPeople()); + } + + // Note delegation is used when tests are invalid and should fail, as otherwise + // the failure will occur on the verify() method in the aspect after + // this method returns, failing the test case + @Test + public void testArgMethodNoMatchExpectReturn() { + try { + new Delegate().testArgMethodNoMatchExpectReturn(); + Assert.fail(); + } catch (IllegalArgumentException expected) { + } + } + + @Test(expected=IllegalArgumentException.class) + public void testArgMethodNoMatchExpectThrow() { + new Delegate().testArgMethodNoMatchExpectThrow(); + } + + private void called(Person found, long id) { + Assert.assertEquals(found, Person.findPerson(id)); + } + + @Test + public void testReentrant() { + long id = 13; + Person found = new Person(); + Person.findPerson(id); + JUnitStaticEntityMockingControl.expectReturn(found); + JUnitStaticEntityMockingControl.playback(); + called(found, id); + } + + @Test(expected=IllegalStateException.class) + public void testRejectUnexpectedCall() { + new Delegate().rejectUnexpectedCall(); + } + + @Test(expected=IllegalStateException.class) + public void testFailTooFewCalls() { + new Delegate().failTooFewCalls(); + } + + @Test + public void testEmpty() { + // Test that verification check doesn't blow up if no replay() call happened + } + + @Test(expected=IllegalStateException.class) + public void testDoesntEverReplay() { + new Delegate().doesntEverReplay(); + } + + @Test(expected=IllegalStateException.class) + public void testDoesntEverSetReturn() { + new Delegate().doesntEverSetReturn(); + } +} + +// Used because verification failures occur after method returns, +// so we can't test for them in the test case itself +@MockStaticEntityMethods +class Delegate { + + @Test + public void testArgMethodNoMatchExpectReturn() { + long id = 13; + Person found = new Person(); + Person.findPerson(id); + JUnitStaticEntityMockingControl.expectReturn(found); + JUnitStaticEntityMockingControl.playback(); + Assert.assertEquals(found, Person.findPerson(id + 1)); + } + + @Test + public void testArgMethodNoMatchExpectThrow() { + long id = 13; + Person found = new Person(); + Person.findPerson(id); + JUnitStaticEntityMockingControl.expectThrow(new PersistenceException()); + JUnitStaticEntityMockingControl.playback(); + Assert.assertEquals(found, Person.findPerson(id + 1)); + } + + @Test + public void failTooFewCalls() { + long id = 13; + Person found = new Person(); + Person.findPerson(id); + JUnitStaticEntityMockingControl.expectReturn(found); + Person.countPeople(); + JUnitStaticEntityMockingControl.expectReturn(25); + JUnitStaticEntityMockingControl.playback(); + Assert.assertEquals(found, Person.findPerson(id)); + } + + @Test + public void doesntEverReplay() { + Person.countPeople(); + } + + @Test + public void doesntEverSetReturn() { + Person.countPeople(); + JUnitStaticEntityMockingControl.playback(); + } + + @Test + public void rejectUnexpectedCall() { + JUnitStaticEntityMockingControl.playback(); + Person.countPeople(); + } + + @Test(expected=RemoteException.class) + public void testVerificationFailsEvenWhenTestFailsInExpectedManner() throws RemoteException { + Person.countPeople(); + JUnitStaticEntityMockingControl.playback(); + // No calls to allow verification failure + throw new RemoteException(); + } +} diff --git a/org.springframework.aspects/src/test/java/org/springframework/mock/static_mock/Person.java b/org.springframework.aspects/src/test/java/org/springframework/mock/static_mock/Person.java new file mode 100644 index 00000000000..65f112cd094 --- /dev/null +++ b/org.springframework.aspects/src/test/java/org/springframework/mock/static_mock/Person.java @@ -0,0 +1,8 @@ +package org.springframework.mock.static_mock; + +import javax.persistence.Entity; + +@Entity +public class Person { +} + diff --git a/org.springframework.aspects/src/test/java/org/springframework/mock/static_mock/Person_Roo_Entity.aj b/org.springframework.aspects/src/test/java/org/springframework/mock/static_mock/Person_Roo_Entity.aj new file mode 100644 index 00000000000..aadad7ce0f1 --- /dev/null +++ b/org.springframework.aspects/src/test/java/org/springframework/mock/static_mock/Person_Roo_Entity.aj @@ -0,0 +1,84 @@ +package org.springframework.mock.static_mock; + +privileged aspect Person_Roo_Entity { + + @javax.persistence.PersistenceContext + transient javax.persistence.EntityManager Person.entityManager; + + @javax.persistence.Id + @javax.persistence.GeneratedValue(strategy = javax.persistence.GenerationType.AUTO) + @javax.persistence.Column(name = "id") + private java.lang.Long Person.id; + + @javax.persistence.Version + @javax.persistence.Column(name = "version") + private java.lang.Integer Person.version; + + public java.lang.Long Person.getId() { + return this.id; + } + + public void Person.setId(java.lang.Long id) { + this.id = id; + } + + public java.lang.Integer Person.getVersion() { + return this.version; + } + + public void Person.setVersion(java.lang.Integer version) { + this.version = version; + } + + @org.springframework.transaction.annotation.Transactional + public void Person.persist() { + if (this.entityManager == null) throw new IllegalStateException("Entity manager has not been injected (is the Spring Aspects JAR configured as an AJC/AJDT aspects library?)"); + this.entityManager.persist(this); + } + + @org.springframework.transaction.annotation.Transactional + public void Person.remove() { + if (this.entityManager == null) throw new IllegalStateException("Entity manager has not been injected (is the Spring Aspects JAR configured as an AJC/AJDT aspects library?)"); + this.entityManager.remove(this); + } + + @org.springframework.transaction.annotation.Transactional + public void Person.flush() { + if (this.entityManager == null) throw new IllegalStateException("Entity manager has not been injected (is the Spring Aspects JAR configured as an AJC/AJDT aspects library?)"); + this.entityManager.flush(); + } + + @org.springframework.transaction.annotation.Transactional + public void Person.merge() { + if (this.entityManager == null) throw new IllegalStateException("Entity manager has not been injected (is the Spring Aspects JAR configured as an AJC/AJDT aspects library?)"); + Person merged = this.entityManager.merge(this); + this.entityManager.flush(); + this.id = merged.getId(); + } + + public static long Person.countPeople() { + javax.persistence.EntityManager em = new Person().entityManager; + if (em == null) throw new IllegalStateException("Entity manager has not been injected (is the Spring Aspects JAR configured as an AJC/AJDT aspects library?)"); + return (Long) em.createQuery("select count(o) from Person o").getSingleResult(); + } + + public static java.util.List Person.findAllPeople() { + javax.persistence.EntityManager em = new Person().entityManager; + if (em == null) throw new IllegalStateException("Entity manager has not been injected (is the Spring Aspects JAR configured as an AJC/AJDT aspects library?)"); + return em.createQuery("select o from Person o").getResultList(); + } + + public static Person Person.findPerson(java.lang.Long id) { + if (id == null) throw new IllegalArgumentException("An identifier is required to retrieve an instance of Person"); + javax.persistence.EntityManager em = new Person().entityManager; + if (em == null) throw new IllegalStateException("Entity manager has not been injected (is the Spring Aspects JAR configured as an AJC/AJDT aspects library?)"); + return em.find(Person.class, id); + } + + public static java.util.List Person.findPersonEntries(int firstResult, int maxResults) { + javax.persistence.EntityManager em = new Person().entityManager; + if (em == null) throw new IllegalStateException("Entity manager has not been injected (is the Spring Aspects JAR configured as an AJC/AJDT aspects library?)"); + return em.createQuery("select o from Person o").setFirstResult(firstResult).setMaxResults(maxResults).getResultList(); + } + +}