001 /*
002 * Copyright 2008-2012 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016 package griffon.test
017
018 import groovy.mock.interceptor.Demand
019 import groovy.mock.interceptor.LooseExpectation
020 import groovy.mock.interceptor.StrictExpectation
021
022 /**
023 * <p>Provides similar behaviour to MockFor and StubFor, but uses
024 * ExpandoMetaClass to mock the methods. This means that it fits much
025 * better with the rest of the Griffon unit testing framework than the
026 * classes it replaces.</p>
027 * <p>Instances of this class support the exact same syntax as MockFor/
028 * StubFor with the addition of explicit support for static methods.
029 * For example:</p>
030 * <pre>
031 * def mockControl = new GriffonMock(MyDomainClass)
032 * mockControl.demand.save() {-> return true} // Instance method
033 * mockControl.demand.static.get() {id -> return null} // Static method
034 * ...
035 * mockControl.verify()
036 * </pre>
037 * <p>You can even create a mock instance of the target class by calling
038 * the {@link GriffonMock#createMock()} method.</p>
039 * <p>Note that you have to be careful when using this class directly
040 * because it uses ExpandoMetaClass to override methods. Any of the
041 * demanded methods will stay on the class for the life of the VM unless
042 * you either override them or save the previous meta-class and restore
043 * it at the end of the test. This is why you should use the
044 * {@link GriffonUnitTestCase#mockFor(Class)} method instead, since it
045 * handles the meta-class management automatically.</p>
046 */
047 class GriffonMock {
048
049 Class mockedClass
050 DemandProxy demand
051
052 /**
053 * Creates a new strict mock for the given class.
054 * @param clazz The class to mock.
055 */
056 GriffonMock(Class clazz) {
057 this(clazz, false)
058 }
059
060 /**
061 * Creates a new mock for the given class.
062 * @param clazz The class to mock.
063 * @param loose If <code>true</code>, a loose-expecation mock is
064 * created, otherwise the mock is strict.
065 */
066 GriffonMock(Class clazz, boolean loose) {
067 mockedClass = clazz
068 demand = new DemandProxy(clazz, loose)
069 }
070
071 /**
072 * Returns a "demand" object that supports the "control.demand.myMethod() {}" syntax.
073 */
074 DemandProxy getDemand() {
075 return demand
076 }
077
078 /**
079 * Creates a mock instance that can be passed as a collaborator to
080 * classes under test.
081 */
082 def createMock() {
083 // Interfaces need to be treated specially because you can't
084 // override interface methods via the interface's metaclass.
085 // So, we use the old-fashioned approach of putting the method
086 // implementations in the map as closures.
087 //
088 // Unfortunately, that populated map causes problems when mocking
089 // classes, so we leave it empty in that case. I have no idea
090 // why it doesn't work with the populated map.
091 def mock = mockedClass.isInterface() ? demand.instanceMethods : [:]
092 mock = mock.asType(mockedClass)
093
094 // If we're mocking a class rather than an interface, we don't
095 // want the real methods invoked at all. So, we override the
096 // "invokeMethod()" method so that if the method exists in the
097 // ExpandoMetaClass we call that one, otherwise we forward it
098 // to the expectation object which will throw an assertion
099 // failure.
100 if (!mockedClass.isInterface()) {
101 mockedClass.metaClass.invokeMethod = { String name, Object[] args ->
102 // Find an expando method with the same signature as the one being invoked.
103 List paramTypes = args.collect([]) { arg ->
104 arg != null ? arg.getClass() : null
105 }
106 def method = delegate.metaClass.expandoMethods.find { MetaMethod m ->
107 // First check the name
108 m.name == name &&
109 // Then the number of method arguments
110 m.parameterTypes.size() == paramTypes.size() &&
111 // And finally the argument types
112 (0..<m.parameterTypes.size()).every { n ->
113 paramTypes[n] == null || m.parameterTypes[n].cachedClass.isAssignableFrom(paramTypes[n])
114 }
115 }
116
117 if (method) {
118 // We found an expando method with the required signature,
119 // so just call it.
120 return method.doMethodInvoke(delegate, args)
121 }
122
123 // No expando method found so pass the invocation on
124 // to the expectation object. This should throw an
125 // assertion error.
126 demand.expectation.match(name)
127 }
128 }
129
130 return mock
131 }
132
133 /**
134 * Checks that all the expected methods have been called. Throws an
135 * assertion failure if any expected method call has not occurred.
136 */
137 def verify() {
138 demand.expectation.verify()
139 }
140 }
141
142 /**
143 * Keeps track of demands and expectations for a particular Griffon mock.
144 */
145 class DemandProxy {
146
147 Class mockedClass
148 Demand demand = new Demand()
149 Object expectation
150 boolean isStatic
151
152 /** Keeps a map of instance methods added via the mock.demand... syntax. */
153 Map instanceMethods = [:]
154
155 DemandProxy(Class mockedClass, boolean loose) {
156 this.mockedClass = mockedClass
157 if (loose) {
158 expectation = new LooseExpectation(demand)
159 }
160 else {
161 expectation = new StrictExpectation(demand)
162 }
163 }
164
165 def invokeMethod(String methodName, Object args) {
166 demand.invokeMethod(methodName, args)
167
168 def c = new MockClosureProxy(args[-1], methodName, expectation)
169 if (isStatic) {
170 mockedClass.metaClass.static."${methodName}" = c
171 }
172 else {
173 mockedClass.metaClass."${methodName}" = c
174
175 // We keep track of the instance methods in a map so that
176 // GriffonMock can use that map as the implementation of an
177 // interface. Of course, this approach doesn't work with
178 // overloaded methods.
179 instanceMethods[methodName] = c
180 }
181 }
182
183 def getStatic() {
184 isStatic = true
185 return this
186 }
187 }
|