diff --git a/spring-aspects/src/main/java/org/springframework/mock/staticmock/AbstractMethodMockingControl.aj b/spring-aspects/src/main/java/org/springframework/mock/staticmock/AbstractMethodMockingControl.aj
index 0c12182ccb..f34dbeb535 100644
--- a/spring-aspects/src/main/java/org/springframework/mock/staticmock/AbstractMethodMockingControl.aj
+++ b/spring-aspects/src/main/java/org/springframework/mock/staticmock/AbstractMethodMockingControl.aj
@@ -26,9 +26,9 @@ import org.springframework.util.ObjectUtils;
*
*
Sub-aspects must define:
*
- * - the {@link #mockStaticsTestMethod()} pointcut to indicate call stacks
+ *
- The {@link #mockStaticsTestMethod()} pointcut to indicate call stacks
* when mocking should be triggered
- *
- the {@link #methodToMock()} pointcut to pick out method invocations to mock
+ *
- The {@link #methodToMock()} pointcut to pick out method invocations to mock
*
*
* @author Rod Johnson
@@ -37,16 +37,41 @@ import org.springframework.util.ObjectUtils;
*/
public abstract aspect AbstractMethodMockingControl percflow(mockStaticsTestMethod()) {
- protected abstract pointcut mockStaticsTestMethod();
-
- protected abstract pointcut methodToMock();
-
+ private final Expectations expectations = new Expectations();
private boolean recording = true;
- static enum CallResponse {
- nothing, return_, throw_
+ protected void expectReturnInternal(Object retVal) {
+ if (!recording) {
+ throw new IllegalStateException("Not recording: Cannot set return value");
+ }
+ expectations.expectReturn(retVal);
+ }
+
+ protected void expectThrowInternal(Throwable throwable) {
+ if (!recording) {
+ throw new IllegalStateException("Not recording: Cannot set throwable value");
+ }
+ expectations.expectThrow(throwable);
+ }
+
+ protected void playbackInternal() {
+ recording = false;
+ }
+
+ protected void verifyInternal() {
+ expectations.verify();
+ }
+
+ protected void resetInternal() {
+ expectations.reset();
+ recording = true;
+ }
+
+
+ private static enum CallResponse {
+ undefined, return_, throw_
};
/**
@@ -63,45 +88,45 @@ public abstract aspect AbstractMethodMockingControl percflow(mockStaticsTestMeth
private final String signature;
private final Object[] args;
+ private CallResponse responseType = CallResponse.undefined;
private Object responseObject; // return value or throwable
- private CallResponse responseType = CallResponse.nothing;
- public Call(String signature, Object[] args) {
+ Call(String signature, Object[] args) {
this.signature = signature;
this.args = args;
}
- public boolean hasResponseSpecified() {
- return responseType != CallResponse.nothing;
+ boolean responseTypeAlreadySet() {
+ return responseType != CallResponse.undefined;
}
- public void setReturnVal(Object retVal) {
+ void setReturnValue(Object retVal) {
this.responseObject = retVal;
responseType = CallResponse.return_;
}
- public void setThrow(Throwable throwable) {
+ void setThrowable(Throwable throwable) {
this.responseObject = throwable;
responseType = CallResponse.throw_;
}
- public Object returnValue(String lastSig, Object[] args) {
+ Object returnValue(String lastSig, Object[] args) {
checkSignature(lastSig, args);
return responseObject;
}
- public Object throwException(String lastSig, Object[] args) {
+ 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");
+ throw new IllegalArgumentException("Signatures do not match");
}
if (!Arrays.equals(this.args, args)) {
- throw new IllegalArgumentException("Arguments don't match");
+ throw new IllegalArgumentException("Arguments do not match");
}
}
@@ -168,24 +193,39 @@ public abstract aspect AbstractMethodMockingControl percflow(mockStaticsTestMeth
public void expectReturn(Object retVal) {
Call c = calls.getLast();
- if (c.hasResponseSpecified()) {
+ if (c.responseTypeAlreadySet()) {
throw new IllegalStateException("No method invoked before setting return value");
}
- c.setReturnVal(retVal);
+ c.setReturnValue(retVal);
}
public void expectThrow(Throwable throwable) {
Call c = calls.getLast();
- if (c.hasResponseSpecified()) {
+ if (c.responseTypeAlreadySet()) {
throw new IllegalStateException("No method invoked before setting throwable");
}
- c.setThrow(throwable);
+ c.setThrowable(throwable);
+ }
+
+ /**
+ * Reset the internal state of this {@code Expectations} instance.
+ */
+ public void reset() {
+ this.calls.clear();
+ this.verified = 0;
}
}
- private final Expectations expectations = new Expectations();
+ /**
+ * Pointcut that identifies call stacks when mocking should be triggered.
+ */
+ protected abstract pointcut mockStaticsTestMethod();
+ /**
+ * Pointcut that identifies which method invocations to mock.
+ */
+ protected abstract pointcut methodToMock();
after() returning : mockStaticsTestMethod() {
if (recording && (expectations.hasCalls())) {
@@ -193,7 +233,7 @@ public abstract aspect AbstractMethodMockingControl percflow(mockStaticsTestMeth
"Calls have been recorded, but playback state was never reached. Set expectations and then call "
+ this.getClass().getSimpleName() + ".playback();");
}
- expectations.verify();
+ verifyInternal();
}
Object around() : methodToMock() && cflowbelow(mockStaticsTestMethod()) {
@@ -207,22 +247,4 @@ public abstract aspect AbstractMethodMockingControl percflow(mockStaticsTestMeth
}
}
- 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/spring-aspects/src/main/java/org/springframework/mock/staticmock/AnnotationDrivenStaticEntityMockingControl.aj b/spring-aspects/src/main/java/org/springframework/mock/staticmock/AnnotationDrivenStaticEntityMockingControl.aj
index 551c193166..f29cc82822 100644
--- a/spring-aspects/src/main/java/org/springframework/mock/staticmock/AnnotationDrivenStaticEntityMockingControl.aj
+++ b/spring-aspects/src/main/java/org/springframework/mock/staticmock/AnnotationDrivenStaticEntityMockingControl.aj
@@ -47,6 +47,11 @@ package org.springframework.mock.staticmock;
* throws an exception.
*
*
+ * Programmatic Control of the Mock
+ * For scenarios where it would be convenient to programmatically verify
+ * the recorded expectations or reset the state of the mock, consider
+ * using combinations of {@link #verify()} and {@link #reset()}.
+ *
* @author Rod Johnson
* @author Ramnivas Laddad
* @author Sam Brannen
@@ -80,11 +85,29 @@ public aspect AnnotationDrivenStaticEntityMockingControl extends AbstractMethodM
public static void playback() {
AnnotationDrivenStaticEntityMockingControl.aspectOf().playbackInternal();
}
-
+
+ /**
+ * Verify that all expectations have been fulfilled.
+ * @since 4.0.2
+ * @see #reset()
+ */
+ public static void verify() {
+ AnnotationDrivenStaticEntityMockingControl.aspectOf().verifyInternal();
+ }
+
+ /**
+ * Reset the state of the mock and enter recording mode.
+ * @since 4.0.2
+ * @see #verify()
+ */
+ public static void reset() {
+ AnnotationDrivenStaticEntityMockingControl.aspectOf().resetInternal();
+ }
+
// Apparently, the following pointcut was originally defined to only match
// methods directly annotated with @Test (in order to allow methods in
- // @MockStaticEntityMethods classes to invoke each other without resetting
- // the mocking environment); however, this is no longer the case. The current
+ // @MockStaticEntityMethods classes to invoke each other without creating a
+ // new mocking environment); however, this is no longer the case. The current
// pointcut applies to all public methods in @MockStaticEntityMethods classes.
protected pointcut mockStaticsTestMethod() : execution(public * (@MockStaticEntityMethods *).*(..));
diff --git a/spring-aspects/src/test/java/org/springframework/mock/staticmock/AnnotationDrivenStaticEntityMockingControlTests.java b/spring-aspects/src/test/java/org/springframework/mock/staticmock/AnnotationDrivenStaticEntityMockingControlTests.java
index 6d3ea5389a..a8fb3d16dc 100644
--- a/spring-aspects/src/test/java/org/springframework/mock/staticmock/AnnotationDrivenStaticEntityMockingControlTests.java
+++ b/spring-aspects/src/test/java/org/springframework/mock/staticmock/AnnotationDrivenStaticEntityMockingControlTests.java
@@ -128,7 +128,8 @@ public class AnnotationDrivenStaticEntityMockingControlTests {
fail("Should have thrown an IllegalStateException");
}
catch (IllegalStateException e) {
- assertTrue(e.getMessage().contains("Calls have been recorded, but playback state was never reached."));
+ String snippet = "Calls have been recorded, but playback state was never reached.";
+ assertTrue("Exception message should contain [" + snippet + "]", e.getMessage().contains(snippet));
}
// Now to keep the mock for "this" method happy:
@@ -210,4 +211,90 @@ public class AnnotationDrivenStaticEntityMockingControlTests {
throw new UnsupportedOperationException();
}
+ @Test
+ public void resetMockWithoutVerficationAndStartOverWithoutRedeclaringExpectations() {
+ final Long ID = 13L;
+ Person.findPerson(ID);
+ expectReturn(new Person());
+
+ reset();
+
+ Person.findPerson(ID);
+ // Omit expectation.
+ playback();
+
+ try {
+ Person.findPerson(ID);
+ fail("Should have thrown an IllegalStateException");
+ }
+ catch (IllegalStateException e) {
+ String snippet = "Behavior of Call with signature";
+ assertTrue("Exception message should contain [" + snippet + "]", e.getMessage().contains(snippet));
+ }
+ }
+
+ @Test
+ public void resetMockWithoutVerificationAndStartOver() {
+ final Long ID = 13L;
+ Person found = new Person();
+ Person.findPerson(ID);
+ expectReturn(found);
+
+ reset();
+
+ // Intentionally use a different ID:
+ final long ID_2 = ID + 1;
+ Person.findPerson(ID_2);
+ expectReturn(found);
+ playback();
+
+ assertEquals(found, Person.findPerson(ID_2));
+ }
+
+ @Test
+ public void verifyResetAndStartOver() {
+ final Long ID_1 = 13L;
+ Person found1 = new Person();
+ Person.findPerson(ID_1);
+ expectReturn(found1);
+ playback();
+
+ assertEquals(found1, Person.findPerson(ID_1));
+ verify();
+ reset();
+
+ // Intentionally use a different ID:
+ final long ID_2 = ID_1 + 1;
+ Person found2 = new Person();
+ Person.findPerson(ID_2);
+ expectReturn(found2);
+ playback();
+
+ assertEquals(found2, Person.findPerson(ID_2));
+ }
+
+ @Test
+ public void verifyWithTooFewCalls() {
+ final Long ID = 13L;
+ Person found = new Person();
+ Person.findPerson(ID);
+ expectReturn(found);
+ Person.findPerson(ID);
+ expectReturn(found);
+ playback();
+
+ assertEquals(found, Person.findPerson(ID));
+
+ try {
+ verify();
+ fail("Should have thrown an IllegalStateException");
+ }
+ catch (IllegalStateException e) {
+ assertEquals("Expected 2 calls, but received 1", e.getMessage());
+ // Since verify() failed, we need to manually reset so that the test method
+ // does not fail.
+ reset();
+ }
+ }
+
}