Re: JMock map expectations
Thank you for the reply,
After you said it's not possible to make it nice, I thought about it a
bit and would like to propose a couple of alternative solutions. It's
going to be wordy, but I coudn't leave anything out - java generics is
not an easy stuff.
So, the inital idea of JMock was to use existing hamcrest matchers for
specifying expectations by wrapping them in with(). But a problem
arises with "generic" matchers (I didn't mean java generics here, I
meant matchers for base types and matchers with wildcards), like:
public static <K, V> Matcher<Map<? extends K, ? extends V>> hasEntry(K
param1, V param2)
public static <T> Matcher<java.lang.Iterable<? extends T>>
containsInAnyOrder(T... param1)
if we use these matchers in expectations like:
oneOf(service).doSomethingWithAMap(with(hasEntry("test", "test")));
oneOf(service).doSomethingWithAList(with(containsInAnyOrder("test")));
the code won't compile, because with() inferred types as:
Map<? extends String,? extends String>
Iterable<? extends String>
which our service does not accept. It wants something more specific:
public interface Service {
void doSomethingWithAMap(Map<String, String> map);
void doSomethingWithAList(List<String> list);
}
The problem is that methods hasEntry() and containsInAnyOrder() infer
container parameters, but specify container types explicitly.
So even if we try to use java "infer from assignment" feature:
Matcher<Map<String, String>> mapMatcher = hasEntry("test", "test");
oneOf(service).doSomethingWithAMap(with(mapMatcher));
it won't compile, since hasEntry() returns Map<? extends String,?
extends String>.
But we can add another level of indirection to infer actual map type,
so we could pass it to doSomethingWithAMap() method.
Consider this method:
public <T> Matcher<T> wrap(Matcher<? super T> matcher) {
return new LooseMatcherWrapper<T>(matcher);
}
where LooseMatcherWrapper simply delegates all calls to the
constructor parameter.
public class LooseMatcherWrapper<T> extends BaseMatcher<T> {
private Matcher<? super T> baseMatcher;
public LooseMatcherWrapper(Matcher<? super T> baseMatcher) {
this.baseMatcher = baseMatcher;
}
<at> Override
public boolean matches(Object item) {
return baseMatcher.matches(item);
}
<at> Override
public void describeMismatch(Object item, Description description) {
super.describeMismatch(item, description);
}
public void describeTo(Description description) {
baseMatcher.describeTo(description);
}
}
With this wrapper we can infer actual type on assignment:
Matcher<Map<String, String>> matcher =
wrap(hasEntry("test", "test"));
oneOf(service).doSomethingWithAMap(with(matcher));
or specify type explicitly:
oneOf(service).doSomethingWithAMap(with(TestUtil.<Map<String,
String>>wrap(hasEntry("test", "test"))));
All compiles and works. Not that smooth, due to java type inference
working only on assignment, but not from arguments of a method in
which generic method is called. So that seems to be the most of what
we can do with the wrap() method.
An alternative is to utilize with() method for type inference. Here I
added with_() method to custom expectations class (we don't wanna
touch existing with() method, so not to ruin its type inference
capabilities):
class MyExpectations extends Expectations {
public <T> T with_(Matcher<? super T> matcher) {
currentBuilder().addParameterMatcher(matcher);
return null;
}
}
Again, we can infer map type implicitly on assignment:
context.checking(new MyExpectations() {{
Map<String, String> map = with_(hasEntry("test", "test"));
oneOf(service).doSomethingWithAMap(map);
}});
or specify it explicitly:
context.checking(new MyExpectations() {{
oneOf(service).doSomethingWithAMap(this.<Map<String,
String>>with_(hasEntry("test", "test")));
}});
(actually, current Expectations implementation doesn't allow calling
with() before oneOf(), so the first example will throw
RuntimeException)
So, my suggestion is to add a method to JMock that allows downcasting
via type inference, so we could utilize "generic" matchers like
hasEntry() and containsInAnyOrder() for expectations:
public <T> T method(Matcher<? super T> matcher)
Regards,
Konstantin Fadeyev
> 28.05.2012, 13:33, "Steve Freeman" <steve@...>:
>
> sorry for the delay, replied on StackOverflow
>
> http://stackoverflow.com/questions/10704443/jmock-map-expectations/10782333#10782333
>
> There isn't a good answer to this as we hit the limits of Java generics. There's a tension between the
generics we need for jMock and what we need for assertThat()
>
> I tend to add a helper method, with an expressive name, to force the types.
>
> <at> Test public void test() {
> context.checking(new Expectations(){{
> oneOf(service).doSomething(with(mapIncluding("test", "test")));
> }});
>
> main.run();
> }
>
> <at> SuppressWarnings({"unchecked", "rawtypes"})
> private Matcher<Map<String, String>> mapIncluding(String key, String value) {
> return (Matcher)Matchers.hasEntry(key, value);
> };
>
> Yes, this is pig-ugly. I can only apologise that this is the best we appear to be able to do. That said, it's
rare that I have to go as far as turning off the types, I can give it a name that's meaningful in the domain, and
I've localised the unchecking to the helper method.
> ---------------------------------------------------------------------
> To unsubscribe from this list, please visit:
>
> http://xircles.codehaus.org/manage_email
> -------- Завершение пересылаемого сообщения --------
---------------------------------------------------------------------
To unsubscribe from this list, please visit:
http://xircles.codehaus.org/manage_email