Compile Save UI Binding based on Mockito Part 2
Geschrieben durch Mike Toggweiler
Remember my previous blog entry about creating compile/refactoring save ui binding based on Mockito and BeansBinding?
Today we'd like to make some improvements to the previous prototype. Mainly we'd like to add support for binding nested properties like
obj.getAdress().getStreet();
On the other hand we'd like to encapsulate the usage of Mockito into simple a helper class which may be needed in different projects. Therefore we investigated the Mockito implementation to trace the problem of binding nested properties and encapsulated the required parts into an own class called 'ObjectBinder'. The ObjectBinder is defined using generics to create a wrapper of any object. The ObjectBinder may be used with the following method calls to create a BeanProperty based on a real method call to the object classes stub.
ObjectBinder<DummyPojo> binder = new ObjectBinder<BeanBindingPrototype2.DummyPojo>(
DummyPojo.class);
binder.getModelTemplate().getName();
String path = binder.getBeanPropertyPath();
BeanProperty<DummyPojo, String> name = BeanProperty.create(path);
Using the so called ModelTemplate of the ObjectBinder creates a new Mock-Object of the provided class and starts the tracking. It is following the method calls to that object.
/**
* @return type template used to declare gui bindings
*/
public O getModelTemplate() {
MockSettingsImpl settings = (MockSettingsImpl)new MockSettingsImpl()
.defaultAnswer(new EXReturnsDeepStubs(m_mockingCache));
O mock = mockUtil.createMock(m_clazz, settings);
mockingProgress.mockingStarted(mock, m_clazz, settings);
return mock;
}
We use an own answer to keep track of all nested method called based on that mock.
private static class EXReturnsDeepStubs extends ReturnsDeepStubs {
private static final long serialVersionUID = 1880837724076720541L;
private final MockingCache m_cache;
private Map<Object, Object> m_follower = new HashMap<Object, Object>();
public EXReturnsDeepStubs(MockingCache cache) {
m_cache = cache;
}
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
// Class<?> clz = invocation.getMethod().getReturnType();
Object answer = super.answer(invocation);
// if (new MockCreationValidator().isTypeMockable(clz)) {
// return will be another mock
Object mock = invocation.getMock();
Object parent = m_follower.get(mock);
if (parent != null) {
m_cache.registerInvocation(parent, mock, invocation);
}
else {
m_cache.registerInvocation(mock, invocation);
}
// register the following mock
m_follower.put(answer, mock);
// }
return answer;
}
}
That is where we register every method invocation in a shared cache object instance:
public static class MockingCache {
public HashMap<Object, Stack<InvocationOnMock>> m_invocationCache = new HashMap<Object, Stack<InvocationOnMock>>();
public void registerInvocation(Object mock, InvocationOnMock invocation) {
Stack<InvocationOnMock> stack = m_invocationCache.get(mock);
if (stack == null) {
stack = new Stack<InvocationOnMock>();
m_invocationCache.put(mock, stack);
}
stack.push(invocation);
}
public void registerInvocation(Object parent, Object mock, InvocationOnMock invocation) {
Stack<InvocationOnMock> stack = m_invocationCache.get(parent);
if (stack == null) {
stack = new Stack<InvocationOnMock>();
}
stack.push(invocation);
m_invocationCache.put(mock, stack);
m_invocationCache.remove(parent);
}
public void clear() {
m_invocationCache.clear();
}
public Stack<InvocationOnMock> retrieveMockInvocationStack(Object mock) {
// m_invocationCache.get(mock);
return m_invocationCache.remove(mock);
}
}
Now the whole trick of getting deep method invocation stack after calling a nested property is fetching the actual stack of the shared cache.
public Stack<InvocationOnMock> getLatestInvocationStack() {
mockingProgress.stubbingStarted();
@SuppressWarnings("rawtypes")
OngoingStubbing stubbing = (OngoingStubbing)mockingProgress.pullOngoingStubbing();
Object mock = stubbing.getMock();
stubbing.thenCallRealMethod();
return m_mockingCache.retrieveMockInvocationStack(mock);
}
The stack of the method invocation will also get converted into an appropriate BeanProperty using a simple conversion mechanism:
public String getBeanPropertyPath() {
// convert stacktrace into bean property
Stack<InvocationOnMock> latestInvocationStack = getLatestInvocationStack();
StringBuffer buf = new StringBuffer();
for (InvocationOnMock invocation : latestInvocationStack) {
String methodName = invocation.getMethod().getName();
for (String prefix : PREFIXES) {
if (methodName.startsWith(prefix)) {
int len = prefix.length();
methodName = String.valueOf(methodName.charAt(len)).toLowerCase()
+ methodName.substring(len + 1);
break;
}
}
if (buf.length() > 0) {
buf.append('.');
}
buf.append(methodName);
}
return buf.toString();
}
public <S> BeanProperty<O, S> getBeanProperty() {
return BeanProperty.create(getBeanPropertyPath());
}
So now we've got a real BeanProperty of a nested method call using compile save method invocation. This BeanProperty can get used as in the first example binding this property to a property of a swing ui element like:
ObjectBinder<DummyPojo> binder = new ObjectBinder<BeanBindingPrototype2.DummyPojo>(
DummyPojo.class);
binder.getModelTemplate().getParent().getStreet();
String path = binder.getBeanPropertyPath();
BeanProperty<DummyPojo, String> street = BeanProperty.create(path);
//bean property refering to swing element
BeanProperty<JTextField, String> textProperty = BeanProperty.create("text");
JTextField field = new JTextField(20);
DummyPojo realObject = new DummyPojo();
ParentPojo parent = new ParentPojo();
realObject.setParent(parent);
//binding nested beanproperty of real object to textfield property
AutoBinding<DummyPojo, String, JTextField, String> binding = Bindings.createAutoBinding(
UpdateStrategy.READ_WRITE, realObject, name, field, textProperty);
binding.bind();
That's all. The drawbacks of this solution is still that we have to do a method call of the mocked template before fetching a bean property of the ObjectBinder. That means two sequential method call which are required in this order otherwise the mockito framework (clearly) will raise an exception.
In the next prototype I'd like to create a simple example of a base ui class that may be used to encapsulate the binding logic and provide a simple an easy interface to extending GUI classes.
The whole maven project can get downloaded from here.
Post your comment
Comments
-
It is my great pleasure to visit your website and to enjoy your excellent post here. I like that very much.
Posted by Debra, 10/02/2012 12:00pm (12 days ago)
-
I’m really loving the template/theme of this blog. It’s simple, yet effective. A lot of times it’s challenging to get that “perfect balance” between user friendliness and appearance. I must say that you’ve done a excellent job with this. Also, the blog loads very quick for me on Opera. Excellent Blog!
Posted by Max Joergen, 12/01/2012 1:18pm (1 month ago)
-
Hi,
Good hacking. However, I might be picky here, but I think It should be mentioned :
You should code your utility with API in mind, try to express in code how you want to use your API then code. I like the saying : We can't be lazy on API design.
For example for your use case, I might want to use it that way :
ObjectBinder<Car> carBinder = ObjectBinder.recordPojoProperties(Car.class);
Car car = carBinder.get();
// …
BeanProperty<Car, String> gearProperty = carBinder.asProperty(car.getGear());
BeanProperty<Car, String> minOilLevelProperty = carinder.asProperty(car.getEngine().getMinimumOilLevel());
// …
Set<BeanProperty<Car, String>> carProperties = carBinder.recordedProperties()
Internally your code should be more "auto-documenting". For example EXReturnDeepStubs don't express intent, I don't know what it does until I look directly at the implementation. In my very own opinion you shouldn't extend it, unless you have a very specific reason for doing that. MockingCache also lies a bit as it's intent is not really to "cache".
getBeanPropertyPath could be refactored. Especially in the loops to express in a more readable way what you want to do here. I think this is important as this code is tacky by definition, I mean you are using internals of Mockito, it should be rather well documented to be understandable by your coworkers or by the people that will need to maintain / extend it.
Don't use StringBuffer, in Java 5+ you should use instead StringBuilder, which doesn't have synchronized cost, in your case synchronization is not needed.
Pay attention to visibility, MockingCache exposes internal fields.
In a more general way you should pay attention to concurrent access, or at least state that your code should't be accessed in a concurrent way. The above snippet is supposed to be ran in the thread local context. This is important as UIs events could run in different threads.
Maybe such code could go to some open source library
--
BricePosted by brice, 30/11/2011 7:07am (3 months ago)
RSS feed for comments on this page | RSS feed for all comments
