OLD | NEW |
| (Empty) |
1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file | |
2 // for details. All rights reserved. Use of this source code is governed by a | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 library mock.mock; | |
6 | |
7 // TOOD(kevmoo): just use `Map` | |
8 import 'dart:collection' show LinkedHashMap; | |
9 import 'dart:mirrors'; | |
10 | |
11 import 'package:matcher/matcher.dart'; | |
12 | |
13 import 'action.dart'; | |
14 import 'behavior.dart'; | |
15 import 'call_matcher.dart'; | |
16 import 'log_entry.dart'; | |
17 import 'log_entry_list.dart'; | |
18 import 'responder.dart'; | |
19 import 'util.dart'; | |
20 | |
21 /** The base class for all mocked objects. */ | |
22 @proxy | |
23 class Mock { | |
24 /** The mock name. Needed if the log is shared; optional otherwise. */ | |
25 final String name; | |
26 | |
27 /** The set of [Behavior]s supported. */ | |
28 final LinkedHashMap<String, Behavior> _behaviors; | |
29 | |
30 /** How to handle unknown method calls - swallow or throw. */ | |
31 final bool _throwIfNoBehavior; | |
32 | |
33 /** For spys, the real object that we are spying on. */ | |
34 final Object _realObject; | |
35 | |
36 /** The [log] of calls made. Only used if [name] is null. */ | |
37 LogEntryList log; | |
38 | |
39 /** Whether to create an audit log or not. */ | |
40 bool _logging; | |
41 | |
42 bool get logging => _logging; | |
43 set logging(bool value) { | |
44 if (value && log == null) { | |
45 log = new LogEntryList(); | |
46 } | |
47 _logging = value; | |
48 } | |
49 | |
50 /** | |
51 * Default constructor. Unknown method calls are allowed and logged, | |
52 * the mock has no name, and has its own log. | |
53 */ | |
54 Mock() : | |
55 _throwIfNoBehavior = false, log = null, name = null, _realObject = null, | |
56 _behaviors = new LinkedHashMap<String,Behavior>() { | |
57 logging = true; | |
58 } | |
59 | |
60 /** | |
61 * This constructor makes a mock that has a [name] and possibly uses | |
62 * a shared [log]. If [throwIfNoBehavior] is true, any calls to methods | |
63 * that have no defined behaviors will throw an exception; otherwise they | |
64 * will be allowed and logged (but will not do anything). | |
65 * If [enableLogging] is false, no logging will be done initially (whether | |
66 * or not a [log] is supplied), but [logging] can be set to true later. | |
67 */ | |
68 Mock.custom({this.name, | |
69 this.log, | |
70 throwIfNoBehavior: false, | |
71 enableLogging: true}) | |
72 : _throwIfNoBehavior = throwIfNoBehavior, _realObject = null, | |
73 _behaviors = new LinkedHashMap<String,Behavior>() { | |
74 if (log != null && name == null) { | |
75 throw new Exception("Mocks with shared logs must have a name."); | |
76 } | |
77 logging = enableLogging; | |
78 } | |
79 | |
80 /** | |
81 * This constructor creates a spy with no user-defined behavior. | |
82 * This is simply a proxy for a real object that passes calls | |
83 * through to that real object but captures an audit trail of | |
84 * calls made to the object that can be queried and validated | |
85 * later. | |
86 */ | |
87 Mock.spy(this._realObject, {this.name, this.log}) | |
88 : _behaviors = null, | |
89 _throwIfNoBehavior = true { | |
90 logging = true; | |
91 } | |
92 | |
93 /** | |
94 * [when] is used to create a new or extend an existing [Behavior]. | |
95 * A [CallMatcher] [filter] must be supplied, and the [Behavior]s for | |
96 * that signature are returned (being created first if needed). | |
97 * | |
98 * Typical use case: | |
99 * | |
100 * mock.when(callsTo(...)).alwaysReturn(...); | |
101 */ | |
102 Behavior when(CallMatcher logFilter) { | |
103 String key = logFilter.toString(); | |
104 if (!_behaviors.containsKey(key)) { | |
105 Behavior b = new Behavior(logFilter); | |
106 _behaviors[key] = b; | |
107 return b; | |
108 } else { | |
109 return _behaviors[key]; | |
110 } | |
111 } | |
112 | |
113 /** | |
114 * This is the handler for method calls. We loop through the list | |
115 * of [Behavior]s, and find the first match that still has return | |
116 * values available, and then do the action specified by that | |
117 * return value. If we find no [Behavior] to apply an exception is | |
118 * thrown. | |
119 */ | |
120 noSuchMethod(Invocation invocation) { | |
121 var method = MirrorSystem.getName(invocation.memberName); | |
122 var args = invocation.positionalArguments; | |
123 if (invocation.isGetter) { | |
124 method = 'get $method'; | |
125 } else if (invocation.isSetter) { | |
126 method = 'set $method'; | |
127 // Remove the trailing '='. | |
128 if (method[method.length - 1] == '=') { | |
129 method = method.substring(0, method.length - 1); | |
130 } | |
131 } | |
132 if (_behaviors == null) { // Spy. | |
133 var mirror = reflect(_realObject); | |
134 try { | |
135 var result = mirror.delegate(invocation); | |
136 log.add(new LogEntry(name, method, args, Action.PROXY, result)); | |
137 return result; | |
138 } catch (e) { | |
139 log.add(new LogEntry(name, method, args, Action.THROW, e)); | |
140 throw e; | |
141 } | |
142 } | |
143 bool matchedMethodName = false; | |
144 Map matchState = {}; | |
145 for (String k in _behaviors.keys) { | |
146 Behavior b = _behaviors[k]; | |
147 if (b.matcher.nameFilter.matches(method, matchState)) { | |
148 matchedMethodName = true; | |
149 } | |
150 if (b.matches(method, args)) { | |
151 List actions = b.actions; | |
152 if (actions == null || actions.length == 0) { | |
153 continue; // No return values left in this Behavior. | |
154 } | |
155 // Get the first response. | |
156 Responder response = actions[0]; | |
157 // If it is exhausted, remove it from the list. | |
158 // Note that for endlessly repeating values, we started the count at | |
159 // 0, so we get a potentially useful value here, which is the | |
160 // (negation of) the number of times we returned the value. | |
161 if (--response.count == 0) { | |
162 actions.removeRange(0, 1); | |
163 } | |
164 // Do the response. | |
165 Action action = response.action; | |
166 var value = response.value; | |
167 if (action == Action.RETURN) { | |
168 if (_logging && b.logging) { | |
169 log.add(new LogEntry(name, method, args, action, value)); | |
170 } | |
171 return value; | |
172 } else if (action == Action.THROW) { | |
173 if (_logging && b.logging) { | |
174 log.add(new LogEntry(name, method, args, action, value)); | |
175 } | |
176 throw value; | |
177 } else if (action == Action.PROXY) { | |
178 var mir = reflect(value) as ClosureMirror; | |
179 var rtn = mir.invoke(#call, invocation.positionalArguments, | |
180 invocation.namedArguments).reflectee; | |
181 if (_logging && b.logging) { | |
182 log.add(new LogEntry(name, method, args, action, rtn)); | |
183 } | |
184 return rtn; | |
185 } | |
186 } | |
187 } | |
188 if (matchedMethodName) { | |
189 // User did specify behavior for this method, but all the | |
190 // actions are exhausted. This is considered an error. | |
191 throw new Exception('No more actions for method ' | |
192 '${qualifiedName(name, method)}.'); | |
193 } else if (_throwIfNoBehavior) { | |
194 throw new Exception('No behavior specified for method ' | |
195 '${qualifiedName(name, method)}.'); | |
196 } | |
197 // Otherwise user hasn't specified behavior for this method; we don't throw | |
198 // so we can underspecify. | |
199 if (_logging) { | |
200 log.add(new LogEntry(name, method, args, Action.IGNORE)); | |
201 } | |
202 } | |
203 | |
204 /** [verifyZeroInteractions] returns true if no calls were made */ | |
205 bool verifyZeroInteractions() { | |
206 if (log == null) { | |
207 // This means we created the mock with logging off and have never turned | |
208 // it on, so it doesn't make sense to verify behavior on such a mock. | |
209 throw new | |
210 Exception("Can't verify behavior when logging was never enabled."); | |
211 } | |
212 return log.logs.length == 0; | |
213 } | |
214 | |
215 /** | |
216 * [getLogs] extracts all calls from the call log that match the | |
217 * [logFilter], and returns the matching list of [LogEntry]s. If | |
218 * [destructive] is false (the default) the matching calls are left | |
219 * in the log, else they are removed. Removal allows us to verify a | |
220 * set of interactions and then verify that there are no other | |
221 * interactions left. [actionMatcher] can be used to further | |
222 * restrict the returned logs based on the action the mock performed. | |
223 * [logFilter] can be a [CallMatcher] or a predicate function that | |
224 * takes a [LogEntry] and returns a bool. | |
225 * | |
226 * Typical usage: | |
227 * | |
228 * getLogs(callsTo(...)).verify(...); | |
229 */ | |
230 LogEntryList getLogs([CallMatcher logFilter, | |
231 Matcher actionMatcher, | |
232 bool destructive = false]) { | |
233 if (log == null) { | |
234 // This means we created the mock with logging off and have never turned | |
235 // it on, so it doesn't make sense to get logs from such a mock. | |
236 throw new | |
237 Exception("Can't retrieve logs when logging was never enabled."); | |
238 } else { | |
239 return log.getMatches(name, logFilter, actionMatcher, destructive); | |
240 } | |
241 } | |
242 | |
243 /** | |
244 * Useful shorthand method that creates a [CallMatcher] from its arguments | |
245 * and then calls [getLogs]. | |
246 */ | |
247 LogEntryList calls(method, | |
248 [arg0 = NO_ARG, | |
249 arg1 = NO_ARG, | |
250 arg2 = NO_ARG, | |
251 arg3 = NO_ARG, | |
252 arg4 = NO_ARG, | |
253 arg5 = NO_ARG, | |
254 arg6 = NO_ARG, | |
255 arg7 = NO_ARG, | |
256 arg8 = NO_ARG, | |
257 arg9 = NO_ARG]) => | |
258 getLogs(callsTo(method, arg0, arg1, arg2, arg3, arg4, | |
259 arg5, arg6, arg7, arg8, arg9)); | |
260 | |
261 /** Clear the behaviors for the Mock. */ | |
262 void resetBehavior() => _behaviors.clear(); | |
263 | |
264 /** Clear the logs for the Mock. */ | |
265 void clearLogs() { | |
266 if (log != null) { | |
267 if (name == null) { // This log is not shared. | |
268 log.logs.clear(); | |
269 } else { // This log may be shared. | |
270 log.logs = log.logs.where((e) => e.mockName != name).toList(); | |
271 } | |
272 } | |
273 } | |
274 | |
275 /** Clear both logs and behavior. */ | |
276 void reset() { | |
277 resetBehavior(); | |
278 clearLogs(); | |
279 } | |
280 } | |
OLD | NEW |