Published on

為什麼要推薦 AssertJ

Authors
  • avatar
    Name
    ChienYu
    Twitter
Table of Contents

Ref

文章內的資訊參考整理自

為什麼要推薦 AssertJ

Junit 算是已經非常通用且主流的測試工具, 通常寫起來會像這樣.

JunitExample.java
// 需要依據測試的類型, 引入各式各樣的斷言
import static org.junit.Assert.*;

class JunitExample {

  private final Calculator calculator = new Calculator();

  @Test
  void addition() {
    assertEquals(2, calculator.add(1, 1));
  }
}

而用 AssertJ 改寫上面的案例, 會像這樣.

AssertJExample.java
// 只需要引入 Assertions, 統一接口做斷言
import org.assertj.core.api.Assertions;

class AssertJExample {

  private final Calculator calculator = new Calculator();

  @Test
  void addition() {
    assertThat(calculator.add(1, 1)).isEqualTo(2);
  }
}

我自己喜歡以 Assertions.assertThat() 的方式起手, 細節請 參閱 assumptions 章節 幾乎可以放入任意要驗證的型別, 包含 Java8 的 Optional 或是 Collection 集合類的物件, 後續再接驗證的邏輯細節, 由左而右的開發與閱讀習慣, AssertJ 會來得更容易理解一點, 且寫法較貼近 lambda 寫法方式. 若要同時使用 Junit 與 AssertJ 也是沒問題的, 兩者並沒有互相衝突.

描述測試

為了讓測試報告更明確, 可已透過 as() 來撰寫描述, 讓實際結果呈現於報告上.

@Test
public void test() {
  final BigDecimal act = BigDecimal.TEN;

  Assertions.assertThat(act)
    .as("check the value(%s) is 100", act.toPlainString())
    .isEqualByComparingTo("100");
}
org.junit.ComparisonFailure: [check the value(10) is 100]
Expected :100
Actual   :10

適應 JAVA8 Lambda 特性

支援 Stream, Filter, Mapping, Flat 等特性, 就簡單地挑 Filter 做介紹了, 其他用法就如同 Lambda 的 Streaming 的寫法差不多.

Filter on Assertions

就如同 Java8 的 Filter(Predicate) 可以簡單的檢核 Collection 結果.

Member.java
@Setter
@Getter
@AllArgsConstructor
class Member {

  private String name;

  private Integer age;
}
@Test
public void test() {
  final Member sam = new Member("Sam", 18);
  final Member susan = new Member("Susan", 20);
  final Member eric = new Member("Eric", 14);
  final List<Member> members = Arrays.asList(sam, susan, eric);

  Assertions.assertThat(members)
    .filteredOn(member -> member.getAge() > 18)
    .doesNotContainNull()
    .as("Only Susan age is gather than 18")
    .containsOnly(susan);
}

Optional Assertions

也支援 Optional.

@Test
public void test() {
  final Optional<String> opt = Optional.of("hello");

  Assertions.assertThat(opt)
    .isPresent()
    .isNotEmpty()
    .as("the option is existed and has value `hello`")
    .hasValue("hello")
    .containsSame("hello");
}

針對 POJO 的 Assertions

在測試 POJO 物件 (單純的貧血模型, key value 物件) 時, 會需要很瑣碎的把每個欄位拿出來做驗證. 在早期我可能會用 Map 把 Pojo 物件重新包過做驗證, 或是非常暴力的直接轉為 JASON 做字串驗證, 但這都不是我理想中的方式.

就拿上面的 Member.java 做案例, 來展示如何用 AssertJ 更無痛的做驗證.

JunitExample.java
@Test
public void test() {
  final Member sam = new Member("Sam", 18);

  // 需要逐一針對欄位做 get 再做驗證
  Assert.assertEquals("Sam", sam.getName());
  Assert.assertEquals(18, sam.getAge().intValue());
}

在 POJO 的驗證上, 我會比較偏向 test1 的方式, 針對重點欄位驗證就好. 然 AssertJ 同時也提供了另一種 Recursive 每一個欄位的方式做驗證. Recursive 的前提是每個欄位都有提供 get() 的方式做存取.

AssertJExample.java
@Test
public void test1() {
  final Member sam = new Member("Sam", 18);

  Assertions.assertThat(sam)
    .extracting("name", "age")
    .doesNotContainNull()
    .containsExactly("Sam", 18);
}

@Test
public void test2() {
  final Member sam = new Member("Sam", 18);

  // 透過另一個 Pojo 對欄位做驗證
  final Member expected = new Member("Sam", 18);

  // Recursive each fields on class
  Assertions.assertThat(sam)
    .isEqualToComparingFieldByFieldRecursively(expected);
}

例外測試 Exception assertions

反向測試, 也是測試中很重要的一個環節, 在一個 Exception Stack 中, 可以針對 Root Cause 做細節的驗證.

DemoService.java
class DemoService {

  static void call() {
    // throw the nested exception
    throw new IllegalStateException("something wrong", new NullPointerException("invoke the NPE"));
  }
}
@Test
public void test() {
  final Throwable throwable = Assertions.catchThrowable(DemoService::call);

  Assertions.assertThat(throwable)
    .isInstanceOf(IllegalStateException.class)
    .hasMessageContaining("something wrong")
    .hasRootCauseExactlyInstanceOf(NullPointerException.class)
    .hasStackTraceContaining("invoke the NPE");
}

Soft Assertions

若有更複雜的邏輯, 推薦用 Soft Assertions, 它可以讓你一次走完流程, 最終再回報哪些細節測試失敗, 不會因為某個驗證就中斷.

@Test
public void test() {
  final Member sam = new Member("Sam", 18);
  final Member susan = new Member("Susan", 20);
  final Member eric = new Member("Eric", 14);
  final List<Member> members = Arrays.asList(sam, susan, eric);

  final SoftAssertions softly = new SoftAssertions();

  // assert is fail
  softly.assertThat(members)
    .hasSize(2)
    .extracting("name")
    .contains("John");

  softly.assertAll();
}

錯誤報告會這樣提示

The following 2 assertions failed:
1)
Expected size:<2> but was:<3> in:
...

2)
Expecting:
 <["Sam", "Susan", "Eric"]>
to contain:
 <["John"]>
but could not find:
 <["John"]>

自定義 Assertions

自定義的 Assertions 需繼承 AbstractAssert<SELF, ACTUAL>, 採用泛型 GenericType 的設計.

  • SELF 用來定義驗證器本身.
  • ACTUAL 用來定義要驗證的目標物件.

一樣擴充上面的 Member 案例來做解釋.

Member.java
@Setter
@Getter
@AllArgsConstructor
class Member {

  private String name;

  private Integer age;

  // 新增 email 欄位
  private String email;
}
MemberAssert.java
public class MemberAssert extends AbstractAssert<MemberAssert, Member> {

  public MemberAssert(Member actual, Class<?> selfType) {
    super(actual, selfType);
  }

  public MemberAssert(Member actual) {
    super(actual, MemberAssert.class);
  }

  /**
   * 我自己喜歡 Assumptions 這種風格, 所以希望驗證以這個方法作為入口去接後續的操作.
   */
  public static MemberAssert assertThat(Member actual) {
    return new MemberAssert(actual);
  }

  /**
   * Check the Member name should not be null.
   */
  public MemberAssert nameShouldNotBeNull() {
    // check the instance is not null
    isNotNull();

    if (Strings.isNullOrEmpty(actual.getName())) {
      failWithMessage("Check the Member name should not be null");
    }

    return this;
  }

  /**
   * Check the Member age is positive.
   */
  public MemberAssert ageIsPositive() {
    // check the instance is not null
    isNotNull();

    if (actual.getAge() <= 0) {
      failWithMessage("Check the Member(%s) age should be Positive", actual.getName());
    }

    return this;
  }

  /**
   * Check the Member email format is ok.
   */
  public MemberAssert checkEmailFormat() {
    // check the instance is not null
    isNotNull();

    // 簡單地用 org.apache.commons.validator 驗證 XD
    if (!EmailValidator.getInstance().isValid(actual.getEmail())) {
      failWithMessage("Check the Member(%s) email(%s) format has wrong", actual.getName(), actual.getEmail());
    }

    return this;
  }
}
public void test() {
  final Member sam = new Member("Sam", 18, "sam@email.com");

  // assert is pass
  MemberAssert.assertThat(sam)
    .nameShouldNotBeNull()
    .ageIsPositive()
    .checkEmailFormat();

  // assert is fail
  final Member susan = new Member("Susan", 20, "susan");
  MemberAssert.assertThat(susan)
    .nameShouldNotBeNull()
    .ageIsPositive()
    .checkEmailFormat();
}

錯誤訊息就會如同上面定義的顯示

Check the Member(Susan) email(susan) format has wrong