Trong bài trước chúng ta đã cùng tìm hiểu về thư viện Hamcrest Matchers và cách sử dụng Matcher có sẵn của Hamcrest với JUnit. Trong bài này chúng ta sẽ cùng tìm hiểu cách tự viết một Matcher.
Để tạo một Custom Matcher chúng ta có thể sử dụng một trong các cách sau:
- Extend từ một abstract class FeatureMatcher.
- Extend từ một abstract class TypeSafeMatcher.
- Extend từ một abstract class TypeSafeDiagnosingMatcher.
- Extend từ một abstract class BaseMatcher.
Nội dung
Custom Matcher sử dụng FeatureMatcher
Chúng ta cần tạo một Custom Matcher khi cần wrap các Matcher đã tồn tại.
Một FeatureMatcher cần:
- Một constructor với các tham số sau:
- Matcher<?> subMatcher : là một matcher cần được wrap.
- String featureDescription : là một mô tả về feature test.
- String featureName : là một mô tả khi test bị failed.
- Override phương thức featureValueOf() : trả về giá trị sẽ được truyền vào phương thức được wrap matches()/ matchesSafely().
- Một static method để gọi khởi tạo matcher. Phương thức này nên được đánh dấu annotation @Factory để cho các tool/ IDE đây là một Hamcrest static factory method.
Ví dụ: chúng ta sẽ tạo Matcher để so sánh độ dài của mộ string.
package com.gpcoder.junit.hamcrest.custom; import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertThat; import org.hamcrest.Factory; import org.hamcrest.FeatureMatcher; import org.hamcrest.Matcher; import org.junit.Test; class GetLength extends FeatureMatcher<String, Integer> { private GetLength(Matcher<? super Integer> subMatcher, String featureDescription, String featureName) { super(subMatcher, featureDescription, featureName); } @Factory public static GetLength length(Matcher<? super Integer> subMatcher) { return new GetLength(subMatcher, "a string of length that", "length"); } @Override protected Integer featureValueOf(String actual) { return actual.length(); } } public class FeatureMatcherExample { @Test public void givenString_WhenLengthIs7_ThenCorrect() { assertThat("gpcoder", GetLength.length(is(7))); } @Test public void givenString_WhenLengthIs8_ThenIncorrect() { assertThat("gpcoder", GetLength.length(is(8))); } }
Kết quả test:
Custom Matcher sử dụng TypeSafeMatcher
Chúng ta có thể tạo một Custom Matcher bằng cách tạo một class extend từ một abstract class TypeSafeMatcher. Nó được gọi là safe bởi vì giá trị được truyền vào phương thức matchesSafely() đã được check null và cast đúng kiểu dữ liệu trước khi phương thức này được gọi.
Một TypeSafeMatcher cần implement 2 phương thức:
- describeTo() : được sử dụng để tạo mô tả về những gì Matcher đang kiểm tra. Mô tả này là những gì được Hamcrest sử dụng sau phần “Expected:” output của nó cho một Matcher không thành công.
- matchesSafely() : là phương thức được Hamcrest thực thi khi chúng ta muốn sử dụng Matcher của mình để kiểm tra giá trị.
Ví dụ: chúng ta sẽ tạo Matcher để kiểm tra một số có phải là một số dương hay không.
package com.gpcoder.junit.hamcrest.custom; import static org.junit.Assert.assertThat; import org.hamcrest.Description; import org.hamcrest.Factory; import org.hamcrest.Matcher; import org.hamcrest.TypeSafeMatcher; import org.junit.Test; class IsPositiveInteger extends TypeSafeMatcher<Integer> { @Factory public static Matcher<Integer> isAPositiveInteger() { return new IsPositiveInteger(); } @Override public void describeTo(Description description) { description.appendText("a positive integer"); } @Override protected boolean matchesSafely(Integer integer) { return integer > 0; } } public class TypeSafeMatcherExample { @Test public void givenInteger_WhenAPositiveValue_ThenCorrect() { int num = 1; assertThat(num, IsPositiveInteger.isAPositiveInteger()); } @Test public void givenInteger_WhenANegativeValue_ThenIncorrect() { int num = -1; assertThat(num, IsPositiveInteger.isAPositiveInteger()); } }
Kết quả test:
Custom Matcher sử dụng TypeSafeDiagnosingMatcher
Tương tự như TypeSafeMatcher, chúng ta cũng có thể tạo một custom Matcher bằng cách extend từ abstract class TypeSafeDiagnosingMatcher. Tuy nhiên, TypeSafeDiagnosingMatcher cho phép chúng ta có thể mô tả chi tiết hơn về lý do tại sao Matcher không khớp. Mô tả này là những gì được Hamcrest sử dụng sau phần “but:” output của nó cho một Matcher không thành công.
Ví dụ: chúng ta sẽ tạo Matcher để kiểm tra một số có phải là một chẵn hay không.
package com.gpcoder.junit.hamcrest.custom; import static org.junit.Assert.assertThat; import org.hamcrest.Description; import org.hamcrest.Factory; import org.hamcrest.TypeSafeDiagnosingMatcher; import org.junit.Test; class IsEven extends TypeSafeDiagnosingMatcher<Integer> { @Factory public static IsEven isEven() { return new IsEven(); } @Override protected boolean matchesSafely(Integer integer, Description description) { description.appendText("was ").appendValue(integer).appendText(", which is an Odd number"); return integer % 2 == 0; } @Override public void describeTo(Description description) { description.appendText("An Even number"); } } public class TypeSafeDiagnosingMatcherExample { @Test public void givenInteger_WhenEvenValue_ThenCorrect() throws Exception { assertThat(4, IsEven.isEven()); } @Test public void givenInteger_WhenOddValue_ThenIncorrect() throws Exception { assertThat(5, IsEven.isEven()); } }
Kết quả test:
Custom Matcher sử dụng BaseMatcher
Tất cả những Matcher trên đều extend từ một abstract class BaseMatcher<T>. Tất nhiên, chúng ta cũng có thể tạo một custom Matcher bằng cách extend từ BaseMatcher<T>.
Ví dụ: chúng ta sẽ tạo một MatchCombiner để thực thi một chain matcher với nhau.
package com.gpcoder.junit.hamcrest.custom; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.hasSize; import static org.junit.Assert.assertThat; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import org.hamcrest.BaseMatcher; import org.hamcrest.Description; import org.hamcrest.Matcher; import org.junit.Test; class MatcherCombinator<T> extends BaseMatcher<T> { private final List<Matcher<? super T>> matchers = new ArrayList<>(); private final List<Matcher<? super T>> failedMatchers = new ArrayList<>(); private MatcherCombinator(final Matcher<? super T> matcher) { matchers.add(matcher); } public MatcherCombinator<T> and(final Matcher<? super T> matcher) { matchers.add(matcher); return this; } @Override public boolean matches(final Object item) { boolean matchesAllMatchers = true; for (final Matcher<? super T> matcher : matchers) { if (!matcher.matches(item)) { failedMatchers.add(matcher); matchesAllMatchers = false; } } return matchesAllMatchers; } @Override public void describeTo(final Description description) { description.appendValueList("\n", " " + "and" + "\n", "", matchers); } @Override public void describeMismatch(final Object item, final Description description) { description.appendText("\n"); for (Iterator<Matcher<? super T>> iterator = failedMatchers.iterator(); iterator.hasNext();) { final Matcher<? super T> matcher = iterator.next(); description.appendText("Expected: <"); description.appendDescriptionOf(matcher).appendText(" but "); matcher.describeMismatch(item, description); if (iterator.hasNext()) { description.appendText(">\n"); } } } public static <LHS> MatcherCombinator<LHS> matches(final Matcher<? super LHS> matcher) { return new MatcherCombinator<LHS>(matcher); } } public class MatcherCombinatorExample { @Test public void givenList_WhenEmptyList_ThenInCorrect() { List<Integer> list = new ArrayList<>(); assertThat(list, MatcherCombinator.matches(hasSize(1)).and(contains(3))); } @Test public void givenList_WhenNotEmptyList_ThenInCorrect() { List<Integer> list = new ArrayList<>(); list.add(3); assertThat(list, MatcherCombinator.matches(hasSize(1)).and(contains(3))); } }
Kết quả test:
Tài liệu tham khảo:
- https://www.javacodegeeks.com/2015/11/custom-hamcrest-matchers.html
- https://www.vogella.com/tutorials/Hamcrest/article.html