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
This commit is contained in:
parent
edb0b0e84b
commit
624170f178
|
|
@ -26,9 +26,9 @@ import org.springframework.util.ObjectUtils;
|
||||||
*
|
*
|
||||||
* <p>Sub-aspects must define:
|
* <p>Sub-aspects must define:
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>the {@link #mockStaticsTestMethod()} pointcut to indicate call stacks
|
* <li>The {@link #mockStaticsTestMethod()} pointcut to indicate call stacks
|
||||||
* when mocking should be triggered
|
* when mocking should be triggered
|
||||||
* <li>the {@link #methodToMock()} pointcut to pick out method invocations to mock
|
* <li>The {@link #methodToMock()} pointcut to pick out method invocations to mock
|
||||||
* </ul>
|
* </ul>
|
||||||
*
|
*
|
||||||
* @author Rod Johnson
|
* @author Rod Johnson
|
||||||
|
|
@ -37,16 +37,41 @@ import org.springframework.util.ObjectUtils;
|
||||||
*/
|
*/
|
||||||
public abstract aspect AbstractMethodMockingControl percflow(mockStaticsTestMethod()) {
|
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;
|
||||||
|
|
||||||
|
|
||||||
static enum CallResponse {
|
protected void expectReturnInternal(Object retVal) {
|
||||||
nothing, return_, throw_
|
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 String signature;
|
||||||
private final Object[] args;
|
private final Object[] args;
|
||||||
|
|
||||||
|
private CallResponse responseType = CallResponse.undefined;
|
||||||
private Object responseObject; // return value or throwable
|
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.signature = signature;
|
||||||
this.args = args;
|
this.args = args;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean hasResponseSpecified() {
|
boolean responseTypeAlreadySet() {
|
||||||
return responseType != CallResponse.nothing;
|
return responseType != CallResponse.undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setReturnVal(Object retVal) {
|
void setReturnValue(Object retVal) {
|
||||||
this.responseObject = retVal;
|
this.responseObject = retVal;
|
||||||
responseType = CallResponse.return_;
|
responseType = CallResponse.return_;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setThrow(Throwable throwable) {
|
void setThrowable(Throwable throwable) {
|
||||||
this.responseObject = throwable;
|
this.responseObject = throwable;
|
||||||
responseType = CallResponse.throw_;
|
responseType = CallResponse.throw_;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Object returnValue(String lastSig, Object[] args) {
|
Object returnValue(String lastSig, Object[] args) {
|
||||||
checkSignature(lastSig, args);
|
checkSignature(lastSig, args);
|
||||||
return responseObject;
|
return responseObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Object throwException(String lastSig, Object[] args) {
|
Object throwException(String lastSig, Object[] args) {
|
||||||
checkSignature(lastSig, args);
|
checkSignature(lastSig, args);
|
||||||
throw (RuntimeException) responseObject;
|
throw (RuntimeException) responseObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkSignature(String lastSig, Object[] args) {
|
private void checkSignature(String lastSig, Object[] args) {
|
||||||
if (!signature.equals(lastSig)) {
|
if (!signature.equals(lastSig)) {
|
||||||
throw new IllegalArgumentException("Signature doesn't match");
|
throw new IllegalArgumentException("Signatures do not match");
|
||||||
}
|
}
|
||||||
if (!Arrays.equals(this.args, args)) {
|
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) {
|
public void expectReturn(Object retVal) {
|
||||||
Call c = calls.getLast();
|
Call c = calls.getLast();
|
||||||
if (c.hasResponseSpecified()) {
|
if (c.responseTypeAlreadySet()) {
|
||||||
throw new IllegalStateException("No method invoked before setting return value");
|
throw new IllegalStateException("No method invoked before setting return value");
|
||||||
}
|
}
|
||||||
c.setReturnVal(retVal);
|
c.setReturnValue(retVal);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void expectThrow(Throwable throwable) {
|
public void expectThrow(Throwable throwable) {
|
||||||
Call c = calls.getLast();
|
Call c = calls.getLast();
|
||||||
if (c.hasResponseSpecified()) {
|
if (c.responseTypeAlreadySet()) {
|
||||||
throw new IllegalStateException("No method invoked before setting throwable");
|
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() {
|
after() returning : mockStaticsTestMethod() {
|
||||||
if (recording && (expectations.hasCalls())) {
|
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 "
|
"Calls have been recorded, but playback state was never reached. Set expectations and then call "
|
||||||
+ this.getClass().getSimpleName() + ".playback();");
|
+ this.getClass().getSimpleName() + ".playback();");
|
||||||
}
|
}
|
||||||
expectations.verify();
|
verifyInternal();
|
||||||
}
|
}
|
||||||
|
|
||||||
Object around() : methodToMock() && cflowbelow(mockStaticsTestMethod()) {
|
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,11 @@ package org.springframework.mock.staticmock;
|
||||||
* throws an exception.
|
* throws an exception.
|
||||||
* </ol>
|
* </ol>
|
||||||
*
|
*
|
||||||
|
* <h3>Programmatic Control of the Mock</h3>
|
||||||
|
* <p>For scenarios where it would be convenient to programmatically <em>verify</em>
|
||||||
|
* the recorded expectations or <em>reset</em> the state of the mock, consider
|
||||||
|
* using combinations of {@link #verify()} and {@link #reset()}.
|
||||||
|
*
|
||||||
* @author Rod Johnson
|
* @author Rod Johnson
|
||||||
* @author Ramnivas Laddad
|
* @author Ramnivas Laddad
|
||||||
* @author Sam Brannen
|
* @author Sam Brannen
|
||||||
|
|
@ -81,10 +86,28 @@ public aspect AnnotationDrivenStaticEntityMockingControl extends AbstractMethodM
|
||||||
AnnotationDrivenStaticEntityMockingControl.aspectOf().playbackInternal();
|
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 <em>recording</em> mode.
|
||||||
|
* @since 4.0.2
|
||||||
|
* @see #verify()
|
||||||
|
*/
|
||||||
|
public static void reset() {
|
||||||
|
AnnotationDrivenStaticEntityMockingControl.aspectOf().resetInternal();
|
||||||
|
}
|
||||||
|
|
||||||
// Apparently, the following pointcut was originally defined to only match
|
// Apparently, the following pointcut was originally defined to only match
|
||||||
// methods directly annotated with @Test (in order to allow methods in
|
// methods directly annotated with @Test (in order to allow methods in
|
||||||
// @MockStaticEntityMethods classes to invoke each other without resetting
|
// @MockStaticEntityMethods classes to invoke each other without creating a
|
||||||
// the mocking environment); however, this is no longer the case. The current
|
// new mocking environment); however, this is no longer the case. The current
|
||||||
// pointcut applies to all public methods in @MockStaticEntityMethods classes.
|
// pointcut applies to all public methods in @MockStaticEntityMethods classes.
|
||||||
protected pointcut mockStaticsTestMethod() : execution(public * (@MockStaticEntityMethods *).*(..));
|
protected pointcut mockStaticsTestMethod() : execution(public * (@MockStaticEntityMethods *).*(..));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -128,7 +128,8 @@ public class AnnotationDrivenStaticEntityMockingControlTests {
|
||||||
fail("Should have thrown an IllegalStateException");
|
fail("Should have thrown an IllegalStateException");
|
||||||
}
|
}
|
||||||
catch (IllegalStateException e) {
|
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:
|
// Now to keep the mock for "this" method happy:
|
||||||
|
|
@ -210,4 +211,90 @@ public class AnnotationDrivenStaticEntityMockingControlTests {
|
||||||
throw new UnsupportedOperationException();
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue