1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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:<joinpoint id>|added|removed=<mock id>
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
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("MockCustomer");
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,"currentTimeMillis"));
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 }