Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Writing and using Hamcrest Matchers

39,171 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

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

×