| 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 // TODO(gram): Replace all this with: |
| 179 // var rtn = reflect(value).apply(invocation.positionalArguments, |
| 180 // invocation.namedArguments); |
| 181 // once that is supported. |
| 182 var rtn; |
| 183 switch (args.length) { |
| 184 case 0: |
| 185 rtn = value(); |
| 186 break; |
| 187 case 1: |
| 188 rtn = value(args[0]); |
| 189 break; |
| 190 case 2: |
| 191 rtn = value(args[0], args[1]); |
| 192 break; |
| 193 case 3: |
| 194 rtn = value(args[0], args[1], args[2]); |
| 195 break; |
| 196 case 4: |
| 197 rtn = value(args[0], args[1], args[2], args[3]); |
| 198 break; |
| 199 case 5: |
| 200 rtn = value(args[0], args[1], args[2], args[3], args[4]); |
| 201 break; |
| 202 case 6: |
| 203 rtn = value(args[0], args[1], args[2], args[3], |
| 204 args[4], args[5]); |
| 205 break; |
| 206 case 7: |
| 207 rtn = value(args[0], args[1], args[2], args[3], |
| 208 args[4], args[5], args[6]); |
| 209 break; |
| 210 case 8: |
| 211 rtn = value(args[0], args[1], args[2], args[3], |
| 212 args[4], args[5], args[6], args[7]); |
| 213 break; |
| 214 case 9: |
| 215 rtn = value(args[0], args[1], args[2], args[3], |
| 216 args[4], args[5], args[6], args[7], args[8]); |
| 217 break; |
| 218 case 9: |
| 219 rtn = value(args[0], args[1], args[2], args[3], |
| 220 args[4], args[5], args[6], args[7], args[8], args[9]); |
| 221 break; |
| 222 default: |
| 223 throw new Exception( |
| 224 "Cannot proxy calls with more than 10 parameters."); |
| 225 } |
| 226 if (_logging && b.logging) { |
| 227 log.add(new LogEntry(name, method, args, action, rtn)); |
| 228 } |
| 229 return rtn; |
| 230 } |
| 231 } |
| 232 } |
| 233 if (matchedMethodName) { |
| 234 // User did specify behavior for this method, but all the |
| 235 // actions are exhausted. This is considered an error. |
| 236 throw new Exception('No more actions for method ' |
| 237 '${qualifiedName(name, method)}.'); |
| 238 } else if (_throwIfNoBehavior) { |
| 239 throw new Exception('No behavior specified for method ' |
| 240 '${qualifiedName(name, method)}.'); |
| 241 } |
| 242 // Otherwise user hasn't specified behavior for this method; we don't throw |
| 243 // so we can underspecify. |
| 244 if (_logging) { |
| 245 log.add(new LogEntry(name, method, args, Action.IGNORE)); |
| 246 } |
| 247 } |
| 248 |
| 249 /** [verifyZeroInteractions] returns true if no calls were made */ |
| 250 bool verifyZeroInteractions() { |
| 251 if (log == null) { |
| 252 // This means we created the mock with logging off and have never turned |
| 253 // it on, so it doesn't make sense to verify behavior on such a mock. |
| 254 throw new |
| 255 Exception("Can't verify behavior when logging was never enabled."); |
| 256 } |
| 257 return log.logs.length == 0; |
| 258 } |
| 259 |
| 260 /** |
| 261 * [getLogs] extracts all calls from the call log that match the |
| 262 * [logFilter], and returns the matching list of [LogEntry]s. If |
| 263 * [destructive] is false (the default) the matching calls are left |
| 264 * in the log, else they are removed. Removal allows us to verify a |
| 265 * set of interactions and then verify that there are no other |
| 266 * interactions left. [actionMatcher] can be used to further |
| 267 * restrict the returned logs based on the action the mock performed. |
| 268 * [logFilter] can be a [CallMatcher] or a predicate function that |
| 269 * takes a [LogEntry] and returns a bool. |
| 270 * |
| 271 * Typical usage: |
| 272 * |
| 273 * getLogs(callsTo(...)).verify(...); |
| 274 */ |
| 275 LogEntryList getLogs([CallMatcher logFilter, |
| 276 Matcher actionMatcher, |
| 277 bool destructive = false]) { |
| 278 if (log == null) { |
| 279 // This means we created the mock with logging off and have never turned |
| 280 // it on, so it doesn't make sense to get logs from such a mock. |
| 281 throw new |
| 282 Exception("Can't retrieve logs when logging was never enabled."); |
| 283 } else { |
| 284 return log.getMatches(name, logFilter, actionMatcher, destructive); |
| 285 } |
| 286 } |
| 287 |
| 288 /** |
| 289 * Useful shorthand method that creates a [CallMatcher] from its arguments |
| 290 * and then calls [getLogs]. |
| 291 */ |
| 292 LogEntryList calls(method, |
| 293 [arg0 = NO_ARG, |
| 294 arg1 = NO_ARG, |
| 295 arg2 = NO_ARG, |
| 296 arg3 = NO_ARG, |
| 297 arg4 = NO_ARG, |
| 298 arg5 = NO_ARG, |
| 299 arg6 = NO_ARG, |
| 300 arg7 = NO_ARG, |
| 301 arg8 = NO_ARG, |
| 302 arg9 = NO_ARG]) => |
| 303 getLogs(callsTo(method, arg0, arg1, arg2, arg3, arg4, |
| 304 arg5, arg6, arg7, arg8, arg9)); |
| 305 |
| 306 /** Clear the behaviors for the Mock. */ |
| 307 void resetBehavior() => _behaviors.clear(); |
| 308 |
| 309 /** Clear the logs for the Mock. */ |
| 310 void clearLogs() { |
| 311 if (log != null) { |
| 312 if (name == null) { // This log is not shared. |
| 313 log.logs.clear(); |
| 314 } else { // This log may be shared. |
| 315 log.logs = log.logs.where((e) => e.mockName != name).toList(); |
| 316 } |
| 317 } |
| 318 } |
| 319 |
| 320 /** Clear both logs and behavior. */ |
| 321 void reset() { |
| 322 resetBehavior(); |
| 323 clearLogs(); |
| 324 } |
| 325 } |
| OLD | NEW |