matchers依赖_定制Hamcrest Matchers

matchers依赖

本文是我们名为“ 用Mockito测试 ”的学院课程的一部分。

在本课程中,您将深入了解Mockito的魔力。 您将了解有关“模拟”,“间谍”和“部分模拟”的信息,以及它们相应的存根行为。 您还将看到使用测试双打和对象匹配器进行验证的过程。 最后,讨论了使用Mockito的测试驱动开发(TDD),以了解该库如何适合TDD的概念。 在这里查看

在本教程中,我们将使用Hamcrest API创建我们自己的自定义匹配器,以扩展Hamcrest提供的“开箱即用”功能。

1.为什么要使用自定义匹配器?

有时我们会遇到Hamcrest Matchers库的限制。 我们需要在标准类(例如字符串,整数或列表)上具有新的匹配器功能,或者需要创建与已创建的高度自定义类匹配的匹配器。 在本教程中,我们将使用Hamcrest提供的工具针对这两种情况创建匹配器。

2.匹配器的解剖

为了创建自定义匹配器,我们将扩展内置的Abstract类TypeSafeDiagnosingMatcher 。 如果您在IDE中将此类扩展为Integer类型,您将看到该类的两个抽象方法:

public class BlankMatcher extends TypeSafeDiagnosingMatcher<Integer> {
   @Override
   protected boolean matchesSafely(Integer integer, Description description) {
       return false;
   }

   @Override
   public void describeTo(Description description) {

   }
}

第一个方法matchesSafely()是Matcher的作用所在,这是Hamcrest在要使用Matcher测试值时执行的方法。 如果是这种情况,它还负责报告Matcher为何不匹配。 不匹配描述是Hamcrest在失败匹配器输出的“但是:”部分之后使用的描述,因此不匹配描述应相应地设置格式。

第二种方法describeTo()用于生成匹配器正在检查的内容的描述。 Hamcrest在失败的匹配器的输出的“ Expected:”部分之后使用此描述,因此该描述应相应设置格式。

在后台,此Matcher将检查输入值是否不为null且类型正确,因此在执行命中我们的matchesSafely方法时,我们保证具有正确类型的值以进行匹配。

Hamcrest为我们完成了繁重的工作,因此这两种方法都是实现自己的Hamcrest Matchers所需要的。 在接下来的示例中,我们还将添加一些语法糖,以简化创建和使用自定义匹配器的过程。

3.现有课程的自定义匹配器

在本节中,我们将创建许多可用于现有类型的自定义匹配器。

甚至()

让我们从创建匹配器开始,以确定输入数字是否为偶数。 和以前一样,我们将为整数扩展TypeSafeDiagnosingMatcher

public class IsEven extends TypeSafeDiagnosingMatcher<Integer> {
   @Override
   protected boolean matchesSafely(Integer integer, Description description) {
       return false;
   }

   @Override
   public void describeTo(Description description) {

   }
}

然后,我们将实现describeTo()方法,以提供对匹配器的期望描述:

@Override
public void describeTo(Description description) {
   description.appendText("An Even number");
}

我们可以使用description参数来创建Matcher的描述。 Description类提供了许多用于格式化输入的辅助方法,在这种情况下,我们使用appendText()方法简单地添加一些文本。

接下来,让我们使用传递到matchesSafely方法的description参数来创建错误消息。 请记住,只有在故障情况下才能看到此输出。 我们将在Description类中使用几种方法来格式化消息:

@Override
protected boolean matchesSafely(Integer integer, Description description) {
   description.appendText("was ").appendValue(integer).appendText(", which is an Odd number");
   return false;
}

接下来,我们将对数字进行实际检查以查看其是否为偶数:

@Override
protected boolean matchesSafely(Integer integer, Description description) {
   description.appendText("was ").appendValue(integer).appendText(", which is an Odd number");
   return integer % 2 == 0;
}

最后,我们将向类添加静态工厂方法以使其易于在测试中使用:

public static IsEven isEven() {
   return new IsEven();
}

总而言之,我们有以下Matcher类:

public class IsEven extends TypeSafeDiagnosingMatcher<Integer> {
   @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 static IsEven isEven() {
       return new IsEven();
   }
}

现在,我们可以使用新的匹配器编写一些测试。

我们创建一个名为IsEvenTest的新Test类,然后导入新的工厂方法:

import static com.javacodegeeks.hughwphamill.mockito.hamcrest.matchers.IsEven.isEven;

public class IsEvenTest {

}

接下来,我们将编写一种测试方法来测试匹配器评估为true的肯定情况。

@Test
public void should_pass_for_even_number() throws Exception {
   // Given
   Integer test = 4;

   // Then
   assertThat(test, isEven());
}

还有一种显示匹配器无法匹配的输出的方法。

@Test
public void should_fail_for_odd_number() throws Exception {
   // Given
   Integer test = 5;

   // Then
   assertThat(test, isEven());
}

这将生成以下输出:

java.lang.AssertionError: 
Expected: An Even number
     but: was <5>, which is an Odd number

通常,在为匹配器编写真实测试时,我们希望通过测试以确保逻辑是正确的,毕竟我们不希望在测试失败的项目上工作! 如果我们想这样做,我们可以将断言更改为以下内容:

assertThat(test, not(isEven()));

但是,在开发过程中编写失败的测试以手动检查匹配器的输出可能会很有用。

divisibleBy(整数除数)

现在我们已经看到了如何创建一个自定义Matcher来测试Integer的固有属性。 是奇数还是偶数? 但是,我们在hamcrest Matcher库中看到许多Matchers会根据输入值进行测试。 现在,我们将自己创建一个Matcher。

想象一下,我们想定期测试数字以发现它们是否可以被另一个数字整除。 我们可以编写一个自定义Matcher为我们完成此任务。

首先,像上一个示例一样,创建一个无参匹配器,并将除数硬编码为3。

public class DivisibleBy extends TypeSafeDiagnosingMatcher<Integer> {

   @Override
   protected boolean matchesSafely(Integer integer, Description description) {
       int remainder = integer % 3; // Hardcoded to 3 for now!
      
       description.appendText("was ").appendValue(integer)
               .appendText(" which left a remainder of ").appendValue(remainder);
       return remainder == 0;
   }

   @Override
   public void describeTo(Description description) {
       description.appendText("A number divisible by 3"); // Hardcoded to 3 for now!
   }
  
   public static DivisibleBy divisibleBy() {
       return new DivisibleBy();
   }
}

我们的Matcher看起来很像我们的IsEven() Matcher,但是不匹配的描述稍微复杂一些。

我们如何从硬编码值更改为输入值? 没什么花招,实际上就像添加一个私有成员变量并将其设置在构造函数中,然后将值通过factory方法传递一样容易。

现在让我们看一下完整的Matcher:

public class DivisibleBy extends TypeSafeDiagnosingMatcher<Integer> {
  
   private final Integer divisor;

   public DivisibleBy(Integer divisor) {
       this.divisor = divisor;
   }

   @Override
   protected boolean matchesSafely(Integer integer, Description description) {
       int remainder = integer % 3; // Hardcoded to 3 for now!

       description.appendText("was ").appendValue(integer)
               .appendText(" which left a remainder of ").appendValue(remainder);
       return remainder == 0;
   }

   @Override
   public void describeTo(Description description) {
       description.appendText("A number divisible by 3"); // Hardcoded to 3 for now!
   }

   public static DivisibleBy divisibleBy(Integer divisor) {
       return new DivisibleBy(divisor);
   }
}

再次,让我们创建一些测试来练习我们的新Matcher:

public class DivisibleByTest {

   @Test
   public void should_pass_for_true_divisor() throws Exception {
       // Given
       Integer test = 15;

       // Then
       assertThat(test, is(divisibleBy(5)));
   }

   @Test
   public void should_fail_for_non_divisor() throws Exception {
       // Given
       Integer test = 17;

       // Then
       assertThat(test, is(divisibleBy(3)));
   }
}

测试失败的输出是

java.lang.AssertionError: 
Expected: is A number divisible by 3
     but: was <17> which left a remainder of <2>

现在我们已经看到了如何为现有类创建自定义匹配器,这些类可以测试内部属性或接受测试值。

接下来,我们将为自己编写的类创建自定义匹配器。

4.针对您自己的班级的自定义匹配器

当我们使用自己创建的类进行工作时,自定义Hamcrest匹配器是测试库中的强大工具。 现在,我们将创建一个域模型,并编写一些自定义匹配器以使用该模型。

我们的模型:一棵树

编程中常见的数据结构是由许多节点组成的树。 这些节点可能只有一个父节点或一个或多个子节点。 没有父节点的节点称为根。 没有子节点的节点称为叶子。 如果可以通过X的父级从X到Y跟踪路径,则将节点X视为另一个节点Y的后代。如果Y是X的后代,则将节点X视为另一个节点Y的祖先。如果X和Y都共享父节点,则视为另一个节点Y的兄弟。

我们将使用Node存储单个int值。

我们的模型将有一个名为Node的类,如下所示:

/**
* Node class for building trees
*

* Uses instance equality.
*/
public class Node {

   private final int value;

   private Node parent;
   private final Set<Node> children;

   /**
    * Create a new Node with the input value
    */
   public Node(int value) {
       this.value = value;
       children = new HashSet<>();
   }

   /**
    * @return The value of this Node
    */
   public int value() {
       return value;
   }

   /**
    * @return The parent of this Node
    */
   public Node parent() {
       return parent;
   }

   /**
    * @return A copy of the Set of children of this Node
    */
   public Set<Node> children() {
       return new HashSet<>(children);
   }

   /**
    * Add a child to this Node
    *
    * @return this Node
    */
   public Node add(Node child) {
       if (child != null) {
           children.add(child);
           child.parent = this;
       }
       return this;
   }

   /**
    * Remove a child from this Node
    *
    * @return this Node
    */
   public Node remove(Node child) {
       if (child != null && children.contains(child)) {
           children.remove(child);
           child.parent = null;
       }
       return this;
   }

   public String toString() {
       StringBuilder builder = new StringBuilder();
       builder.append("Node{")
               .append("value=").append(value).append(",")
               .append("parent=").append(parent != null ?parent.value : "null").append(",")
               .append("children=").append("[")
               .append(children.stream().map(n -> Integer.toString(n.value)).collect(Collectors.joining(",")))
               .append("]}");

       return builder.toString();
   }
}

请注意,对于这个简单的示例,我们的类不是线程安全的。

该模型使我们能够构建一个节点树,从根节点开始并添加子节点。 我们可以在下面的小型应用程序类中看到这一点:

public class App {

   public static void main(String... args) {
       Node root = createTree();

       printNode(root);
   }

   private static Node createTree() {
       /*
                        1
                    /       \
                  2           3
                /   \         /   \
              4       5   6       7
            /   \       |
          8      9   10
       */
       Node root = new Node(1);
       root.add(
               new Node(2).add(
                       new Node(4).add(
                               new Node(8)
                       ).add(
                               new Node(9)
                       )

               ).add(
                       new Node(5).add(
                               new Node(10)
                       )
               )
       ).add(
               new Node(3).add(
                       new Node(6)
               ).add(
                       new Node(7)
               )
       );
       return root;
   }

   private static void printNode(Node node) {
       System.out.println(node);
       for (Node child : node.children()) {
           printNode(child);
       }

   }
}

这将产生以下输出:

Node{value=1,parent=null,children=[3,2]}
Node{value=3,parent=1,children=[7,6]}
Node{value=7,parent=3,children=[]}
Node{value=6,parent=3,children=[]}
Node{value=2,parent=1,children=[5,4]}
Node{value=5,parent=2,children=[10]}
Node{value=10,parent=5,children=[]}
Node{value=4,parent=2,children=[8,9]}
Node{value=8,parent=4,children=[]}
Node{value=9,parent=4,children=[]}

现在我们已经定义了模型并知道如何使用它,我们可以开始针对它创建一些Matchers。

我们将在类中使用Node测试治具,以便针对一致的模型进行测试,该模型将与示例应用程序中定义的树结构相同。 夹具在这里定义:

public class NodeTestFixture {

   static Node one = new Node(1);
   static Node two = new Node(2);
   static Node three = new Node(3);
   static Node four = new Node(4);
   static Node five = new Node(5);
   static Node six = new Node(6);
   static Node seven = new Node(7);
   static Node eight = new Node(8);
   static Node nine = new Node(9);
   static Node ten = new Node(10);

   static {
       one.add(two);
       one.add(three);

       two.add(four);
       two.add(five);

       three.add(six);
       three.add(seven);

       four.add(eight);
       four.add(nine);

       five.add(ten);
   }
}

叶()

我们将创建的第一个Matcher将检查输入节点是否为叶节点。 它将通过检查输入节点是否有任何子节点来完成此操作,如果有则子节点不是叶节点。

public class IsLeaf extends TypeSafeDiagnosingMatcher<Node> {
   @Override
   protected boolean matchesSafely(Node node, Description mismatchDescription) {
       if (!node.children().isEmpty()) {
           mismatchDescription.appendText("a node with ")
                   .appendValue(node.children().size())
                   .appendText(" children");
           return false;
       }
       return true;
   }

   @Override
   public void describeTo(Description description) {
       description.appendText("a leaf node with no children");
   }

   public static IsLeaf leaf() {
       return new IsLeaf();
   }
}

测试类别:

public class IsLeafTest extends NodeTestFixture {

   @Test
   public void should_pass_for_leaf_node() throws Exception {
       // Given
       Node node = NodeTestFixture.seven;

       // Then
       assertThat(node, is(leaf()));
   }

   @Test
   public void should_fail_for_non_leaf_node() throws Exception {
       // Given
       Node node = NodeTestFixture.four;

       // Then
       assertThat(node, is(not(leaf())));
   }
}

根()

现在,我们将创建一个匹配器,以检查节点是否为根节点。 我们将通过检查父节点的存在来做到这一点。

public class IsRoot extends TypeSafeDiagnosingMatcher<Node> {
   @Override
   protected boolean matchesSafely(Node node, Description mismatchDescription) {
       if (node.parent() != null) {
           mismatchDescription.appendText("a node with parent ")
                   .appendValue(node.parent());
           return false;
       }
       return true;
   }

   @Override
   public void describeTo(Description description) {
       description.appendText("a root node with no parent");
   }

   public static IsRoot root() {
       return new IsRoot();
   }
}

测试类别:

public class IsRootTest {

   @Test
   public void should_pass_for_root_node() throws Exception {
       // Given
       Node node = NodeTestFixture.one;

       // Then
       assertThat(node, is(root()));
   }

   @Test
   public void should_fail_for_non_root_node() throws Exception {
       // Given
       Node node = NodeTestFixture.five;

       // Then
       assertThat(node, is(not(root())));
   }
}

DescendantOf(节点节点)

接下来是带有输入的匹配器,我们将检查给定Node是否为输入Node的后代。 我们将向上移动父母,看看是否在根之前到达了测试节点。

public class IsDescendant extends TypeSafeDiagnosingMatcher<Node> {

   private final Node ancestor;

   public IsDescendant(Node ancestor) {
       this.ancestor = ancestor;
   }

   @Override
   protected boolean matchesSafely(Node node, Description description) {
       while (node.parent() != null) {
           if (node.parent().equals(ancestor)) {
               return true;
           }
           node = node.parent();
       }
       description.appendText("a Node which was not a descendant of ")
               .appendValue(ancestor);
       return false;
   }

   @Override
   public void describeTo(Description description) {
       description.appendText("a descendant Node of ").appendValue(ancestor);
   }

   public static IsDescendant descendantOf(Node ancestor) {
       return new IsDescendant(ancestor);
   }
}

测试类别:

public class IsDescendantTest {

   @Test
   public void should_pass_for_descendant_node() throws Exception {
       // Given
       Node node = NodeTestFixture.nine;
       Node ancestor = NodeTestFixture.two;

       // Then
       assertThat(node, is(descendantOf(ancestor)));
   }

   @Test
   public void should_fail_for_non_descendant_node() throws Exception {
       // Given
       Node node = NodeTestFixture.ten;
       Node ancestor = NodeTestFixture.three;

       // Then
       assertThat(node, is(not(descendantOf(ancestor))));
   }
}

ancestorOf(节点节点)

下一步将检查给定的节点是否是输入节点的祖先。 此操作实际上是descendantOf()的相反操作,因此我们将上移输入节点的父级而不是测试节点。

public class IsAncestor extends TypeSafeDiagnosingMatcher<Node> {

   private final Node descendant;

   public IsAncestor(Node descendant) {
       this.descendant = descendant;
   }

   @Override
   protected boolean matchesSafely(Node node, Description description) {
       Node descendantCopy = descendant;
       while (descendantCopy.parent() != null) {
           if (descendantCopy.parent().equals(node)) {
               return true;
           }
           descendantCopy = descendantCopy.parent();
       }
       description.appendText("a Node which was not an ancestor of ")
               .appendValue(descendant);
       return false;
   }

   @Override
   public void describeTo(Description description) {
       description.appendText("an ancestor Node of ").appendValue(descendant);
   }

   public static IsAncestor ancestorOf(Node descendant) {
       return new IsAncestor(descendant);
   }
}

测试类别:

public class IsAncestorTest {
   @Test
   public void should_pass_for_ancestor_node() throws Exception {
       // Given
       Node node = NodeTestFixture.two;
       Node descendant = NodeTestFixture.ten;

       // Then
       assertThat(node, is(ancestorOf(descendant)));
   }

   @Test
   public void should_fail_for_non_ancestor_node() throws Exception {
       // Given
       Node node = NodeTestFixture.three;
       Node descendant = NodeTestFixture.eight;

       // Then
       assertThat(node, is(not(ancestorOf(descendant))));
   }
}

siblingOf(节点节点)

最后,我们将创建一个Matcher来检查输入节点是否是另一个节点的同级节点。 我们将检查他们是否共享父母。 此外,当用户尝试测试根节点的同级节点时,我们将进行一些检查并提供一些输出。

public class IsSibling extends TypeSafeDiagnosingMatcher<Node> {

   private final Node sibling;

   public IsSibling(Node sibling) {
       this.sibling = sibling;
   }

   @Override
   protected boolean matchesSafely(Node node, Description description) {
       if (sibling.parent() == null) {
           description.appendText("input root node cannot be tested for siblings");
           return false;
       }

       if (node.parent() != null && node.parent().equals(sibling.parent())) {
           return true;
       }

       if (node.parent() == null) {
           description.appendText("a root node with no siblings");
       }
       else {
           description.appendText("a node with parent ").appendValue(node.parent());
       }

       return false;
   }

   @Override
   public void describeTo(Description description) {
       if (sibling.parent() == null) {
           description.appendText("a sibling of a root node");
       } else {
           description.appendText("a node with parent ").appendValue(sibling.parent());
       }
   }

   public static IsSibling siblingOf(Node sibling) {
       return new IsSibling(sibling);
   }
}

测试类别:

public class IsSiblingTest {

   @Test
   public void should_pass_for_sibling_node() throws Exception {
       // Given
       Node a = NodeTestFixture.four;
       Node b = NodeTestFixture.five;

       // Then
       assertThat(a, is(siblingOf(b)));
   }

   @Test
   public void should_fail_for_testing_root_node() throws Exception {
       // Given
       Node a = NodeTestFixture.one;
       Node b = NodeTestFixture.six;

       // Then
       assertThat(a, is(not(siblingOf(b))));
   }

   @Test
   public void should_fail_for_input_root_node() throws Exception {
       // Given
       Node a = NodeTestFixture.five;
       Node b = NodeTestFixture.one;

       // Then
       assertThat(a, is(not(siblingOf(b))));
   }

   @Test
   public void should_fail_for_non_sibling_node() throws Exception {
       // Given
       Node a = NodeTestFixture.five;
       Node b = NodeTestFixture.six;

       // Then
       assertThat(a, is(not(siblingOf(b))));
   }
}

5.结论

现在我们已经看到了如何创建Custom Hamcrest Matchers来测试既有的标准Java类和我们自己的类。 在下一个教程中,我们将把到目前为止所学到的所有知识放在一起,因为我们了解了将模拟和测试放在首位的软件开发技术。 测试驱动开发。

翻译自: https://www.javacodegeeks.com/2015/11/custom-hamcrest-matchers.html

matchers依赖

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值