Writing and using Hamcrest Matchers

21,496 views
21,130 views

Published on

Hamcrest is a library for creating matchers for usage in unit tests, mocks and UI validation. This talk gives a brief introduction to using and writing Hamcrest matchers.

The topics covered:
* Basic introduction to Hamcrest
* Using Matchers in assertions
* Using Matchers with Mockito
* Writing custom matchers
* Ad-hoc matchers

Published in: Technology, Business
2 Comments
41 Likes
Statistics
Notes
No Downloads
Views
Total views
21,496
On SlideShare
0
From Embeds
0
Number of Embeds
686
Actions
Shares
0
Downloads
0
Comments
2
Likes
41
Embeds 0
No embeds

No notes for slide
  • This is a simplified version of the actual matcher from Hamcrest Core
  • For those of you who’re not familiar with Mockito, here’s how you use it
  • This has the disadvantage of being type-unsafe, but we’ll get back to this point later on.
  • Like the previous example, this is type-unsafe because we’re not sure that the Cat class even has a field named “kittens”, let alone its type
  • Matcher is the common ancestor for all Hamcrest matchers. Note that I removed a warning urging you not to implement Matcher<T>, extending BaseMatcher<T> instead.In most cases, you’ll want to extend TypeSafeMatcher<T>
  • This matcher works on the WixUrl class, which is a type-safe URL builder class from the Wix infrastructure.It’s a good example for the simple, yet somewhat confusing, structure of a matcher. Note that this matcher has one member, which is another string matcher for specifying the expected value. We could’ve used a string here, but using a Matcher<String> allows us to be more flexible when using this matcher.This pattern of replaying a matcher against the actual value is a core principle of Matcher programming.Note that our matcher extends TypeSafeMatcher, which implements the Matcher.matches() method and delegates to the matchesSafely() method after performing a type assertion followed by a cast.
  • Why do we need a Url matcher? Sometimes we don’t care about the query string, the host, the port, the protocol, etc. We could’ve used a string matcher such as contains() or endsWith() but this is more type safe, readable and illustrates the tested behavior more clearly.Note how the assertion error is much more readable than the one produced by assertEquals()
  • This one takes two matchers, one for the parameter name and one for the parameter value.Note another principle introduced here, that of creating a new matcher wrapping our matchers and replaying it against the actual value. Thus, we abstract away from the test the fact the WixUrl.params is a map. In fact, we lately changed it from a Map<String, String> to a Multimap<String, String> without breaking any test – I just changed the field type and the appropriate matcher class.
  • Note the use of the allOf() and anyOf() aggregate matchers that represent boolean AND and OR operations, respectively.
  • Based on a true storyThis class accepts an instance of HttpClient (Apache HttpComponents 4.0). I wanted to give it a mock of HttpClient, then stub and verify it.
  • I needed to be able to write matchers for the HttpGet class, for two reasons:1) It was much more readable than constructing an instance of it2) HttpGet takes on parameter, URI, and contains logic that deals with the HTTP protocol itself – not something that’s relevant for the test
  • Note, again, how we replay the expected value matcher against the actual value. This prevents the need to construct our own instance on HttpGet and populate it with the expected value. Also note that this is a factory method, creating an anonymous class. The method starts with a capital letter and is named according to the name of the class we’re matching against. This is a convention we developed here at Wix, but we find it useful and clear.
  • The use case here is that the proxy should send all traffic from www.wix.com/ to theproxied server, appending any nested path.Note that response can be either a real object constructed beforehand or a mock, but the mock must also be constructed before calling the when() method because of some Mockito limitation.Also note the reuse of our matcher both for stubbing the mock and verifying the desired behavior.
  • The matcher is for a Header array because HttpGet.getAllHeaders() returns an array
  • Note that without using Hamcrest, there’s no straightforward way to test for the inexistence of a value
  • Writing and using Hamcrest Matchers

    1. 1. Hamcrest MatchersassertThat(audience, is(payingAttention())); Writing and using them for assertions, mocking and behavioral verification
    2. 2. What is Hamcrest?According to the project homepage, [Hamcrest] provides a library of matcher objects (also known as constraints or predicates) allowing match rules to be defined declaratively, to be used in other frameworks. Typical scenarios include testing frameworks, mocking libraries and UI validation rules. Hamcrest is not a testing library: it just happens that matchers are very useful for testing.
    3. 3. Typical usage example Actual valueassertThat( someString, is(equalTo(someOtherString))); Expectation on the value, represented as a Matcher
    4. 4. Wait – but what’s wrong with assertEquals?assertEquals(someString, someOtherString) This looks just as good, doesn’t it?assertEquals( cat.getName(), otherCat.getName()) Not so bad, either
    5. 5. However, What about collections?assertEquals(someKitten, cat.getKittens().iterator().next()) This works if our kitten is the first element in the collection, but what about asserting that the kitten exists anywhere in the collection?
    6. 6. Well, we can do this:boolean found = false;for (Kitten kitten : cat.getKittens()) { if (kitten.equals(someKitten)) found = true;}assertTrue(found);
    7. 7. But don’t you prefer this? Iterable<Kitten>assertThat( cat.getKittens(), hasItem(someKitten)) Matcher on Iterable<Kitten> that accepts a Matcher<Kitten>
    8. 8. OK, so how does this work?
    9. 9. Basic MatchersA Matcher is initialized with the expectedvalues, which are compared against theactual object we’re matching against wheninvoking the matcher.
    10. 10. IsEqual Matcherclass IsEqual<T> extends BaseMatcher<T> { private final Object object; // c’tor omitted for readability public boolean matches(Object arg) { return object.equals(arg); }}
    11. 11. StringEndsWith Matcherclass StringEndsWith <T> extends BaseMatcher<T> { private final String suffix; // c’tor omitted for readability public boolean matches(String s) { return s.endsWith(suffix); }}
    12. 12. Using while stubbing mocks
    13. 13. Mockito in one slideCatShop catShop = mock(CatShop.class);when(catShop.purchase(somePurchaseRequest).thenReturn(someCat) Creating the mock... do some work Stubbing the mockverify(catShop) Behavior verification .purchase(expectedPurchaseRequest)
    14. 14. Without HamcrestCatPurchase catPurchase = new CatPurchase();catPurchase.setBreed(“Brittish”);when(catShop.purchase(catPurchase))).thenReturn(somePreviouslyStubbedCat)However, this will force us to set all other fields of theCatPurchase class, since Mockito will perform an exact matchcomparison between our instance and the actual one
    15. 15. Of course, you could do this:when( catShop.purchase( any(CatPurchaseDTO.class))).thenReturn(somePreviouslyStubbedCat)This works, but lacks the benefit of asserting that ouroperation is only valid for the expected input
    16. 16. The solution: use argThat() Mockito helper that creates an argument matcher fromwhen( a Hamcrest matcher catShop.purchase(argThat( hasPropertyWithValue( “breed”, startsWith(“Brittish”))))).thenReturn(somePreviouslyStubbedCat) Hamcrest matcher that accepts a Java Bean property name and a nested value matcher
    17. 17. Using for behavioral verificationCatDao catDao = mock(CatDao.class);CatStore catStore = new CatStore (catDao);with a Catcall to Verify that there was a CatDao.update() instance,catStore.saveOrUpdate(existingCat); the ‘name’ property is for which “felix” and the ‘kittens’ property is anverify(catDao).update(argThat( Iterable containing two kittens, kitten1 and kitten2 allOf( hasPropertyWithValue(“name”, “felix”), hasPropertyWithValue(“kittens”, hasItems(kitten1, kitten2)))));
    18. 18. Writing custom matchers
    19. 19. Writing your own matchersIn the previous examples, we used thehasPropertyWithValue() matcher, which, whileallowing for fluent assertions or stubbing, hasthe disadvantage of not being type-safe.This is where writing custom matchers becomesuseful (or, as some would say, necessary).
    20. 20. The Matcher<T> hierarchyabstract class TypeSafeMatcher<T> extends BaseMatcher<T> { boolean matchesSafely(T item);}interface Matcher<T> extends SelfDescribing { boolean matches(Object item);}interface SelfDescribing { void describeTo(Description description);}
    21. 21. Dissecting some Wix matchersclass HostMatcher extends TypeSafeMatcher<WixUrl> { private final Matcher<String> host; // c’tor omitted for readability public boolean matchesSafely(WixUrl url) { return host.matches(url.host); Nested matcher that } will be replayed on the Actual value being actual value matched against public void describeTo(Description description) { description.appendText("Host that matches ").appendValue(host); we write a Here } readable description of our expected value public static HostMatcher hasHost(Matcher<String> host)A utility factory method for { return new HostMatcher(host); fluently creating this } matcher. Not mandatory} but very convenient.
    22. 22. Using our matcherWixUrl url =new WixUrl(“http://www.wix.com/some/path”);assertThat(url, hasHost(is(“www.wix.com”))); ✔assertThat(url, hasHost(endsWith(“wix.com”))); ✔assertThat(url, hasHost(is(“google.com”))); ✗ java.lang.AssertionError: Expected: Host that matches <is ”google.com"> got: <http://www.wix.com/some/path>
    23. 23. Another URL matcherclass WixUrlParamMatcher extends TypeSafeMatcher<WixUrl> { private final Matcher<String> name; // c’tor omitted for readability url.params is a Map<String, String>, so private final Matcher<String> value; we create a matcher for a map entry around our name and value matchers public boolean matchesSafely(WixUrl url) { replay it against the actual value and return hasEntry(name, value).matches(url.params); } public void describeTo(Description description) { description .appendText("Param with name ").appendValue(name) .appendText(" and value ").appendValue(value); }}
    24. 24. Using the two matchers togetherString s = “www.wix.com?p=v1&p=v2&p3=v3”;WixUrl url = new WixUrl(s);assertThat(url, allOf( hasHost(is(“www.wix.com”)), hasParam(is(“p”), anyOf(is(“v1”), is(“v2”))), hasParam(is(“p3”), startsWith(“v”))));
    25. 25. But wait – my URL is a String!Sometimes you’ll have matchers that accept a specifictype, such as WixUrl or XML Document. For thispurpose, use a wrapping matcher that performs theconversion for you:class StringAsUrlMatcher extends TypeSafeMatcher<String> { private final Matcher<WixUrl> urlMatcher; public boolean matchesSafely(String urlString) { return matcher.matches(new WixUrl(urlString)); } public void describeTo(Description description) { description.appendText("Url that matches ") .appendDescriptionOf(urlMatcher); }}
    26. 26. Ad-Hoc matchers for readable tests
    27. 27. Consider the following classclass Proxy { private final HttpClient httpClient; private String targetUrl; public String handle (String path) { httpClient.execute(// some HttpGet); }}
    28. 28. The HttpClient interfacepublic HttpResponse execute(HttpGet get);Our class under test is expected to replace the domainin path with targetUrl, thus serving as an HTTP Proxy.We would like to stub and verify the HttpGet parameterto make sure it builds the proxy URL properly.
    29. 29. My test looks something like thisHttpClient client = mock(HttpClient.class);String url = “http://www.example.com/”;{…} handler = new Proxy(client, url);when(client.execute({www.a.com/path})) .thenReturn(someResponse);handler.handle(“www.a.com/path”);verify(client).execute({www.example.com/path});
    30. 30. The solutionMatcher<HttpGet> HttpGet(final Matcher<String> urlMatcher) { return new TypeSafeMatcher<HttpGet>() { public boolean matchesSafely(HttpGet httpGet) { return urlMatcher.matches(httpGet.getURI().toString())); } public void describeTo(Description description) { description.appendText("HttpGet with url ") .appendDescriptionOf(urlMatcher); } };}
    31. 31. Usage of the HttpGet matcherwhen(handler.execute(argThat( is(HttpGet(startsWith(“http://www.a.com”)))))) .thenReturn(response);handler.handle(“http://www.a.com/some/path”);verify(client).execute(argThat(is(HttpGet( is(“http://www.example.com/some/path”)))));
    32. 32. The plot thickensMoments after triumphantly running the test Irealized that in addition to verifying that therequest went to the appropriate URL, I had toverify that some – but not all – HTTP headerswere copied to the proxy request and some newones were added to it.
    33. 33. Ad-hoc matchers to the rescue1) Add the following parameter to the HttpGet method: final Matcher<Header[]> headersMatcher2) Change the matchesSafetly() method: public boolean matchesSafely(HttpGet httpGet) { return urlMatcher.matches(httpGet.getURI().toString()) && headersMatcher.matches(httpGet.getAllHeaders()); }
    34. 34. Ad-hoc matchers to the rescue3) Write a matcher for the Header class:Matcher<Header> Header( final Matcher<String> name, final Matcher<String> value) { return new TypeSafeMatcher<Header>() { public boolean matchesSafely(Header header) { return name.matches(header.getName()) && value.matches(header.getValue()); } }}
    35. 35. Putting it all togetherverify(client).execute(argThat(is(HttpGet( that the X-Wix-Base-Uri header Asserts contains the expected value (using the is({URL matcher omitted for readability}), WixUrl matchers we’ve seen before). allOf( hasItemInArray( Header( is("X-Wix-Base-Uri"), Asserts that there’s no header by isUrlThat( the name of X-Seen-By, no matter what value it has hasHost(“www.wix.com”), hasPath(myPath)))), not(hasItemInArray( Header(is("X-Seen-By"), any(String.class)))))))));
    36. 36. Questions? shaiy@wix.comhttp://il.linkedin.com/in/electricmonk http://twitter.com/shaiyallin

    ×