griffon/test/GriffonMock.groovy

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