使用AssertJ让单元测试和TDD更加简单

文章目录

使用AssertJ让单元测试和TDD更加简单

前言

在编写单元测试时,一个重要的工作就是编写断言(Assertion),而JUnit自带的断言机制和Hamcrest的assertThat都不那么好用。

利用AssertJ,可以让单元测试更加简单,让TDD过程更加顺畅。

AssertJ的优点:

  • 通用的assertThat “流式断言”,让编写断言更加简单快捷。
  • API丰富,对各种类型的断言都非常友好。

环境

本文的测试环境:

  • JUnit4
  • assertj-core
  • Maven
  • Java8

添加assertJ的Maven依赖:

<dependency>
  <groupId>org.assertj</groupId>
  <artifactId>assertj-core</artifactId>
  <version>3.17.0</version>
  <scope>test</scope>
</dependency>

在测试类中引入AssertJ:

import static org.assertj.core.api.Assertions.*;

断言类型

常用的断言类型

常用的断言类型包括:

  • isSameAs: 同一个对象。
  • isEuqalsTo: 值相等。
  • isCloseTo: 值相近。
  • isTrue:为真,isFalse 为假。
  • containsExactly:包含全部元素,顺序也保持一致。
  • containsOnly:包含全部元素,顺序不需要保持一致。
  • contains:包含给定的元素。
  • hasSize:长度或大小或元素个数为N个。
  • isEmpty: 是否为空。
  • isNull:是否为null。
  • isInstanceOf:是否为指定类型。

断言的辅助方法

断言的辅助方法:

  • extracting: 提取,根据类名或方法引用或字段名反射调用,或根据lambda表达式调用。

  • mathes: 匹配,根据lambda表达式调用。

  • atIndex: 获取指定位置/索引的元素。

  • offset: 偏移量。

  • withPercentage:百分比。

  • tuple:将一组属性的值包装成元组。

  • entry:将键值对({K,V})包装成Map.Entry。

AssertJ例子

官网:

基本类型

参见:

说明:

  • 对可以精确匹配的值(比如字符串、整数),用isEqualTo()来比较,对不能精确匹配的值(比如浮点数),用isCloseTo()比较。

  • 对布尔值,用isTrue()isFalse()来比较。

    @Test
    public void test_value_equals() {
    String hello = “hello”.toUpperCase();
    assertThat(hello).isEqualTo(“HELLO”);

    int secondsOfDay = 24 * 60 * 60;
    assertThat(secondsOfDay).isEqualTo(86400);
    }

    @Test
    public void test_value_close() {
    double result = 0.1 + 0.1 + 0.1; // 0.30000000000000004
    assertThat(result).isCloseTo(0.3, offset(0.0001)); // 误差值
    assertThat(result).isCloseTo(0.3, withPercentage(0.01)); // 误差百分比
    }

    @Test
    public void test_boolean() {
    boolean flag = “Kubernetes”.length() > 8;
    assertThat(flag).isTrue();

    boolean flag2 = “Docker”.length() > 8;
    assertThat(flag2).isFalse();
    }

单个对象

参见:

说明:

  • 判断是否为同一个对象用isSameAs()

  • 如果重写了euqals和hashcode方法,也可以用isEqualTo来判断对象是否相同。

  • 如果只是判断对象的值是否相等,则可以用extracting提取后再判断,或用matches来用lambda表达式判断。

  • 判断是否为null用isNull()isNotNull()

    @Test
    public void test_object_null_or_not_null() {
    Person p1 = new Person(“William”, 34);
    assertThat(p1).isNotNull();

    Person p2 = null;
    assertThat(p2).isNull();
    

    }

    @Test
    public void test_object_same_as_other() {
    Person p1 = new Person(“William”, 34);
    Person p2 = p1;
    assertThat(p1).isSameAs(p2);

    Person p3 = new Person("John", 35);
    assertThat(p1).isNotSameAs(p3);
    

    }

    @Test
    public void test_object_equals() {
    Person p1 = new Person(“William”, 34);
    Person p2 = new Person(“William”, 34);

    assertThat(p1).isNotSameAs(p2);
    assertThat(p1).isNotEqualTo(p2); // 如果用isEqualTo判断,则必须要重写equals方法
    
    // extracting method reference
    assertThat(p1).extracting(Person::getName, Person::getAge).containsExactly("William", 34);
    assertThat(p1).extracting(Person::getName, Person::getAge).containsExactly(p2.getName(), p2.getAge());
    
    // extracting field
    assertThat(p1).extracting("name", "age").containsExactly("William", 34);
    assertThat(p1).extracting("name", "age").containsExactly(p2.getName(), p2.getAge());
    
    // matches
    assertThat(p1).matches(x -> x.getName().equals("William") && x.getAge() == 34);
    assertThat(p1).matches(x -> x.getName().equals(p2.getName()) && x.getAge() == p2.getAge());
    

    }

数组

参见:

说明:

  • isNull来判断数组是否为null。

  • isEmpty来判断数组是否为空(不包含任何元素)。

  • hasSize来判断数组的元素个数。

  • contains 判断数组中包含指定元素;用containsOnly判断数组中包含全部元素,但是顺序可以不一致;用cotainsExactly判断数组中包含全部元素且顺序需要一致。

  • 如果数组中的元素为对象,则需要通过extracting提取出对象的属性值,再来判断;如果提取出对象的多个属性值时,可以用tuple将多个属性值包装成元组

  • atIndex来获取指定位置/索引的元素

    @Test
    public void test_array_null_or_empty() {
    String[] nullNames = null;
    assertThat(nullNames).isNull();

    String[] emptyNames = {};
    assertThat(emptyNames).isEmpty();
    assertThat(emptyNames).hasSize(0);
    

    }

    @Test
    public void test_array_contains() {
    String[] names = {“Python”, “Golang”, “Docker”, “Java”};

    assertThat(names).contains("Docker");
    assertThat(names).doesNotContain("Haddop");
    
    assertThat(names).containsExactly("Python", "Golang", "Docker", "Java"); // 完全匹配,且顺序也一致
    assertThat(names).contains("Java", "Docker", "Golang", "Python"); // 完全匹配,顺序可以不一致
    
    assertThat(names).contains("Docker", atIndex(2)); // names[2]
    

    }

    @Test
    public void test_array_object_contains() {
    Person[] names = {new Person(“William”, 34),
    new Person(“John”, 36),
    new Person(“Tommy”, 28),
    new Person(“Lily”, 32)};

    assertThat(names).extracting(Person::getName)
      .containsExactly("William", "John", "Tommy", "Lily");
    
    assertThat(names).extracting("name", "age")
      .containsExactly(tuple("William", 34),
                       tuple("John", 36),
                       tuple("Tommy", 28),
                       tuple("Lily", 32));
    
    assertThat(names).extracting(x -> x.getName(), x -> x.getAge())
      .containsExactly(tuple("William", 34),
                       tuple("John", 36),
                       tuple("Tommy", 28),
                       tuple("Lily", 32));
    

    }

集合

List

参见:

List的断言与数组的断言类似。

@Test
public void test_list_contains() {
    List<Person> names = Arrays.asList(new Person("William", 34),
                                       new Person("John", 36),
                                       new Person("Tommy", 28),
                                       new Person("Lily", 32));

    assertThat(names).extracting(Person::getName)
      .containsExactly("William", "John", "Tommy", "Lily");

    assertThat(names).extracting("name", "age")
      .containsExactly(tuple("William", 34),
                       tuple("John", 36),
                       tuple("Tommy", 28),
                       tuple("Lily", 32));

    assertThat(names).extracting(x -> x.getName(), x -> x.getAge())
      .containsExactly(tuple("William", 34),
                       tuple("John", 36),
                       tuple("Tommy", 28),
                       tuple("Lily", 32));
  }
}
Map

参见:

说明:

  • 可以对key进行断言:

    • containsKeys:包含指定key。
    • containsOnlyKeys:包含全部key,对顺序无要求。
  • 可以对value进行断言:

    • containsValues:包含指定value。
  • 可以对entry进行断言:

    • contains:包含指定entry。
    • containsOnly:包含全部entry,对顺序无要求。
  • 注意,HashMap中的entry无序,而TreeMap中的entry有序。因此TreeMap可用containsExactly判断是否包含全部Entry,且顺序也保持一致。

    @Test
    public void test_hash_map_contains() {
    Person william = new Person(“William”, 34);
    Person john = new Person(“John”, 36);
    Person tommy = new Person(“Tommy”, 28);
    Person lily = new Person(“Lily”, 32);
    Person jimmy = new Person(“Jimmy”, 38);

    Map<String, Person> map = new HashMap<>();
    map.put("A1001", william);
    map.put("A1002", john);
    map.put("A1003", tommy);
    map.put("A1004", lily);
    
    Map<String, Person> map2 = new HashMap<>();
    map2.put("A1001", william);
    map2.put("A1002", john);
    map2.put("A1003", tommy);
    map2.put("A1004", lily);
    
    // contains keys
    assertThat(map).containsKeys("A1003");
    assertThat(map).containsOnlyKeys("A1001", "A1002", "A1003", "A1004"); // 需要包含全部的key
    assertThat(map).doesNotContainKeys("B1001");
    
    // contains values
    assertThat(map).containsValues(tommy);
    assertThat(map).doesNotContainValue(jimmy);
    
    // contains entries
    assertThat(map).containsEntry("A1003", tommy);
    assertThat(map).containsAllEntriesOf(map2);
    assertThat(map).contains(entry("A1003", tommy));
    // 需要包含全部的entry
    assertThat(map).containsOnly(entry("A1001", william),
                                 entry("A1002", john),
                                 entry("A1003", tommy),
                                 entry("A1004", lily));
    

    }

    @Test
    public void test_tree_map_contains() {
    Person william = new Person(“William”, 34);
    Person john = new Person(“John”, 36);
    Person tommy = new Person(“Tommy”, 28);
    Person lily = new Person(“Lily”, 32);

    Map<String, Person> map = new TreeMap<>();
    map.put("A1001", william);
    map.put("A1002", john);
    map.put("A1003", tommy);
    map.put("A1004", lily);
    
    // 需要包含全部的entry,且顺序也一致
    assertThat(map).containsExactly(entry("A1001", william),
                                    entry("A1002", john),
                                    entry("A1003", tommy),
                                    entry("A1004", lily));
    

    }

    @Test
    public void test_map_extracting() {
    Person william = new Person(“William”, 34);
    Person john = new Person(“John”, 36);
    Person tommy = new Person(“Tommy”, 28);
    Person lily = new Person(“Lily”, 32);

    Map<String, Person> map = new TreeMap<>();
    map.put("A1001", william);
    map.put("A1002", john);
    map.put("A1003", tommy);
    map.put("A1004", lily);
    
    assertThat(map).extracting("A1001").isEqualTo(william);
    assertThat(map).extracting("A1001")
      .extracting("name", "age")
      .containsExactly("William", 34);
    

    }

Set

Set的断言与List的断言类似。

异常

参见:

说明:

  • try catch 来捕捉可能抛出的异常。

  • isInstanceOf来判断异常类型。

  • hasMessageContaining来模糊匹配异常信息,用hasMessage来精确匹配异常信息。

  • hasCauseInstanceOf 来判断异常原因(root cause)的类型。

  • 对checked exception和runtime exception的断言是一样的。

    public class ExceptionExampleTest {

    public String readFirstLine(String fileName) throws IncorrectFileNameException {
        try (Scanner file = new Scanner(new File(fileName))) {
            if (file.hasNextLine()) {
                return file.nextLine();
            }
        } catch (FileNotFoundException e) {
           throw new IncorrectFileNameException("Incorrect file name: " + fileName, e);
        }
        return "";
    }
    
    /**
     * java custom exception:
     * https://www.baeldung.com/java-new-custom-exception
      */
    
    public class IncorrectFileNameException extends Exception {
        public IncorrectFileNameException(String message) {
            super(message);
        }
    
        public IncorrectFileNameException(String message, Throwable cause) {
            super(message, cause);
        }
    }
    
    
    @Test
    public void test_exception_ArithmeticException() {
        try {
            double result = 1 / 0;
        } catch(Exception e) {
            // check exception type
            assertThat(e).isInstanceOf(ArithmeticException.class);
    
            // check exception message
            assertThat(e).hasMessageContaining("/ by zero");
        }
    }
    
    @Test
    public void test_exception_ArrayIndexOutOfBoundsException() {
        try {
            String[] names = {"William", "John", "Tommy", "Lily"};
            String name = names[4];
        } catch (Exception e) {
            // check exception type
            assertThat(e).isInstanceOf(ArrayIndexOutOfBoundsException.class);
    
            // check exception message
            assertThat(e).hasMessage("4");
        }
    }
    
    @Test
    public void test_exception_IllegalStateException() {
        try {
            List<String> names = Arrays.asList("William", "John", "Tommy", "Lily");
            Iterator<String> iter = names.iterator();
            iter.remove();
        } catch (Exception e) {
            // check exception type
            assertThat(e).isInstanceOf(IllegalStateException.class);
        }
    }
    
    
    @Test
    public void test_custom_exception() {
        try {
            String line = readFirstLine("./unknown.txt");
        } catch (Exception e) {
            // check exception type
            assertThat(e).isInstanceOf(IncorrectFileNameException.class);
    
            // check exception message
            assertThat(e).hasMessageContaining("Incorrect file name");
    
            // check cause type
            assertThat(e).hasCauseInstanceOf(FileNotFoundException.class);
        }
    }
    

    }

Optional

参见:

说明:

  • isPresent来判断Optional是否为空。

  • 如果Optional中的值是基本类型,用hasValue判断。

  • 如果Optional中的值为对象,用hasValueSatisfying判断。

    public class OptionalExampleTest {

    @Test
    public void test_optional_null_or_empty() {
        Optional<String> op1 = Optional.ofNullable(null);
    
        assertThat(op1).isNotPresent();
        assertThat(op1).isEmpty();
    
        Optional<String> op2 = Optional.empty();
        assertThat(op2).isNotPresent();
        assertThat(op2).isEmpty();
    }
    
    @Test
    public void test_optional_basic_type() {
        Optional<String> op1 = Optional.ofNullable("hello");
        assertThat(op1).isPresent().hasValue("hello");
    
        Optional<Integer> op2 = Optional.ofNullable(365);
        assertThat(op2).isPresent().hasValue(365);
    }
    
    @Test
    public void test_optional_object() {
        Optional<Person> p1 = Optional.ofNullable(new Person("William", 34));
    
        assertThat(p1).isPresent()
                .hasValueSatisfying(x -> {
                    assertThat(x).extracting(Person::getName, Person::getAge)
                            .containsExactly("William", 34);
                });
    }
    

    }

AssertJ 是 JAVA 的流畅断言库。示例代码:// unique entry point to get access to all assertThat methods and utility methods (e.g. entry) import static org.assertj.core.api.Assertions.*;  // common assertions assertThat(frodo.getName()).isEqualTo("Frodo"); assertThat(frodo).isNotEqualTo(sauron)                  .isIn(fellowshipOfTheRing);  // String specific assertions assertThat(frodo.getName()).startsWith("Fro")                            .endsWith("do")                            .isEqualToIgnoringCase("frodo");  // collection specific assertions assertThat(fellowshipOfTheRing).hasSize(9)                                .contains(frodo, sam)                                .doesNotContain(sauron);  // using extracting magical feature to check fellowshipOfTheRing characters name :) assertThat(fellowshipOfTheRing).extracting("name").contains("Boromir", "Gandalf", "Frodo", "Legolas")                                                   .doesNotContain("Sauron", "Elrond");  // map specific assertions, ringBearers initialized with the elves rings and the one ring bearers. assertThat(ringBearers).hasSize(4)                        .contains(entry(oneRing, frodo), entry(nenya, galadriel))                        .doesNotContainEntry(oneRing, aragorn);  // and many more assertions : dates, file, numbers, exceptions ... 标签:AssertJ
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值