AssertJ核心功能
AssertJ有很多很有用的API,你未必知道,下面是这些api的列表:
由于内容较多,所以这里只讲最新版本的内容
基本技巧:
java8新增的断言,请查看这里
配置IDE自动提示assertThat
如果你想输入assert就让ide自动帮你完成assertThat(并且是assertj的)的输入.你可以配置你的ide
- Eclipse配置
- Go to : preferences > Java > Editor > Content assist > Favorites > New Type
- Enter : org.assertj.core.api.Assertions
- You should see : org.assertj.core.api.Assertions.* in a the list.
- intellij idea无需特殊配置,自动会提示
用as(String description,Object... agrs)可以描述你的断言
默认的错误消息只是简单提示你预期的值和真实的值.没有具体的业务描述.使用它可以对你的断言进行描述,尤其是布尔类型的断言 你可以用as(String description,Object... agrs)设置你的描述信息,但是一定在调用断言之前设置,否则将会被忽略
@Test
public void test_as(){
Jedi jedi = new Jedi("Judi", "green");
assertThat(jedi.getName()).as("检查%s的颜色", jedi.getName()).isEqualTo("blue");
}
提示信息会变成这样:
org.junit.ComparisonFailure: [检查Judi的颜色]
Expected :"blue"
Actual :"Judi"
迭代器或者数组的过滤和断言
过滤有以下这些方式:
- java8的Predicate
- condition (org.assertj.core.api.Condition)
- 数据/迭代中的元素的一些属性/字段的操作
使用Predicate过滤:
@Test
public void test_Predicate(){
assertThat(fellowshipOfTheRing).filteredOn( character -> character.getName().contains("o") )//过滤name属性中包含o的对象
.containsOnly(aragorn, frodo, legolas, boromir);//断言
}
过滤属性或者字段:
首先你指定属性/字段名称通过给定的预期值进行筛选,该过滤器会先尝试从属性中获取value,然后再去字段中获取.默认情况下是可以读取私有字段的,你可以通过Assertions.setAllowExtractingPrivateFields(false)禁止.
过滤器支持读取嵌套属性/字段,但是如果嵌套的属性/字段有一个是null那么整个嵌套都会被认为是null.例如:如果"address.street.name"会返回null,那么"address.street"也是返回null 过滤器的基本操作:not,in,notIn
List<TolkienCharacter> fellowshipOfTheRing = Lists.newArrayList();
@Test
public void test_filters() {
//筛选出TolkienCharacter中的race属性是HOBBIT的对象,断言包含sam, frodo, pippin, merry
assertThat(fellowshipOfTheRing).filteredOn("race", HOBBIT).containsOnly(sam, frodo, pippin, merry);
//嵌套属性
assertThat(fellowshipOfTheRing).filteredOn("race.name", "Man").containsOnly(aragorn, boromir);
//筛选出race属性不是HOBBIT和MAN的对象
assertThat(fellowshipOfTheRing).filteredOn("race", notIn(HOBBIT, MAN)).containsOnly(gandalf, gimli, legolas);
//筛选出race属性是MAIA和MAN的对象
assertThat(fellowshipOfTheRing).filteredOn("race", in(MAIA, MAN)).containsOnly(gandalf, boromir, aragorn);
//筛选出race属性不是HOBBIT的对象
assertThat(fellowshipOfTheRing).filteredOn("race", not(HOBBIT))
.containsOnly(gandalf, boromir, aragorn, gimli, legolas);
//支持多次过滤
assertThat(fellowshipOfTheRing).filteredOn("race", MAN)
.filteredOn("name", not("Boromir"))
.containsOnly(aragorn);
}
为了方便理解将所有涉及到的类都列出如下:
public class TolkienCharacter {
// public to test extract on field
public int age;
private String name;
private Race race;
// not accessible field to test that field by field comparison does not use it
@SuppressWarnings("unused")
private long notAccessibleField = 1;
public enum Race {
HOBBIT("Hobbit", false, GOOD), MAIA("Maia", true, GOOD), MAN("Man", false, NEUTRAL), ELF("Elf", true, GOOD), DWARF("Dwarf", false, GOOD), ORC("Orc", false, EVIL);
private final String name;
public final boolean immortal;
private Alignment alignment;
Race(String name, boolean immortal, Alignment alignment) {
this.name = name;
this.immortal = immortal;
this.alignment = alignment;
}
public enum Alignment {
SUPER_EVIL, EVIL, NEUTRAL, GOOD, SUPER_GOOD;
}
用Condition过滤
过滤器只会保留迭代/数组中满足匹配Condition的元素 有两个方法可用:being(Condition)和having(Condition):
@Test
public void test_condition_BasketBallPlayer() {
BasketBallPlayer rose = new BasketBallPlayer(new Name("Derrick", "Rose"), "Chicago Bulls");
rose.setAssistsPerGame(8);
rose.setPointsPerGame(25);
rose.setReboundsPerGame(5);
BasketBallPlayer lebron = new BasketBallPlayer(new Name("Tony", "Parker"), "Spurs");
lebron.setAssistsPerGame(9);
lebron.setPointsPerGame(21);
lebron.setReboundsPerGame(5);
BasketBallPlayer james = new BasketBallPlayer(new Name("Lebron", "James"), "Miami Heat");
james.setAssistsPerGame(6);
james.setPointsPerGame(27);
james.setReboundsPerGame(8);
BasketBallPlayer dwayne = new BasketBallPlayer(new Name("Dwayne", "Wade"), "Miami Heat");
dwayne.setAssistsPerGame(16);
dwayne.setPointsPerGame(55);
dwayne.setReboundsPerGame(16);
BasketBallPlayer noah = new BasketBallPlayer(new Name("Joachim", "Noah"), "Chicago Bulls");
noah.setAssistsPerGame(4);
noah.setPointsPerGame(10);
noah.setReboundsPerGame(11);
dwayne.getTeamMates().add(james);
james.getTeamMates().add(dwayne);
Condition<BasketBallPlayer> mvpStats = new Condition<BasketBallPlayer>() {
@Override
public boolean matches(BasketBallPlayer player) {
return player.getPointsPerGame() > 20 && (player.getAssistsPerGame() >= 8 || player.getReboundsPerGame() >= 8);
}
};
List<BasketBallPlayer> players = newArrayList();
players.add(rose);
players.add(lebron);
players.add(noah);
assertThat(players).filteredOn(mvpStats).containsOnly(rose, lebron);
}
为了方便理解列出BasketBallPlayer类:
public class BasketBallPlayer {
private Name name;
public double size;
private float weight;
private int pointsPerGame;
private int assistsPerGame;
private int reboundsPerGame;
private String team;
private boolean rookie;
private List<BasketBallPlayer> teamMates = new ArrayList<BasketBallPlayer>();
private List<int[]> points = new ArrayList<>();
public BasketBallPlayer(Name name, String team) {
setName(name);
setTeam(team);
}
获取迭代/数组中元素的属性/字段
比如说,你有请求一个service/dao然后得到一个TolkienCharacters的集合(或者数组),想要检查结果,你需要先建立一个预期TolkienCharacters(s).这个工作量可能会很大
List<TolkienCharacter> fellowshipOfTheRing = tolkienDao.findHeroes(); // 集合中包含frodo, sam, aragorn ...
// 这里你需要创建预期的TolkienCharacter:frodo,aragorn
assertThat(fellowshipOfTheRing).contains(frodo, aragorn);
然而,通常情况下我们只是检查集合中元素的某些属性或字段,这也你需要在断言之前写一段代码获取这些字段或者属性,比如:
//获取name属性
List<String> names = new ArrayList<String>();
for (TolkienCharacter tolkienCharacter : fellowshipOfTheRing) {
names.add(tolkienCharacter.getName());
}
// ... 然后断言
assertThat(names).contains("Boromir", "Gandalf", "Frodo", "Legolas");
现在你可以用assertj帮你提取这些属性了,这样做:
//"name"必须是TolkienCharacter的属性或者字段
assertThat(fellowshipOfTheRing).extracting("name")
.contains("Boromir", "Gandalf", "Frodo", "Legolas")
.doesNotContain("Sauron", "Elrond");
而且你可以同时获取多个属性/字段,比如这样:
// 如果你想要同时检查多个属性,你必须使用tuple
import static org.assertj.core.api.Assertions.tuple;
//获取 name, age 和嵌套属性 race.name
assertThat(fellowshipOfTheRing).extracting("name", "age", "race.name")
.contains(tuple("Boromir", 37, "Man"),
tuple("Sam", 38, "Hobbit"),
tuple("Legolas", 1000, "Elf"));
当前元素的name,age还有race.name的值会分组到tuple中,所以你需要用tuple来获取这些值
在只检查一个属性的时候,你可以指定这个属性的类型
assertThat(fellowshipOfTheRing).extracting("name", String.class)
.contains("Boromir", "Gandalf", "Frodo", "Legolas")
.doesNotContain("Sauron", "Elrond");
更多的用法请查看这里
获取flatMap
准备:
private Extractor<BasketBallPlayer,List<BasketBallPlayer>> teamMates =new PlayerTeammatesExtractor();
public class PlayerTeammatesExtractor implements Extractor<BasketBallPlayer,List<BasketBallPlayer>> {
@Override
public List<BasketBallPlayer> extract(BasketBallPlayer input) {
return input.getTeamMates();
}
}
@Test
public void iterable_assertions_on_flat_extracted_values_examples(){
BasketBallPlayer rose = new BasketBallPlayer(new Name("Derrick", "Rose"), "Chicago Bulls");
rose.setAssistsPerGame(8);
rose.setPointsPerGame(25);
rose.setReboundsPerGame(5);
BasketBallPlayer noah = new BasketBallPlayer(new Name("Joachim", "Noah"), "Chicago Bulls");
noah.setAssistsPerGame(4);
noah.setPointsPerGame(10);
noah.setReboundsPerGame(11);
BasketBallPlayer james = new BasketBallPlayer(new Name("Lebron", "James"), "Miami Heat");
james.setAssistsPerGame(6);
james.setPointsPerGame(27);
james.setReboundsPerGame(8);
BasketBallPlayer dwayne = new BasketBallPlayer(new Name("Dwayne", "Wade"), "Miami Heat");
dwayne.setAssistsPerGame(16);
dwayne.setPointsPerGame(55);
dwayne.setReboundsPerGame(16);
noah.getTeamMates().add(rose);
rose.getTeamMates().add(noah);
james.getTeamMates().add(dwayne);
ArrayList<BasketBallPlayer> basketBallPlayers = newArrayList(noah, james);
//通过指定teamMates属性,获取所有的getTeamMates()返回的集合
assertThat(basketBallPlayers).flatExtracting("teamMates").contains(dwayne, rose);
//这里需要你实现Extractor
assertThat(basketBallPlayers).flatExtracting(teamMates).contains(dwayne, rose);
}
关于iterable/数组元素的返回值的断言
从iterable/数组中获取出来的对象的调用方法会被放入一个新的iterable/数组中,变成被测试的对象. 这样可以用来测试元素的调用方法的结果而不是测试元素本身.这种方式对于不符合java bean的getter规范的属性特别有意义(比如toString或者String status())
@Test
public void iterable_assertions_on_extracted_method_result_example() {
// 获取'toString'返回的结果
assertThat(fellowshipOfTheRing).extractingResultOf("getRace").contains("Frodo 33 years old Hobbit",
"Aragorn 87 years old Man");
assertThat(fellowshipOfTheRing).extractingResultOf("getSurname").contains("Sam the Hobbit",
"Merry the Hobbit");
}
用soft断言收集所有错误
一般情况下在你的断言中如果有一个断言错误,那么整个测试就会停止,使用soft断言会运行完你的所有断言之后才停止运行,并且收集你的错误信息 如果你使用的是标准的普通的断言,例如:
@Test
public void host_dinner_party_where_nobody_dies() {
Mansion mansion = new Mansion();
mansion.hostPotentiallyMurderousDinnerParty();
assertThat(mansion.guests()).as("Living Guests").isEqualTo(7);
assertThat(mansion.kitchen()).as("Kitchen").isEqualTo("clean");
assertThat(mansion.library()).as("Library").isEqualTo("clean");
assertThat(mansion.revolverAmmo()).as("Revolver Ammo").isEqualTo(6);
assertThat(mansion.candlestick()).as("Candlestick").isEqualTo("pristine");
assertThat(mansion.colonel()).as("Colonel").isEqualTo("well kempt");
assertThat(mansion.professor()).as("Professor").isEqualTo("well kempt");
}
你会得到这样的错误提示信息:
org.junit.ComparisonFailure: [Living Guests] expected:<[7]> but was<[6]>
如果使用soft断言你就可以收集所有的失败的断言信息:
@Test
public void host_dinner_party_where_nobody_dies() {
Mansion mansion = new Mansion();
mansion.hostPotentiallyMurderousDinnerParty();
// use SoftAssertions instead of direct assertThat methods
SoftAssertions softly = new SoftAssertions();
softly.assertThat(mansion.guests()).as("Living Guests").isEqualTo(7);
softly.assertThat(mansion.kitchen()).as("Kitchen").isEqualTo("clean");
softly.assertThat(mansion.library()).as("Library").isEqualTo("clean");
softly.assertThat(mansion.revolverAmmo()).as("Revolver Ammo").isEqualTo(6);
softly.assertThat(mansion.candlestick()).as("Candlestick").isEqualTo("pristine");
softly.assertThat(mansion.colonel()).as("Colonel").isEqualTo("well kempt");
softly.assertThat(mansion.professor()).as("Professor").isEqualTo("well kempt");
//这一步一定要写
softly.assertAll();
}
这时候你会看到这样的提示信息:
org.assertj.core.api.SoftAssertionError:
The following 4 assertions failed:
1) [Living Guests] expected:<[7]> but was:<[6]>
2) [Library] expected:<'[clean]'> but was:<'[messy]'>
3) [Candlestick] expected:<'[pristine]'> but was:<'[bent]'>
4) [Professor] expected:<'[well kempt]'> but was:<'[bloodied and dishevelled]'>
assertj还提供了JUnit的规则(rule),它会自动调用soft断言来全局收集错误信息
@Rule
public final JUnitSoftAssertions softly = new JUnitSoftAssertions();
@Test
public void host_dinner_party_where_nobody_dies() {
Mansion mansion = new Mansion();
mansion.hostPotentiallyMurderousDinnerParty();
// use SoftAssertions instead of direct assertThat methods
softly.assertThat(mansion.guests()).as("Living Guests").isEqualTo(7);
softly.assertThat(mansion.kitchen()).as("Kitchen").isEqualTo("clean");
softly.assertThat(mansion.library()).as("Library").isEqualTo("clean");
softly.assertThat(mansion.revolverAmmo()).as("Revolver Ammo").isEqualTo(6);
softly.assertThat(mansion.candlestick()).as("Candlestick").isEqualTo("pristine");
softly.assertThat(mansion.colonel()).as("Colonel").isEqualTo("well kempt");
softly.assertThat(mansion.professor()).as("Professor").isEqualTo("well kempt");
// 这里就不需要再写softly.assertAll()了
}
这里只能用于junit,testNG没办法做全局收集信息,因为testNG只要抛出一个异常就会跳过后面的测试
用字符串断言,断言文件内容
File xFile = writeFile("xFile", "The Truth Is Out There");
// 典型的文件断言
assertThat(xFile).exists().isFile().isRelative();
// 文件内容断言
assertThat(Assertions.contentOf(xFile)).startsWith("The Truth").contains("Is Out").endsWith("There");
异常断言
如何断言异常被抛出,并检查它是你所预期的? 在java8中测试断言是非常优雅的,使用assertThartThrownBy(ThrowingCallable)来捕获异常,断言Throwable.ThrowingCallable是一个功能接口,可以通过lambda来调用 例如:
@Test
public void testException() {
assertThatThrownBy(() -> { throw new Exception("boom!"); }).isInstanceOf(Exception.class)
.hasMessageContaining("boom");
}
还有一种更自然的语法:
@Test
public void testException() {
assertThatExceptionOfType(IOException.class).isThrownBy(() -> { throw new IOException("boom!"); })
.withMessage("%s!", "boom")
.withMessageContaining("boom")
.withNoCause();
}
BDD爱好者还可以这样写:
@Test
public void testException3() {
//given
List<String> list = Lists.newArrayList();
// when
Throwable thrown = catchThrowable(() -> { list.get(1); });
// then
assertThat(thrown).isInstanceOf(IndexOutOfBoundsException.class)
.hasMessageContaining("Index");
}
官网原文还有java7的异常断言,由于我们使用的是java8这里不再翻译有兴趣的可以看这里
使用自定义的comparison来做比较断言
有的适合你不想用equalse放来比较对象,那么你可以使用以下两种方法:
- usingComparator(Comparator) : 关注对象本身的断言
- usingElementComparator(Comparator) :关注iterable/数组的元素的断言
usingComparator(Comparator)例子:
//frodo和sam都是race为Hobbit类型的TolkienCharacter的实例,很明显他们是不相等的
assertThat(frodo).isNotEqualTo(sam);
//但是,如果我们仅是比较他们的race属性,那么他们是相等的
//sauron在集合fellowshipOfTheRing中
assertThat(frodo).usingComparator(raceComparator).isEqualTo(sam);
usingElementComparator(Comparator)的例子
//普通的比较,fellowshipOfTheRing中包含gandalf不包含sauron
assertThat(fellowshipOfTheRing).contains(gandalf).doesNotContain(sauron);
//但是只比较race属性的话,sauron就在fellowshipOfTheRing集合中了
assertThat(fellowshipOfTheRing).usingElementComparator(raceComparator).contains(sauron);
属性比较
assertj为属性/字段比较提供了以下几种方法:
- isEqualToComparingFieldByField : 比较各字段/属性包括继承的-不是递归
- isEqualToComparingOnlyGivenFields :仅仅比较指定的字段/属性-非递归
- isEqualToIgnoringGivenFields : 比较除了指定的字段/属性以外的字段/属性-非递归
- isEqualToIgnoringNullFields : 只比较非空字段/属性-非递归
- isEqualToComparingFieldByFieldRecursively : 比较所有的字段/属性-递归
除了isEqualToComparingFieldByFieldRecursively其他的都不是递归的.递归是指:如果对象的属性也是一个对象,那么assertj会自动调用该属性的equalse方法进行比较.另外isEqualToComparingFieldByFieldRecursively默认使用字段的比较,除非你有自定义的equalse方法实现.
以下例子中TolkienCharacter的equalse方法没有被覆盖 isEqualToComparingFieldByField例子:
TolkienCharacter frodo = new TolkienCharacter("Frodo", 33, HOBBIT);
TolkienCharacter frodoClone = new TolkienCharacter("Frodo", 33, HOBBIT);
// equalse比较的是对象引用,所以是失败的
assertThat(frodo).isEqualsTo(frodoClone);
//两者只是比较了属性值而已,所以是相等的
assertThat(frodo).isEqualToComparingFieldByField(frodoClone);
isEqualToComparingOnlyGivenFields例子:
TolkienCharacter frodo = new TolkienCharacter("Frodo", 33, HOBBIT);
TolkienCharacter sam = new TolkienCharacter("Sam", 38, HOBBIT);
// 只是比较race属性的话,它们俩都是HOBBIT
assertThat(frodo).isEqualToComparingOnlyGivenFields(sam, "race"); // OK
// 嵌套属性比较
assertThat(frodo).isEqualToComparingOnlyGivenFields(sam, "race.name"); // OK
// name属性两者不相等
assertThat(frodo).isEqualToComparingOnlyGivenFields(sam, "name", "race"); // FAIL
isEqualToIgnoringGivenFields例子:
TolkienCharacter frodo = new TolkienCharacter("Frodo", 33, HOBBIT);
TolkienCharacter sam = new TolkienCharacter("Sam", 38, HOBBIT);
// 如果忽略name和age属性那么两者的race属性都是HOBBIT
assertThat(frodo).isEqualToIgnoringGivenFields(sam, "name", "age"); // OK both are HOBBIT
// ... 如果只忽略age属性,那么就不相等了
assertThat(frodo).isEqualToIgnoringGivenFields(sam, "age"); // FAIL
isEqualToIgnoringNullFields例子:
TolkienCharacter frodo = new TolkienCharacter("Frodo", 33, HOBBIT);
TolkienCharacter mysteriousHobbit = new TolkienCharacter(null, 33, HOBBIT);
// mysteriousHobbit的name属性是null所以会被忽略,只比较age和race
assertThat(frodo).isEqualToIgnoringNullFields(mysteriousHobbit); // OK
// ... 两者的位置是不可以更换的!
assertThat(mysteriousHobbit).isEqualToIgnoringNullFields(frodo); // FAIL
isEqualToComparingFieldByFieldRecursively例子:
public class Person {
public String name;
public double height;
public Home home = new Home();
public Person bestFriend;
// 简洁起见,构造函数和get set方法都省略了
}
public class Home {
public Address address = new Address();
}
public static class Address {
public int number = 1;
}
Person jack = new Person("Jack", 1.80);
jack.home.address.number = 123;
Person jackClone = new Person("Jack", 1.80);
jackClone.home.address.number = 123;
jack.bestFriend = jackClone;
jackClone.bestFriend = jack;
// 直接比较会失败
assertThat(jack).isEqualsTo(jackClone);
// jack and jackClone是递归比较属性的
assertThat(jack).isEqualToComparingFieldByFieldRecursively(jackClone);
jack.height = 1.81;
//因为height属性不相等,所以断言会失败
assertThat(jack).isEqualToComparingFieldByFieldRecursively(jackClone);
//使用usingComparatorForType指定比较类型,这样就可以只比较height了
//由于DoubleComparator允许0.5的误差,所以断言会成功
assertThat(jack).usingComparatorForType(new DoubleComparator(0.5), Double.class)
.isEqualToComparingFieldByFieldRecursively(jackClone);
// 使用 usingComparatorForFields 指定哪些属性比较(支持嵌套属性)
assertThat(jack).usingComparatorForFields(new DoubleComparator(0.5), "height")
.isEqualToComparingFieldByFieldRecursively(jackClone);