View Javadoc

1   /* Copyright (c) 2005 Per S Hustad. All rights reserved.
2    *
3    * Redistribution and use in source and binary forms, with or without 
4    * modification, are permitted provided that the following conditions are met:
5    *
6    *  o Redistributions of source code must retain the above copyright notice, 
7    *    this list of conditions and the following disclaimer. 
8    *    
9    *  o Redistributions in binary form must reproduce the above copyright notice, 
10   *    this list of conditions and the following disclaimer in the documentation 
11   *    and/or other materials provided with the distribution. 
12   *    
13   *  o Neither the name of surrogate.sourceforge.net nor the names of 
14   *    its contributors may be used to endorse or promote products derived 
15   *    from this software without specific prior written permission. 
16   *    
17   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
18   * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 
19   * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 
20   * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 
21   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 
22   * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 
23   * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 
24   * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
25   * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 
26   * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 
27   * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28   */
29  package net.sf.surrogate.core;
30  
31  import java.lang.reflect.Constructor;
32  import java.lang.reflect.Method;
33  import java.util.Enumeration;
34  import java.util.Hashtable;
35  
36  import org.aspectj.lang.JoinPoint;
37  import org.aspectj.lang.Signature;
38  import org.aspectj.lang.reflect.CodeSignature;
39  import org.aspectj.lang.reflect.ConstructorSignature;
40  import org.aspectj.lang.reflect.MethodSignature;
41  
42  /***
43   * The link between Mock/ Unit Test objects and AspectJ code.
44   * <p> 
45   * The SurrogateManager is the common controller for Junit tests manipulating
46   * MockObjects and mock methods.
47   * <h4>Test Case View</h4>
48   * The SurrogateManager provides utility methods for test cases to control which
49   * Mock objects should be active for a particular test case run. These methods
50   * are normally:
51   * <pre>
52   *    {@link #getInstance()}
53   *    {@link #reset()}
54   *    {@link #addMock(Object)}
55   *    {@link #addMockMethod(MockMethod)}
56   *    {@link #removeMock(Object)}
57   *    {@link #removeMockMethod(MockMethod)}
58   * </pre>
59   * <h4>Aspect View</h4>
60   * Every time an aspect detects a "mock poincut", it will ask the
61   * SurrogateManager if a mock object has been registered for the corresponding
62   * joinpoint. If so, the mock executes instead of the "real" method. Otherwise,
63   * the real object is allowed to execute. The methods used by the aspects are
64   * normally:
65   * <pre>
66   *   {@link #getInstance()}
67   *   {@link #getMockExecutor(JoinPoint)}
68   * </pre>
69   * <h4>Test Case responsibility</h4>
70   * A test case should normally always call the {@link #reset}method before
71   * starting to use the SurrogateManager to avoid independence from other
72   * testcases which might have been run earlier from within the same VM.
73   * 
74   * <h4><a name="Debugging">Debugging </a></h4>
75   * If the System property "surrogate.debug" is set to "true", e.g. with the java
76   * <b>-Dsurrogate.debug=true </b> option, the SurrogateManager will report every
77   * time it is asked to resolve a JoinPoint into a mock object or method. The
78   * information is written to <code>System.out</code>. Each debug line is on
79   * the following format:
80   * <pre>
81   *   surrogate:&lt;joinpoint id&gt;|added|removed=&lt;mock id&gt;
82   *   
83   *   Example:
84   *   surrogate:added=public static native long java.lang.System.currentTimeMillis()
85   *   surrogate:added=net.sf.surrogate.example.MockFileWriter@38e059
86   *   surrogate:call(long java.lang.System.currentTimeMillis())=public static native long java.lang.System.currentTimeMillis()
87   *   surrogate:call(java.io.FileWriter(String))=net.sf.surrogate.example.MockFileWriter@38e059
88   *   surrogate:call(java.io.BufferedWriter(Writer))=null
89   * </pre>
90   * I.e. the unit test has added mocks for <code>currentTimeMillis</code> and
91   * the <code>FileWriter</code>. As expected, mocks were returned for the
92   * <code>currentTimeMillis</code> and <code>FileWriter</code>
93   * mockJoinPoint's but no mock was found for the</code> BufferedWriter</code>
94   * joinpoint (<code>null</code> was returned).
95   * 
96   * @see SurrogateCalls
97   * @see MockMethod
98   * @author Per S Hustad
99   */
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.hasMoreElements();) {
161             debug(REMOVED_MOCK, e2.nextElement());
162         }
163         allMockRefs.clear();
164     }
165 
166     /***
167      * Adds a Mock object to the manager for later lookup by Aspect code. 
168      * <p>
169      * Note that for objects to be substituted with their mock implementation,
170      * there must have been defined a <code>pointcut</code> intercepting the
171      * method call or method execution. Otherwise, the mock object will never be
172      * substituted, even if it has been registered. See {@link SurrogateCalls}
173      * for pointcut definition details.
174      * <p>
175      * Surrogate uses the <code>java.lang.Class.isAssignableFrom</code> to see
176      * whether the registered mock can subsitute a "real" object. It does this
177      * by looking up the declared return type signature of the method or the
178      * class signature of the constructor call.
179      * <p>
180      * Example usage:
181      * 
182      * <pre>
183      * 
184      *       SurrogateManager mm = SurrogateManager.getInstance();
185      *       mm.reset();
186      *       MockCustomerService mock = new MockCustomerService();
187      *       mm.addMock(mock);
188      *       mock.setGetCustomerReturnValue(&quot;MockCustomer&quot;);
189      *       ...
190      *  
191      * </pre>
192      * 
193      * @param o
194      *            the Mock object to add. Must be non-null.
195      * @return the Mock object given as argument
196      * @see #addMockMethod(MockMethod)
197      * @see #removeMock(Object)
198      * @see SurrogateCalls
199      */
200     public Object addMock(Object o) {
201         allMocks.put(o.getClass(), o);
202         debug(ADDED_MOCK, o.toString());
203         return o;
204     }
205 
206     /***
207      * Adds a mock method to the manager for later lookup by Aspect code.
208      * <p>
209      * Note that for objects to be substituted with their mock implementation,
210      * there must have been defined a <code>pointcut</code> intercepting the
211      * method call or method execution. Otherwise, the mock object will never be
212      * substituted, even if it has been registered. See {@link SurrogateCalls}
213      * for pointcut definition details.
214      * <p>
215      * Example usage:
216      * <pre>
217      *       SurrogateManager mm = SurrogateManager.getInstance();
218      *       mm.reset();
219      *       MockMethod mockTime = 
220      *            mm.addMockMethod(new MockMethod(System.class,&quot;currentTimeMillis&quot;));
221      *       mockTime.addReturnValue(1000L);
222      *       mockTime.addReturnValue(2000L);
223      *       mockTime.setExpectedCalls(2);
224      *       ... Call object using System.currentTimeMillis
225      *       mockTime.verify(); 
226      *       ...
227      * </pre>
228      * 
229      * @param m
230      *            the Mock object to add. Must be non-null.
231      * @return the MockMethod as given on input
232      * @see #addMock(Object)
233      * @see #removeMockMethod(MockMethod)
234      */
235     public MockMethod addMockMethod(MockMethod m) {
236         allMockRefs.put(m.getAccessibleObject(), m);
237         debug(ADDED_MOCK, m.toString());
238         return m;
239     }
240 
241     /***
242      * Removes a mock from the manager. The object will hence no longer be
243      * returned instead of a "real" object when the corresponding aspect advice
244      * executes
245      * 
246      * @param o
247      *            the object to remove.
248      * @return the removed object or <code>null</code> if the object was not
249      *         found
250      * @see #addMock(Object)
251      */
252     public Object removeMock(Object o) {
253         debug(REMOVED_MOCK, o.toString());
254         return allMocks.remove(o.getClass());
255     }
256 
257     /***
258      * Removes a mock method from the manager. The method will hence no longer
259      * execute instead of the "real" method when the corresponding aspect advice
260      * executes.
261      * 
262      * @param o
263      *            the object to remove.
264      * @return the removed method or <code>null</code> if the mock method was
265      *         not found.
266      * @see #addMockMethod(MockMethod)
267      */
268     public MockMethod removeMockMethod(MockMethod m) {
269         debug(REMOVED_MOCK, m.toString());
270         return (MockMethod) allMockRefs.remove(m.getAccessibleObject());
271     }
272 
273     /***
274      * Locates and gets a Mock object for an interface or a class. This method
275      * looks for a Mock object implementing the interface or the class
276      * assignable from <code>myClassOrInterface</code> by searching in the
277      * list of Mock objects for the first object which
278      * <code>myClassOrInterface</code> is assignable from
279      * 
280      * @param myClassOrInterface
281      *            the interface/class to find a mock object for
282      * @return the Mock object for the interface/class or <code>null</code> if
283      *         no such mock object has been registered via the
284      *         <code>addMock</code> method.
285      * @see #addMock(Object)
286      * @see Class#isAssignableFrom(java.lang.Class)
287      */
288     Object getMockObject(Class myClassOrInterface) {
289         for (Enumeration keys = allMocks.keys(); keys.hasMoreElements();) {
290             Class c = (Class) keys.nextElement();
291             if (myClassOrInterface.isAssignableFrom(c)) {
292                 return allMocks.get(c);
293             }
294         }
295         return null;
296     }
297 
298     /***
299      * Gets a registered <code>Mock method reference</code> for a method. This
300      * method should normally only be called from the aspect code.
301      * 
302      * @param m
303      *            the Method to check
304      * @return the registered Mock method reference or <code>null</code> if no
305      *         such reference exists for the method
306      * @see #addMockMethod(MockMethod)
307      */
308     MockMethod getMockMethod(Method m) {
309         return (MockMethod) allMockRefs.get(m);
310     }
311 
312     /***
313      * Gets a registered <code>Mock method reference</code> for a constructor
314      * 
315      * @param c
316      *            the constructor to check
317      * @return the registered Mock method reference or <code>null</code> if no
318      *         such reference exists for the method
319      * @see #addMockMethod(MockMethod)
320      */
321     MockMethod getMockConstructor(Constructor c) {
322         return (MockMethod) allMockRefs.get(c);
323     }
324 
325     /***
326      * Checks if an AspectJ "mock" joinpoint has an assoicated mock object
327      * registered by the manager.
328      * 
329      * @param p
330      *            the join point
331      * @return the corresponding mock object or <code>null</code> if no match
332      *         is found
333      */
334     protected Object getReturnedClassMock(JoinPoint p) throws IllegalArgumentException {
335         Signature sig = p.getSignature();
336         Class c = null;
337         if (sig instanceof MethodSignature) {
338             c = ((MethodSignature) sig).getReturnType();
339         } else if (sig instanceof ConstructorSignature) {
340             c = ((CodeSignature) sig).getDeclaringType();
341         } else {
342             throw new IllegalArgumentException("Unhandled Signature: " + sig.getClass().getName());
343         }
344         return getMockObject(c);
345     }
346 
347     /***
348      * Checks if an AspectJ "mock" joinpoint has an assoicated mock method
349      * registered by the manager.
350      * 
351      * @param p
352      *            the join point
353      * @return the corresponding mock method or <code>null</code> if no match
354      *         is found
355      * @throws IllegalArgumentException
356      *             if the joinpoint signature is not a "method" or "constructor"
357      *             signature
358      */
359     protected MockMethod getMockMethod(JoinPoint p) throws IllegalArgumentException, NoSuchMethodException {
360         MockMethod mockMethod;
361         CodeSignature sig = (CodeSignature) p.getSignature();
362         Class c = sig.getDeclaringType();
363         if (sig instanceof ConstructorSignature) {
364             Constructor constr = c.getDeclaredConstructor(sig.getParameterTypes());
365             mockMethod = getMockConstructor(constr);
366         } else if (sig instanceof MethodSignature) {
367             Method m = c.getDeclaredMethod(sig.getName(), sig.getParameterTypes());
368             mockMethod = getMockMethod(m);
369         } else {
370             throw new IllegalArgumentException("Unhandled Signature: " + sig.getClass().getName());
371         }
372         return mockMethod;
373 
374     }
375 
376     /***
377      * Gets the mock object or mock method for a joinpoint. This method should
378      * normally only be called from the corresponding
379      * <code>SurrogateCalls</code> advice but can also be called from other
380      * advices which might want to check if a mock matches the joinpoint.
381      * <p>
382      * The rules are as follows
383      * <ul>
384      * <li>If a <code>MockMethod</code> matches the joinpoint, this method is
385      * returned.
386      * <li>Otherwise, if a Mock object matches the joinpoint, this object is
387      * returned, wrapped behind a <code>MockExecutor</code>
388      * <li>Otherwise, <code>null</code> is returned.
389      * </ul>
390      * The advices should, if receiving a non-null <code>MockExecutor</code>
391      * return value, call the <code>MockExecutor.execute</code> with the
392      * <b>same </b> JoinPoint as used as arguments to
393      * <code>getMockExecutor</code>. If the returned
394      * <code>MockExecutor</code> is <code>null</code>, the advice should
395      * return a sensible value, e.g. with <code>return proceed();</code>
396      * 
397      * @param p
398      *            the <code>thisJoinPoint</code> on the advice executing
399      * @return the corresponding mock executor or <code>null</code> if no
400      *         match has been found with a registered mock object or mock method
401      * 
402      * @throws IllegalArgumentException
403      *             if the joinpoint signature is not a "constructor" or "method"
404      *             signature.
405      * 
406      * @throws NoSuchMethodException
407      *             if the joinpoint is inconsistent. This should normally be
408      *             considered as a bug.
409      * @see SurrogateCalls
410      * @see #addMock(Object)
411      * @see #addMockMethod(MockMethod)
412      */
413     public MockExecutor getMockExecutor(JoinPoint p) throws IllegalArgumentException, NoSuchMethodException {
414         MockMethod m = getMockMethod(p);
415         if (m != null) {
416             debug(p.toString(), m.toString());
417             return m;
418         }
419         Object o = getReturnedClassMock(p);
420         if (o != null) {
421             debug(p.toString(), o.toString());
422             return new MockExecutorWrapper(o);
423         }
424         debug(p.toString(), null);
425         return null;
426 
427     }
428 
429     /***
430      * Prints debug information if debug is enabled.
431      * 
432      * @param pred
433      *            the predicate
434      * @param mockId
435      *            the mock ID
436      * @see #Debugging
437      */
438     private void debug(String pred, Object mockId) {
439         if (isDebugEnabled) {
440             System.out.println("surrogate:" + pred + "=" + (mockId != null ? mockId : "null"));
441         }
442     }
443 
444     /***
445      * Defines, towards the AspectJ layer, the mock object or method to be
446      * executed.
447      * 
448      * @author Per S Hustad
449      */
450     public static interface MockExecutor {
451         /***
452          * If the manager returns a non-null MockExecutor, the calling advice
453          * should call "execute" and return the "execute" return value. The
454          * throwable can be cached and rethrown with the
455          * <code>ExceptionThrower</code> utility.
456          * 
457          * @param args
458          *            the arguments, always use
459          *            <code>thisJoinPoint.getArgs()<code>
460          *            Ignored if the underlying mock is a mock object but used
461          *            if it is a MockMethod.
462          * @return the return value of the underlying mock object. If the
463          *         underlying mock object is a <code>MockMethod</code>, the return value as
464          *         setup in the method is returned. If the underlying object is
465          *         a plain mock object, that object is returned
466          * @throws Throwable
467          *             if a <code>MockMethod</code> has been set up explicitly to throw an
468          *             exeption or there was a mismatch between the JoinPoint args and
469          *             the method args.
470          */
471         public Object execute(Object[] args) throws Throwable;
472     }
473 
474     /***
475      * Wrapper class to wrap a standard mock object as a MockExecutor. This
476      * class exists in order to provide a uniform interface to the AspectJ
477      * layer.
478      * 
479      * @author Per S Hustad
480      */
481     private static final class MockExecutorWrapper implements MockExecutor {
482 
483         private Object mockObject;
484 
485         public MockExecutorWrapper(Object o) {
486             mockObject = o;
487         }
488 
489         public Object execute(Object[] args) {
490             return mockObject;
491         }
492 
493     }
494 
495 }