| 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 |