001: /* Copyright (c) 2005 Per S Hustad. All rights reserved.
002: *
003: * Redistribution and use in source and binary forms, with or without
004: * modification, are permitted provided that the following conditions are met:
005: *
006: * o Redistributions of source code must retain the above copyright notice,
007: * this list of conditions and the following disclaimer.
008: *
009: * o Redistributions in binary form must reproduce the above copyright notice,
010: * this list of conditions and the following disclaimer in the documentation
011: * and/or other materials provided with the distribution.
012: *
013: * o Neither the name of surrogate.sourceforge.net nor the names of
014: * its contributors may be used to endorse or promote products derived
015: * from this software without specific prior written permission.
016: *
017: * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
018: * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
019: * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
020: * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
021: * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
022: * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
023: * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
024: * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
025: * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
026: * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
027: * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
028: */
029: package net.sf.surrogate.core;
030:
031: import java.lang.reflect.Constructor;
032: import java.lang.reflect.Method;
033: import java.util.Enumeration;
034: import java.util.Hashtable;
035:
036: import org.aspectj.lang.JoinPoint;
037: import org.aspectj.lang.Signature;
038: import org.aspectj.lang.reflect.CodeSignature;
039: import org.aspectj.lang.reflect.ConstructorSignature;
040: import org.aspectj.lang.reflect.MethodSignature;
041:
042: /**
043: * The link between Mock/ Unit Test objects and AspectJ code.
044: * <p>
045: * The SurrogateManager is the common controller for Junit tests manipulating
046: * MockObjects and mock methods.
047: * <h4>Test Case View</h4>
048: * The SurrogateManager provides utility methods for test cases to control which
049: * Mock objects should be active for a particular test case run. These methods
050: * are normally:
051: * <pre>
052: * {@link #getInstance()}
053: * {@link #reset()}
054: * {@link #addMock(Object)}
055: * {@link #addMockMethod(MockMethod)}
056: * {@link #removeMock(Object)}
057: * {@link #removeMockMethod(MockMethod)}
058: * </pre>
059: * <h4>Aspect View</h4>
060: * Every time an aspect detects a "mock poincut", it will ask the
061: * SurrogateManager if a mock object has been registered for the corresponding
062: * joinpoint. If so, the mock executes instead of the "real" method. Otherwise,
063: * the real object is allowed to execute. The methods used by the aspects are
064: * normally:
065: * <pre>
066: * {@link #getInstance()}
067: * {@link #getMockExecutor(JoinPoint)}
068: * </pre>
069: * <h4>Test Case responsibility</h4>
070: * A test case should normally always call the {@link #reset}method before
071: * starting to use the SurrogateManager to avoid independence from other
072: * testcases which might have been run earlier from within the same VM.
073: *
074: * <h4><a name="Debugging">Debugging </a></h4>
075: * If the System property "surrogate.debug" is set to "true", e.g. with the java
076: * <b>-Dsurrogate.debug=true </b> option, the SurrogateManager will report every
077: * time it is asked to resolve a JoinPoint into a mock object or method. The
078: * information is written to <code>System.out</code>. Each debug line is on
079: * the following format:
080: * <pre>
081: * surrogate:<joinpoint id>|added|removed=<mock id>
082: *
083: * Example:
084: * surrogate:added=public static native long java.lang.System.currentTimeMillis()
085: * surrogate:added=net.sf.surrogate.example.MockFileWriter@38e059
086: * surrogate:call(long java.lang.System.currentTimeMillis())=public static native long java.lang.System.currentTimeMillis()
087: * surrogate:call(java.io.FileWriter(String))=net.sf.surrogate.example.MockFileWriter@38e059
088: * surrogate:call(java.io.BufferedWriter(Writer))=null
089: * </pre>
090: * I.e. the unit test has added mocks for <code>currentTimeMillis</code> and
091: * the <code>FileWriter</code>. As expected, mocks were returned for the
092: * <code>currentTimeMillis</code> and <code>FileWriter</code>
093: * mockJoinPoint's but no mock was found for the</code> BufferedWriter</code>
094: * joinpoint (<code>null</code> was returned).
095: *
096: * @see SurrogateCalls
097: * @see MockMethod
098: * @author Per S Hustad
099: */
100: public class SurrogateManager {
101: private static final String ADDED_MOCK = "added";
102:
103: private static final String REMOVED_MOCK = "removed";
104:
105: private static final String SURROGATE_DEBUG = "surrogate.debug";
106:
107: private static SurrogateManager theInstance = null;
108:
109: private Hashtable allMocks = new Hashtable();
110:
111: private Hashtable allMockRefs = new Hashtable();
112:
113: private boolean isDebugEnabled = false;
114:
115: /**
116: * There should ony be one instance of this object in the VM!
117: */
118: private SurrogateManager() {
119:
120: // Check if debug is enabled
121: String debugOption = System.getProperty(SURROGATE_DEBUG);
122: isDebugEnabled = Boolean.valueOf(debugOption).booleanValue();
123: }
124:
125: /**
126: * Gets the manager singleton
127: *
128: * @return the manager instance.
129: * @see #reset
130: */
131: public static SurrogateManager getInstance() {
132: if (theInstance == null) {
133: theInstance = createInstance();
134: }
135: return theInstance;
136: }
137:
138: /**
139: * Created an instance of the manager. Override this method if you want to
140: * create a subclass of the manager
141: *
142: * @return a new instance of the manager
143: */
144: protected static SurrogateManager createInstance() {
145: return new SurrogateManager();
146: }
147:
148: /**
149: * Removes all Mock objects and methods from the list of active objects.
150: * This method should be called by every TestCase method to ensure that
151: * "old" Mocks created by other testcases are not hanging around in the
152: * system ...
153: */
154: public void reset() {
155: for (Enumeration e1 = allMocks.elements(); e1.hasMoreElements();) {
156: debug(REMOVED_MOCK, e1.nextElement());
157: }
158: allMocks.clear();
159:
160: for (Enumeration e2 = allMockRefs.elements(); e2
161: .hasMoreElements();) {
162: debug(REMOVED_MOCK, e2.nextElement());
163: }
164: allMockRefs.clear();
165: }
166:
167: /**
168: * Adds a Mock object to the manager for later lookup by Aspect code.
169: * <p>
170: * Note that for objects to be substituted with their mock implementation,
171: * there must have been defined a <code>pointcut</code> intercepting the
172: * method call or method execution. Otherwise, the mock object will never be
173: * substituted, even if it has been registered. See {@link SurrogateCalls}
174: * for pointcut definition details.
175: * <p>
176: * Surrogate uses the <code>java.lang.Class.isAssignableFrom</code> to see
177: * whether the registered mock can subsitute a "real" object. It does this
178: * by looking up the declared return type signature of the method or the
179: * class signature of the constructor call.
180: * <p>
181: * Example usage:
182: *
183: * <pre>
184: *
185: * SurrogateManager mm = SurrogateManager.getInstance();
186: * mm.reset();
187: * MockCustomerService mock = new MockCustomerService();
188: * mm.addMock(mock);
189: * mock.setGetCustomerReturnValue("MockCustomer");
190: * ...
191: *
192: * </pre>
193: *
194: * @param o
195: * the Mock object to add. Must be non-null.
196: * @return the Mock object given as argument
197: * @see #addMockMethod(MockMethod)
198: * @see #removeMock(Object)
199: * @see SurrogateCalls
200: */
201: public Object addMock(Object o) {
202: allMocks.put(o.getClass(), o);
203: debug(ADDED_MOCK, o.toString());
204: return o;
205: }
206:
207: /**
208: * Adds a mock method to the manager for later lookup by Aspect code.
209: * <p>
210: * Note that for objects to be substituted with their mock implementation,
211: * there must have been defined a <code>pointcut</code> intercepting the
212: * method call or method execution. Otherwise, the mock object will never be
213: * substituted, even if it has been registered. See {@link SurrogateCalls}
214: * for pointcut definition details.
215: * <p>
216: * Example usage:
217: * <pre>
218: * SurrogateManager mm = SurrogateManager.getInstance();
219: * mm.reset();
220: * MockMethod mockTime =
221: * mm.addMockMethod(new MockMethod(System.class,"currentTimeMillis"));
222: * mockTime.addReturnValue(1000L);
223: * mockTime.addReturnValue(2000L);
224: * mockTime.setExpectedCalls(2);
225: * ... Call object using System.currentTimeMillis
226: * mockTime.verify();
227: * ...
228: * </pre>
229: *
230: * @param m
231: * the Mock object to add. Must be non-null.
232: * @return the MockMethod as given on input
233: * @see #addMock(Object)
234: * @see #removeMockMethod(MockMethod)
235: */
236: public MockMethod addMockMethod(MockMethod m) {
237: allMockRefs.put(m.getAccessibleObject(), m);
238: debug(ADDED_MOCK, m.toString());
239: return m;
240: }
241:
242: /**
243: * Removes a mock from the manager. The object will hence no longer be
244: * returned instead of a "real" object when the corresponding aspect advice
245: * executes
246: *
247: * @param o
248: * the object to remove.
249: * @return the removed object or <code>null</code> if the object was not
250: * found
251: * @see #addMock(Object)
252: */
253: public Object removeMock(Object o) {
254: debug(REMOVED_MOCK, o.toString());
255: return allMocks.remove(o.getClass());
256: }
257:
258: /**
259: * Removes a mock method from the manager. The method will hence no longer
260: * execute instead of the "real" method when the corresponding aspect advice
261: * executes.
262: *
263: * @param o
264: * the object to remove.
265: * @return the removed method or <code>null</code> if the mock method was
266: * not found.
267: * @see #addMockMethod(MockMethod)
268: */
269: public MockMethod removeMockMethod(MockMethod m) {
270: debug(REMOVED_MOCK, m.toString());
271: return (MockMethod) allMockRefs.remove(m.getAccessibleObject());
272: }
273:
274: /**
275: * Locates and gets a Mock object for an interface or a class. This method
276: * looks for a Mock object implementing the interface or the class
277: * assignable from <code>myClassOrInterface</code> by searching in the
278: * list of Mock objects for the first object which
279: * <code>myClassOrInterface</code> is assignable from
280: *
281: * @param myClassOrInterface
282: * the interface/class to find a mock object for
283: * @return the Mock object for the interface/class or <code>null</code> if
284: * no such mock object has been registered via the
285: * <code>addMock</code> method.
286: * @see #addMock(Object)
287: * @see Class#isAssignableFrom(java.lang.Class)
288: */
289: Object getMockObject(Class myClassOrInterface) {
290: for (Enumeration keys = allMocks.keys(); keys.hasMoreElements();) {
291: Class c = (Class) keys.nextElement();
292: if (myClassOrInterface.isAssignableFrom(c)) {
293: return allMocks.get(c);
294: }
295: }
296: return null;
297: }
298:
299: /**
300: * Gets a registered <code>Mock method reference</code> for a method. This
301: * method should normally only be called from the aspect code.
302: *
303: * @param m
304: * the Method to check
305: * @return the registered Mock method reference or <code>null</code> if no
306: * such reference exists for the method
307: * @see #addMockMethod(MockMethod)
308: */
309: MockMethod getMockMethod(Method m) {
310: return (MockMethod) allMockRefs.get(m);
311: }
312:
313: /**
314: * Gets a registered <code>Mock method reference</code> for a constructor
315: *
316: * @param c
317: * the constructor to check
318: * @return the registered Mock method reference or <code>null</code> if no
319: * such reference exists for the method
320: * @see #addMockMethod(MockMethod)
321: */
322: MockMethod getMockConstructor(Constructor c) {
323: return (MockMethod) allMockRefs.get(c);
324: }
325:
326: /**
327: * Checks if an AspectJ "mock" joinpoint has an assoicated mock object
328: * registered by the manager.
329: *
330: * @param p
331: * the join point
332: * @return the corresponding mock object or <code>null</code> if no match
333: * is found
334: */
335: protected Object getReturnedClassMock(JoinPoint p)
336: throws IllegalArgumentException {
337: Signature sig = p.getSignature();
338: Class c = null;
339: if (sig instanceof MethodSignature) {
340: c = ((MethodSignature) sig).getReturnType();
341: } else if (sig instanceof ConstructorSignature) {
342: c = ((CodeSignature) sig).getDeclaringType();
343: } else {
344: throw new IllegalArgumentException("Unhandled Signature: "
345: + sig.getClass().getName());
346: }
347: return getMockObject(c);
348: }
349:
350: /**
351: * Checks if an AspectJ "mock" joinpoint has an assoicated mock method
352: * registered by the manager.
353: *
354: * @param p
355: * the join point
356: * @return the corresponding mock method or <code>null</code> if no match
357: * is found
358: * @throws IllegalArgumentException
359: * if the joinpoint signature is not a "method" or "constructor"
360: * signature
361: */
362: protected MockMethod getMockMethod(JoinPoint p)
363: throws IllegalArgumentException, NoSuchMethodException {
364: MockMethod mockMethod;
365: CodeSignature sig = (CodeSignature) p.getSignature();
366: Class c = sig.getDeclaringType();
367: if (sig instanceof ConstructorSignature) {
368: Constructor constr = c.getDeclaredConstructor(sig
369: .getParameterTypes());
370: mockMethod = getMockConstructor(constr);
371: } else if (sig instanceof MethodSignature) {
372: Method m = c.getDeclaredMethod(sig.getName(), sig
373: .getParameterTypes());
374: mockMethod = getMockMethod(m);
375: } else {
376: throw new IllegalArgumentException("Unhandled Signature: "
377: + sig.getClass().getName());
378: }
379: return mockMethod;
380:
381: }
382:
383: /**
384: * Gets the mock object or mock method for a joinpoint. This method should
385: * normally only be called from the corresponding
386: * <code>SurrogateCalls</code> advice but can also be called from other
387: * advices which might want to check if a mock matches the joinpoint.
388: * <p>
389: * The rules are as follows
390: * <ul>
391: * <li>If a <code>MockMethod</code> matches the joinpoint, this method is
392: * returned.
393: * <li>Otherwise, if a Mock object matches the joinpoint, this object is
394: * returned, wrapped behind a <code>MockExecutor</code>
395: * <li>Otherwise, <code>null</code> is returned.
396: * </ul>
397: * The advices should, if receiving a non-null <code>MockExecutor</code>
398: * return value, call the <code>MockExecutor.execute</code> with the
399: * <b>same </b> JoinPoint as used as arguments to
400: * <code>getMockExecutor</code>. If the returned
401: * <code>MockExecutor</code> is <code>null</code>, the advice should
402: * return a sensible value, e.g. with <code>return proceed();</code>
403: *
404: * @param p
405: * the <code>thisJoinPoint</code> on the advice executing
406: * @return the corresponding mock executor or <code>null</code> if no
407: * match has been found with a registered mock object or mock method
408: *
409: * @throws IllegalArgumentException
410: * if the joinpoint signature is not a "constructor" or "method"
411: * signature.
412: *
413: * @throws NoSuchMethodException
414: * if the joinpoint is inconsistent. This should normally be
415: * considered as a bug.
416: * @see SurrogateCalls
417: * @see #addMock(Object)
418: * @see #addMockMethod(MockMethod)
419: */
420: public MockExecutor getMockExecutor(JoinPoint p)
421: throws IllegalArgumentException, NoSuchMethodException {
422: MockMethod m = getMockMethod(p);
423: if (m != null) {
424: debug(p.toString(), m.toString());
425: return m;
426: }
427: Object o = getReturnedClassMock(p);
428: if (o != null) {
429: debug(p.toString(), o.toString());
430: return new MockExecutorWrapper(o);
431: }
432: debug(p.toString(), null);
433: return null;
434:
435: }
436:
437: /**
438: * Prints debug information if debug is enabled.
439: *
440: * @param pred
441: * the predicate
442: * @param mockId
443: * the mock ID
444: * @see #Debugging
445: */
446: private void debug(String pred, Object mockId) {
447: if (isDebugEnabled) {
448: System.out.println("surrogate:" + pred + "="
449: + (mockId != null ? mockId : "null"));
450: }
451: }
452:
453: /**
454: * Defines, towards the AspectJ layer, the mock object or method to be
455: * executed.
456: *
457: * @author Per S Hustad
458: */
459: public static interface MockExecutor {
460: /**
461: * If the manager returns a non-null MockExecutor, the calling advice
462: * should call "execute" and return the "execute" return value. The
463: * throwable can be cached and rethrown with the
464: * <code>ExceptionThrower</code> utility.
465: *
466: * @param args
467: * the arguments, always use
468: * <code>thisJoinPoint.getArgs()<code>
469: * Ignored if the underlying mock is a mock object but used
470: * if it is a MockMethod.
471: * @return the return value of the underlying mock object. If the
472: * underlying mock object is a <code>MockMethod</code>, the return value as
473: * setup in the method is returned. If the underlying object is
474: * a plain mock object, that object is returned
475: * @throws Throwable
476: * if a <code>MockMethod</code> has been set up explicitly to throw an
477: * exeption or there was a mismatch between the JoinPoint args and
478: * the method args.
479: */
480: public Object execute(Object[] args) throws Throwable;
481: }
482:
483: /**
484: * Wrapper class to wrap a standard mock object as a MockExecutor. This
485: * class exists in order to provide a uniform interface to the AspectJ
486: * layer.
487: *
488: * @author Per S Hustad
489: */
490: private static final class MockExecutorWrapper implements
491: MockExecutor {
492:
493: private Object mockObject;
494:
495: public MockExecutorWrapper(Object o) {
496: mockObject = o;
497: }
498:
499: public Object execute(Object[] args) {
500: return mockObject;
501: }
502:
503: }
504:
505: }
|