GriffonMock.groovy
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 }