【问题标题】:Hamcrest: How to instanceOf and cast for a matcher?Hamcrest:如何为匹配器实例化和强制转换?
【发布时间】:2017-07-20 20:01:12
【问题描述】:

问题

假设以下简单测试:

@Test
public void test() throws Exception {
    Object value = 1;
    assertThat(value, greaterThan(0));
}

测试无法编译,因为“greaterThan”只能应用于Comparable 类型的实例。但我想断言value 是一个大于零的整数。我如何使用 Hamcrest 来表达这一点?

到目前为止我尝试了什么:

简单的解决方案是通过像这样强制转换匹配器来删除泛型:

assertThat(value, (Matcher)greaterThan(0));

可能,但会生成编译器警告并感觉不对。

一个冗长的替代方案是:

@Test
public void testName() throws Exception {
    Object value = 1;

    assertThat(value, instanceOfAnd(Integer.class, greaterThan(0)));
}

private static<T> Matcher<Object> instanceOfAnd(final Class<T> clazz, final Matcher<? extends T> submatcher) {
    return new BaseMatcher<Object>() {
        @Override
        public boolean matches(final Object item) {
            return clazz.isInstance(item) && submatcher.matches(clazz.cast(item));
        }

        @Override
        public void describeTo(final Description description) {
            description
                .appendText("is instanceof ")
                .appendValue(clazz)
                .appendText(" and ")
                .appendDescriptionOf(submatcher);
        }

        @Override
        public void describeMismatch(final Object item, final Description description) {
            if (clazz.isInstance(item)) {
                submatcher.describeMismatch(item, description);
            } else {
                description
                    .appendText("instanceof ")
                    .appendValue(item == null ? null : item.getClass());
            }
        }
    };
}

感觉“整洁”和“正确”,但对于看似简单的事情来说,这确实是很多代码。我试图在 hamcrest 中找到类似的内置内容,但没有成功,但也许我错过了什么?

背景

在我的实际测试用例中,代码是这样的:

Map<String, Object> map = executeMethodUnderTest();
assertThat(map, hasEntry(equalTo("the number"), greaterThan(0)));

在我简化的问题中,我也可以写assertThat((Integer)value, greaterThan(0))。在我的实际情况中,我可以写assertThat((Integer)map.get("the number"), greaterThan(0)));,但如果出现问题,这当然会使错误消息恶化。

【问题讨论】:

    标签: java hamcrest


    【解决方案1】:

    这个答案将显示如何使用 Hamcrest 执行此操作,我不知道是否有比建议的更好的方法。

    但是,如果您有可能包含另一个测试库,AssertJ 完全支持这一点:

    import org.junit.Test;
    
    import static org.assertj.core.api.Assertions.assertThat;
    
    public class TestClass {
    
      @Test
      public void test() throws Exception {
        Object value = 1;
        assertThat(value).isInstanceOfSatisfying(Integer.class, integer -> assertThat(integer).isGreaterThan(0));
      }
    
    }
    

    无需任何强制转换,AssertJ 为您完成。

    此外,如果断言失败,它会打印一条漂亮的错误消息,value 太小:

    java.lang.AssertionError:
    Expecting:
     <0>
    to be greater than:
     <0> 
    

    或者如果value 的类型不正确:

    java.lang.AssertionError: 
    Expecting:
     <"not an integer">
    to be an instance of:
     <java.lang.Integer>
    but was instance of:
     <java.lang.String>
    

    isInstanceOfSatisfying(Class&lt;T&gt; type, Consumer&lt;T&gt; requirements) 的 Javadoc 可以在 here 找到,其中还包含一些稍微复杂的断言示例:

    // second constructor parameter is the light saber color
    Object yoda = new Jedi("Yoda", "Green");
    Object luke = new Jedi("Luke Skywalker", "Green");
    
    Consumer<Jedi> jediRequirements = jedi -> {
      assertThat(jedi.getLightSaberColor()).isEqualTo("Green");
      assertThat(jedi.getName()).doesNotContain("Dark");
    };
    
    // assertions succeed:
    assertThat(yoda).isInstanceOfSatisfying(Jedi.class, jediRequirements);
    assertThat(luke).isInstanceOfSatisfying(Jedi.class, jediRequirements);
    
    // assertions fail:
    Jedi vader = new Jedi("Vader", "Red");
    assertThat(vader).isInstanceOfSatisfying(Jedi.class, jediRequirements);
    // not a Jedi !
    assertThat("foo").isInstanceOfSatisfying(Jedi.class, jediRequirements);
    

    【讨论】:

      【解决方案2】:

      问题是你在这里丢失了类型信息:

       Object value = 1;
      

      如果你仔细想想,这是一条非常奇怪的线。这里value 是可能的最通用的东西,没有 可以合理地告诉它,除了可能检查它是否是null 或检查它的字符串表示是否不是。我有点不知所措,试图在现代 Java 中想象上述行的合法用例。

      明显的解决方法是assertThat((Comparable)value, greaterThan(0));

      一个更好的解决方法是转换为Integer,因为你是在比较一个整数常量;字符串也是可比较的,但仅限于它们之间。

      如果你不能假设你的value 甚至是Comparable,那么将它与任何东西进行比较都是没有意义的。如果您的测试在转换为 Comparable 时失败,这是一个有意义的报告,表明您从其他失败的东西动态转换为 Object

      【讨论】:

      • 最小的可编译示例显然不是最好的;-)。我用“背景”部分扩展了我的问题来解释用例。
      【解决方案3】:

      您的原始尝试的稍微修改版本怎么样:

      @Test
      public void testName() throws Exception {
          Map<String, Object> map = executeMethodUnderTest();
      
          assertThat(map, hasEntry(equalTo("the number"),
                  allOf(instanceOf(Integer.class), integerValue(greaterThan(0)))));
      }
      
      private static<T> Matcher<Object> integerValue(final Matcher<T> subMatcher) {
          return new BaseMatcher<Object>() {
              @Override
              public boolean matches(Object item) {
                  return subMatcher.matches(Integer.class.cast(item));
              }
      
              @Override
              public void describeTo(Description description) {
                  description.appendDescriptionOf(subMatcher);
              }
      
              @Override
              public void describeMismatch(Object item, Description description) {
                  subMatcher.describeMismatch(item, description);
              }
          };
      }
      

      现在自定义匹配器不再那么冗长了,你仍然可以实现你想要的。

      如果值太小:

      java.lang.AssertionError: 
      Expected: map containing ["the number"->(an instance of java.lang.Integer and a value greater than <0>)]
           but: map was [<the number=0>]
      

      如果值类型错误:

      java.lang.AssertionError: 
      Expected: map containing ["the number"->(an instance of java.lang.Integer and a value greater than <0>)]
           but: map was [<the number=something>]
      

      【讨论】:

        【解决方案4】:

        包含 Object 值的映射的问题是您必须假设要比较的特定类。

        hamcrest 缺少的是一种将匹配器从给定类型转换为另一种类型的方法,例如以下要点中的那个: https://gist.github.com/dmcg/8ddf275688fd450e977e

        public class TransformingMatcher<U, T> extends TypeSafeMatcher<U> {
            private final Matcher<T> base;
            private final Function<? super U, ? extends T> function;
        
            public TransformingMatcher(Matcher<T> base, Function<? super U, ? extends T> function) {
                this.base = base;
                this.function = function;
            }
        
            @Override
            public void describeTo(Description description) {
                description.appendText("transformed version of ");
                base.describeTo(description);
            }
        
            @Override
            protected boolean matchesSafely(U item) {
                return base.matches(function.apply(item));
            }
        }
        

        这样,你可以这样写你的断言:

        @Test
        public void testSomething() {
            Map<String, Object> map = new HashMap<>();
            map.put("greater", 5);
        
            assertThat(map, hasEntry(equalTo("greater"), allOf(instanceOf(Number.class),
                    new TransformingMatcher<>(greaterThan((Comparable)0), new Function<Object, Comparable>(){
                        @Override
                        public Comparable apply(Object input) {
                            return Integer.valueOf(input.toString());
                        }
                    }))));
        }
        

        但问题是,您需要指定给定的 Comparable 数字类(在本例中为整数)。

        如果出现断言错误,消息将是:

        java.lang.AssertionError
        Expected: map containing ["string"->(an instance of java.lang.Number and transformed version of a value greater than <0>)]
             but: map was [<string=NaN>]
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多