First, let's start with the enhanced StaticMockFor class itself:
/** * Use this class to mock static methods in a similar way to * Groovy's MockFor implementation (which does not support * mocking of static methods). * * Note that this implementation does not support demand * ranges, unlike Groovy's MockFor. * * @author Gerardo Viedma */ class StaticMockFor { final Class clazz //final Demand demand = new Demand() final OrderedDemand demand = new OrderedDemand() StaticMockFor(Class clazz) { this.clazz = clazz } def use(Closure clo) { try { // override with mock behavior clazz.metaClass.static.invokeMethod = { String name, args -> demand.invoke(name, args) } // execute the closure clo() // verify that we satisfied all the demands demand.verify() } finally { // reset to the original invokeMethod after verifying mocks clazz.metaClass.static.invokeMethod = { String name, args -> def original = clazz.metaClass.getStaticMetaMethod(name, args) if (original) original.invoke(name, args) else throw new RuntimeException("Method $name not found!") } } } }
Note that the StaticMockFor implementation relies on the OrderedDemand type to verify cardinality and ordering of method calls to the mocked class. OrderedDemand leverages Groovy's Expando mechanism to add dynamic behavior allowing us to override the mocked static methods.
/** * Encapsulates demands for instances of StaticMockFor. * The implementation is based on a queue of demanded * method invokations that have to be met in order * and with the specified cardinality. * * @author Gerardo Viedma */ class OrderedDemand { final Map closures = [:] // maintain calls in order final Queue calls = [] as Queue // maps method name to an integer count final Map actualCount = [:] // maps method name to a range final Map expectedCount = [:] // add dynamic method behavior static { OrderedDemand.metaClass.invokeMethod = { String name, args -> def metaMethod = OrderedDemand.metaClass.getMetaMethod(name, args) // pass on calls to invoke and verify if(metaMethod) { return metaMethod.invoke(delegate,args) } // add methods dynamically and keep track of their counts def range Closure clo if (args.size() == 1) { range = 1..1 clo = args[0] } else { range = args[0] if (!(range instanceof Range)) range = range..range clo = args[1] } // repeat the methods as many times as necessary calls.add(name) actualCount.put(name, 0) expectedCount.put(name, range) closures.put(name, clo) } } /* * Invokes a method, removing it from the demand queue * if it was the next invokation in the demand queue. * Otherwise, throws an assertion failure. */ def invoke(String name, args) { if (calls.isEmpty()) throw new RuntimeException("Did not expect any calls to $name") def head = calls.peek() if (name == head) { def rslt = closures[name](args) // update the count def actual = actualCount.get(name) + 1 actualCount.put(name, actual) return rslt } else { verify() // updated the head, so call recursively invoke(name, args) } } /* * Verifies that all demands have been met by ensuring * that all demanded methods were invoked. */ def verify() { def head = calls.remove() def expected = expectedCount.get(head) def actual = actualCount.get(head) if (!expected.contains(actual)) { throw new RuntimeException("Incorrect number of calls to $head") } } }
Finally, we can demonstrate usage of the StaticMockFor class and verify its behavior by writing some unit tests:
/** * Tests functionality of StaticMockFor instances. * * @author Gerardo Viedma */ class StaticMockForTest extends GroovyTestCase { static final NOT_SO_RANDOM = 0.5 void testSingleCall() { def mathMock = new StaticMockFor(Math) mathMock.demand.random { println 'Mocking Math.random()' NOT_SO_RANDOM } mathMock.use { assertEquals Math.random(), NOT_SO_RANDOM } assertFalse Math.random() == NOT_SO_RANDOM } void testMultipleCalls() { def mathMock = new StaticMockFor(Math) mathMock.demand.random(3) { println 'Mocking Math.random()' NOT_SO_RANDOM } mathMock.use { assertEquals Math.random(), NOT_SO_RANDOM assertEquals Math.random(), NOT_SO_RANDOM assertEquals Math.random(), NOT_SO_RANDOM } assertFalse Math.random() == NOT_SO_RANDOM } void testCallRange() { def mathMock = new StaticMockFor(Math) mathMock.demand.random(2..4) { println 'Mocking Math.random()' NOT_SO_RANDOM } mathMock.use { assertEquals Math.random(), NOT_SO_RANDOM assertEquals Math.random(), NOT_SO_RANDOM assertEquals Math.random(), NOT_SO_RANDOM } assertFalse Math.random() == NOT_SO_RANDOM } void testMissingCalls() { def mathMock = new StaticMockFor(Math) mathMock.demand.random(4) { println 'Mocking Math.random()' NOT_SO_RANDOM } shouldFail { mathMock.use { assertEquals Math.random(), NOT_SO_RANDOM assertEquals Math.random(), NOT_SO_RANDOM assertEquals Math.random(), NOT_SO_RANDOM } } assertFalse Math.random() == NOT_SO_RANDOM } void testMissingCallsInRange() { def mathMock = new StaticMockFor(Math) mathMock.demand.random(2..4) { println 'Mocking Math.random()' NOT_SO_RANDOM } shouldFail { mathMock.use { assertEquals Math.random(), NOT_SO_RANDOM } } assertFalse Math.random() == NOT_SO_RANDOM } void testExceededCalls() { def mathMock = new StaticMockFor(Math) mathMock.demand.random(2) { println 'Mocking Math.random()' NOT_SO_RANDOM } shouldFail { mathMock.use { assertEquals Math.random(), NOT_SO_RANDOM assertEquals Math.random(), NOT_SO_RANDOM assertEquals Math.random(), NOT_SO_RANDOM } } assertFalse Math.random() == NOT_SO_RANDOM } void testExceededCallsInRange() { def mathMock = new StaticMockFor(Math) mathMock.demand.random(1..2) { println 'Mocking Math.random()' NOT_SO_RANDOM } shouldFail { mathMock.use { assertEquals Math.random(), NOT_SO_RANDOM assertEquals Math.random(), NOT_SO_RANDOM assertEquals Math.random(), NOT_SO_RANDOM } } assertFalse Math.random() == NOT_SO_RANDOM } void testCorrectCallOrder() { def mathMock = new StaticMockFor(Math) mathMock.demand.random { println 'Mocking Math.random()' NOT_SO_RANDOM } mathMock.demand.abs { a -> println 'Mocking Math.abs()' (a > 0) ? a : -a } mathMock.use { assertEquals Math.random(), NOT_SO_RANDOM assertEquals Math.abs(NOT_SO_RANDOM), NOT_SO_RANDOM } } void testWrongCallOrder() { def mathMock = new StaticMockFor(Math) mathMock.demand.random { println 'Mocking Math.random()' NOT_SO_RANDOM } mathMock.demand.abs { a -> println 'Mocking Math.abs()' (a > 0) ? a : -a } shouldFail { mathMock.use { assertEquals Math.abs(NOT_SO_RANDOM), NOT_SO_RANDOM assertEquals Math.random(), NOT_SO_RANDOM } } } }
No comments:
Post a Comment