I would like to share some nice feature I am using extensively to test Groovy code. I am going to make this post short and hopefully useful.
Mocking in Groovy
There are tons of possiblities to mock in Groovy: groovy metaprogramming. Groovy metaprogramming is one of the features which stands out for sure. We are talking about runtime and compile time one. There are others features like: ability to use static and dynamic types, ability to use functional and oo paradigm and also almost full Java compatiblity so you can write in Java if you need for some reason. Groovy is JVM language, so it works wherever JVM works: if you never tried you should at least take a look. It's been years I have been using it and I am still impressed. Ok, I really want to make it short...
StubFor/MockFor example
So, to selectively mock a method it seems one of the most popular way to do it (aside ExpandoMetaClass) is to use mockFor/stubFor classes.
Given is the class under test:
class ClassUnderTest {
def start(){
println "starting"
inner()
}
def inner(){
return "original start message"
}
def stop(){
println "stopping"
return "original stop message"
}
}
When inner method is to be mocked to return modified return value, we can try to test it like this:
void "test: stubFor use"() {
given:
def stub = new StubFor(ClassUnderTest)
stub.demand.with {
inner{ return "mocked start" }
}
stub.ignore("start")
stub.ignore("stop")
when:
stub.use {
def classUnderTest = new ClassUnderTest()
def startResult = classUnderTest.start()
def stopResult = classUnderTest.stop()
then:
assert startResult == "mocked start"
assert stopResult == "original stop message"
}
}
And... it doesn't work. I put inner method on purpose in ClassUnderTest as it cannot be mocked in this way. The above test fails:
Assertion failed:
assert startResult == "mocked start"
| |
| false
'original start message'
It happens so as when ignoring specific method, all underlying calls seem to be ignored as well. I stumbled upon this so many times I needed another solution.
ProxyMetaClass
ProxyMetaClass extends MetaClassImpl so it is a sibling for ExpandoMetaClass. I put the link for Expando but not for ProxyMetaClass as for some reason there is no decent documentation for it.
Anyway, it turns out this is very handy way for mocking things in Groovy, which works as needed:
void "test: ProxyMetaClass use"() {
given:
ProxyMetaClass clsUnderTestSpy = ProxyMetaClass.getInstance(ClassUnderTest)
clsUnderTestSpy.interceptor = new GroovySpy([inner: "mocked start"])
when:
clsUnderTestSpy.use {
def classUnderTest = new ClassUnderTest()
def startResult = classUnderTest.start()
def stopResult = classUnderTest.stop()
then:
assert startResult == "mocked start"
assert stopResult == "original stop message"
}
}
With GroovySpy class:
class GroovySpy implements Interceptor {
Map mockMap = [:]
boolean invokeMethod = true
GroovySpy(map) {
mockMap = map
}
Object beforeInvoke(Object obj, String methodName, Object[] args) {
if (mockMap.any { entry -> entry.key == methodName }) {
invokeMethod = false // do not invoke if method is mocked
}
}
boolean doInvoke() {
return invokeMethod
}
Object afterInvoke(Object obj, String methodName, Object[] args, Object res) {
invokeMethod = true // restore variable value
if (mockMap.any { entry -> entry.key == methodName }) {
return mockMap[methodName] // return mocked value
}
return res // or return original result if method is not mocked
}
}
We can provide multiple methods in a map and they will be mocked while the others will be run as originals.
There is huge amount of information about ExpandoMetaClass around so I do not put anything related here. Also, I authored Jenkins testing framework where I extensively use ExpandoMetaClass to mock everything in pipeline file (which is beyond default Groovy syntax) so you could take a look as well: Jenkinson
Summary
Groovy is extremely flexible and handy in most of the situations. ProxyMetaClass is great way for mocking but lacks documentation or even useful examples that is why I think above information should be useful one. At least it helped me a lot in my tasks.
Top comments (0)