우아한테크코스 로또 미션을 진행하던 중 두 리스트를 비교하여 테스트할 일이 있었다.
@Test
@DisplayName("수동 로또 번호 생성 검증")
void createLottoTicketsByManual() {
List<List<Integer>> lottoNumbers = List.of(List.of(1, 2, 3, 4, 5, 6),
List.of(7, 8, 9, 10, 11, 12));
List<LottoTicket> lottoTickets = lottoMachine.purchaseLottoTicketsByManual(lottoNumbers);
Iterator<List<Integer>> iterator = lottoNumbers.iterator();
for (LottoTicket lottoTicket : lottoTickets) {
List<Integer> ticketNumbers = lottoTicket.getTicketNumbers();
assertThat(ticketNumbers).isEqualTo(iterator.next());
}
}
직접 지정한 정수로 LottoTickets가 잘 생성되는지 확인해보기 위한 테스트였다. 어떻게 할까 고민하다가 하나는 for-each로 반복하고 하나는 Iterator를 써서 각각이 순회되면서 비교하도록 테스트를 짰다.
그랬더니 이런 피드백을 받았다. AssertJ에서 map 연산도 지원하고 이렇게 가독성 좋게 테스트를 처리할 수 있다고 한다. 이 피드백을 기회 삼아 반복자, 즉 배열이나 컬렉션에서 어떻게 AssertJ를 활용할 수 있는지 공부해 보기로 했다.
contains
@Test
@DisplayName("contains 테스트")
void contains() {
List<Integer> list = List.of(1, 2, 3);
assertThat(list).contains(2); // pass
assertThat(list).contains(2, 3); // pass
assertThat(list).contains(4); // fail
}
contains는 컬렉션 안에 해당 데이터가 존재하는지 확인한다. 순서는 상관없다.
containsOnly
@Test
@DisplayName("containsOnly 테스트")
void containsOnly() {
List<Integer> list = List.of(1, 1, 2, 2, 3);
assertThat(list).containsOnly(1, 2, 3); // pass
assertThat(list).containsOnly(1, 2, 3, 4); // fail
assertThat(list).containsOnly(1, 2); // fail
}
컬렉션에 지정된 값들이 순서에 관계없이 모두 포함되면서 (중복 관계 X) 지정된 값들 외의 값들을 포함되어 있지 않는지 확인한다. 이름에서도 알 수 있듯이 오직 지정된 값만을 포함하고 있어야 하는 것이다.
containsExactly, containsExactlryAnyOrder
@Test
@DisplayName("containsExactly 테스트")
void containsExactly() {
List<Integer> list = List.of(1, 2, 3, 4, 5);
assertThat(list).containsExactly(1, 2, 3, 4, 5); // pass
assertThat(list).containsExactly(5, 4, 3, 2, 1); // fail
assertThat(list).containsExactly(2, 3, 4); // fail
assertThat(list).containsExactlyInAnyOrder(5, 4, 3, 2, 1); // pass
}
containsExactly는 컬렉션의 값이 지정된 값과 정확히 일치해야 한다. 주어진 값을 포함해야 하고, 순서도 맞아야 한다. containsExactlyInAnyOrder를 사용하면 값이 다 포함되는지만 확인하고 순서는 무시한다.
containsSequence, containsSubsequence
@Test
@DisplayName("containsSequence 테스트")
void containsSequence() {
List<Integer> list = List.of(1, 2, 3, 4, 5);
assertThat(list).containsSequence(2, 3, 4); // pass
assertThat(list).containsSequence(3, 2); // fail
assertThat(list).containsSubsequence(1, 3, 4); // pass
}
containsSequence는 컬렉션 요소들의 부분집합을 연속되고 순서에 맞게 가지고 있는지 확인한다. 부분집합을 가지고 있더라도 연속되지 않거나 순서에 맞지 않으면 테스트가 통과되지 않는다. containsSubsequence는 순서에 맞는 부분집합을 가져야 하는 것은 똑같지만 연속되지는 않아도 된다.
containsOnlyOnce
@Test
@DisplayName("containsOnlyOnce 테스트")
void containsOnlyOnce() {
List<Integer> list = List.of(1, 2, 1, 2, 3, 4, 5);
assertThat(list).containsOnlyOnce(5, 4, 3); // pass
assertThat(list).containsOnlyOnce(1, 2); // fail
assertThat(list).containsOnlyOnce(1); // fail
}
컬렉션 안에 값들이 주어진 값을 한 번만 포함하는지 확인한다. 순서는 상관없다. 데이터 (5, 4, 3)의 경우 컬렉션에 한 번만 나타나기에 통과하지만, 1과 2는 2번씩 중복되기 때문에 1과 2를 검증하려 하면 테스트는 실패한다.
containsAnyOf
@Test
@DisplayName("containsAnyOf 테스트")
void containsAnyOf() {
List<Integer> list = List.of(1, 2, 3, 4, 5);
assertThat(list).containsAnyOf(5, 6, 7, 8); // pass
}
컬렉션과 주어진 값이 적어도 하나를 포함하는지 확인한다. 위의 예에선 5가 겹치기에 통과한다.
allSatisfy, anySatisfy, noneSatisfy
@Test
@DisplayName("Satisfy 테스트")
void satisfy() {
List<Integer> list = List.of(1, 2, 3, 4, 5);
assertThat(list).allSatisfy(number -> {
assertThat(number).isLessThan(6);
assertThat(number).isGreaterThan(0);
}); // pass
assertThat(list).anySatisfy(number -> {
assertThat(number).isEqualTo(3);
}); // pass
assertThat(list).noneSatisfy(number -> {
assertThat(number).isZero();
}); // pass
}
satisfy 메서드는 Consumer 인터페이스 타입을 인자로 받아 컬렉션 값들을 검증한다. allSatisfy는 모든 요소들이 assert문을 만족하는지, anySatisfy는 하나라도 만족하는지, noneSatisfy는 모두 만족하지 않는지를 확인한다.
allMatch, anyMatch, noneMatch
@Test
@DisplayName("match 테스트")
void match() {
List<Integer> list = List.of(1, 2, 3, 4, 5);
assertThat(list).allMatch(number -> number < 10); // pass
assertThat(list).anyMatch(number -> number == 1); // pass
assertThat(list).noneMatch(number -> number < 0); // pass
}
match 메서드는 Predicate 인터페이스를 인자로 받아 컬렉션 값들을 검증한다. allMatch는 모든 요소들이 람다식을 만족하는지, anyMatch는 하나라도 만족하는지, noneMatch는 모두 만족하지 않는지를 확인한다.
filteredOn (Predicate)
@Test
@DisplayName("filteredOn 테스트")
void filteredOn() {
List<Integer> list = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
assertThat(list)
.filteredOn(number -> number % 2 == 0)
.containsExactly(2, 4, 6, 8, 10); // pass
}
filteredOn은 Predicate 인터페이스를 인자로 받아 컬렉션 안의 값들 중 특정 값들만 필터링해서 검증할 수 있게 해주는 메서드다. 위의 예에선 짝수인 값들만 필터링했고 정확히 짝수들만 걸러졌다.
밑의 예제에서 사용할 Member 클래스
public class Member {
private String name;
private int age;
private Gender gender;
public Member(String name, int age, Gender gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public Gender getGender() {
return gender;
}
enum Gender {
MAN, WOMAN
}
}
filteredOn (property/filed)
@Test
@DisplayName("filteredOn element property/field 테스트")
void filteredOn_element_property_field() {
Member a = new Member("a", 22, Member.Gender.MAN);
Member b = new Member("b", 20, Member.Gender.WOMAN);
Member c = new Member("c", 21, Member.Gender.MAN);
List<Member> members = List.of(a, b, c);
assertThat(members).filteredOn("gender", Member.Gender.MAN)
.containsExactly(a, c); // pass
assertThat(members).filteredOn("gender", not(Member.Gender.WOMAN))
.containsExactly(a, c); // pass
assertThat(members).filteredOn("name", in("a", "b"))
.containsExactly(a, b); // pass
assertThat(members).filteredOn("age", notIn(20, 21))
.containsExactly(a); // pass
assertThat(members)
.filteredOn("age", 20)
.filteredOn("gender", Member.Gender.MAN)
.isEmpty(); // pass
assertThat(members)
.filteredOnAssertions(member -> {
assertThat(member.getAge()).isLessThan(22);
}).containsExactly(b, c); // pass
}
filteredOn은 검증하려는 컬렉션 요소의 필드 값으로도 필터링이 가능하다. 해당 필드들이 private으로 선언되어 있고 getter를 사용하지 않더라도 값을 확인하여 필터링할 수 있다. filteredOnAssertions를 사용하면 Consumer 인터페이스를 인자로 받기 때문에 내부에서 assert문을 사용하여 필터링할 수도 있다.
map
@Test
@DisplayName("map 테스트")
void map() {
List<Member> members = List.of(
new Member("a", 20, Member.Gender.MAN),
new Member("b", 23, Member.Gender.WOMAN),
new Member("c", 25, Member.Gender.MAN)
);
assertThat(members)
.map(Member::getName)
.containsExactly("a", "b", "c"); // pass
}
다음과 같이 map을 사용하면 원하는 필드 값을 추출해서 확인할 수 있다. 하지만 이는 getter 메서드가 있어야 가능하다.
extracting
@Test
@DisplayName("extracting 테스트")
void extracting() {
List<Member> members = List.of(
new Member("a", 25, Member.Gender.MAN),
new Member("b", 25, Member.Gender.MAN));
assertThat(members).extracting("name")
.contains("a", "b"); // pass
assertThat(members).extracting("age")
.containsOnly(25); // pass
}
filteredOn 때와 마찬가지로 extracting을 사용하면 객체 필드 값으로 추출하여 검증할 수 있다. 이 검증 방법은 getter 같은 접근자가 없어도 추출이 가능하다.
assertJ를 사용하니 강력한 메서드들을 가독성 좋게 작성할 수 있었다.. 위의 메서드들을 조합하여 메서드 체이닝을 적극 활용한다면 정확하고 편리한 테스트 코드가 작성될 것이다.
참고
https://assertj.github.io/doc/#assertj-core-group-assertions
'JAVA' 카테고리의 다른 글
[JAVA] Enum으로 if문 제거하기 (0) | 2022.03.16 |
---|---|
옵저버 패턴(Observer Pattern) 살펴보기 (0) | 2022.03.05 |
[자바] 객체 내부 컬렉션 데이터 보호하기 (0) | 2022.02.26 |
자바 JDK, JRE, JVM 간단 정리 (0) | 2022.02.21 |
자바 스트림 사용 정리 (0) | 2022.02.20 |