From 624170f178a9d8d1404889b396e32b42b5be3074 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Wed, 5 Feb 2014 23:18:41 +0100 Subject: [PATCH] Introduce verify & reset methods in ADSEMockCtrl This commit introduces static verify() and reset() methods in AnnotationDrivenStaticEntityMockingControl for programmatic control on the mock. Issue: SPR-11395 --- .../AbstractMethodMockingControl.aj | 104 +++++++++++------- ...otationDrivenStaticEntityMockingControl.aj | 29 ++++- ...DrivenStaticEntityMockingControlTests.java | 89 ++++++++++++++- 3 files changed, 177 insertions(+), 45 deletions(-) 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 0c12182ccb5..f34dbeb535f 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: *

* * @author Rod Johnson @@ -37,16 +37,41 @@ import org.springframework.util.ObjectUtils; */ public abstract aspect AbstractMethodMockingControl percflow(mockStaticsTestMethod()) { - protected abstract pointcut mockStaticsTestMethod(); + private final Expectations expectations = new Expectations(); - protected abstract pointcut methodToMock(); + private boolean recording = true; - private boolean recording = true; + 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; + } - static enum CallResponse { - nothing, return_, throw_ + 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 551c1931669..f29cc82822b 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 6d3ea5389ad..a8fb3d16dc1 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(); + } + } + }