날짜시간 검증
LocalDate/LocalDateTime 전용 메서드
| 메서드 |
설명 |
isBefore |
특정 날짜보다 이전인지 확인 |
isAfter |
특정 날짜보다 이후인지 확인 |
isBeforeOrEqualTo |
특정 날짜보다 이전이거나 같은지 확인 |
isAfterOrEqualTo |
특정 날짜보다 이후이거나 같은지 확인 |
isBetween |
두 날짜 사이에 있는지 확인 |
isStrictlyBetween |
두 날짜 사이에 있는지 확인 (경계 제외) |
isToday |
오늘 날짜인지 확인 |
isIn |
특정 년도/월/일인지 확인 |
hasYear |
특정 년도인지 확인 |
hasMonthValue |
특정 월인지 확인 (1-12) |
hasDayOfMonth |
특정 일인지 확인 |
isEqualToIgnoringHours |
시간을 무시하고 날짜만 비교 |
isEqualToIgnoringMinutes |
분 이하를 무시하고 비교 |
isEqualToIgnoringSeconds |
초 이하를 무시하고 비교 |
코드 예제
@Test
void localDateAssertions() {
LocalDate today = LocalDate.now();
LocalDate yesterday = today.minusDays(1);
LocalDate tomorrow = today.plusDays(1);
// 날짜 비교
assertThat(today)
.isToday()
.isAfter(yesterday)
.isBefore(tomorrow)
.isBeforeOrEqualTo(today);
// 범위 검증
assertThat(today)
.isBetween(yesterday, tomorrow)
.isStrictlyBetween(yesterday, tomorrow);
// 특정 날짜 필드 검증
LocalDate specificDate = LocalDate.of(2024, 3, 15);
assertThat(specificDate)
.hasYear(2024)
.hasMonthValue(3)
.hasDayOfMonth(15)
.isIn(2024, 3, 15);
}
@Test
void localDateTimeAssertions() {
LocalDateTime now = LocalDateTime.now();
LocalDateTime oneHourAgo = now.minusHours(1);
LocalDateTime oneHourLater = now.plusHours(1);
// 시간 비교
assertThat(now)
.isAfter(oneHourAgo)
.isBefore(oneHourLater)
.isBetween(oneHourAgo, oneHourLater);
// 특정 시간 필드 검증
LocalDateTime specificTime = LocalDateTime.of(2024, 3, 15, 14, 30, 45);
assertThat(specificTime)
.hasYear(2024)
.hasMonthValue(3)
.hasDayOfMonth(15)
.hasHour(14)
.hasMinute(30)
.hasSecond(45);
}
@Test
void dateTimeIgnoringAssertions() {
LocalDateTime dt1 = LocalDateTime.of(2024, 3, 15, 14, 30, 45);
LocalDateTime dt2 = LocalDateTime.of(2024, 3, 15, 14, 30, 50);
LocalDateTime dt3 = LocalDateTime.of(2024, 3, 15, 14, 35, 45);
LocalDateTime dt4 = LocalDateTime.of(2024, 3, 15, 15, 30, 45);
// 초 무시하고 비교
assertThat(dt1).isEqualToIgnoringSeconds(dt2);
// 분 무시하고 비교
assertThat(dt1).isEqualToIgnoringMinutes(dt3);
// 시간 무시하고 비교
assertThat(dt1).isEqualToIgnoringHours(dt4);
}
@Test
void instantAssertions() {
Instant now = Instant.now();
Instant fiveSecondsAgo = now.minusSeconds(5);
Instant fiveSecondsLater = now.plusSeconds(5);
assertThat(now)
.isAfter(fiveSecondsAgo)
.isBefore(fiveSecondsLater)
.isBetween(fiveSecondsAgo, fiveSecondsLater);
}
예외 검증
예외 검증 메서드
| 메서드 |
설명 |
assertThatThrownBy |
예외 발생을 검증하는 람다 실행 |
assertThatExceptionOfType |
특정 예외 타입 검증 |
assertThatCode |
예외가 발생하지 않음을 검증 |
assertThatNullPointerException |
NullPointerException 전용 단축 메서드 |
assertThatIllegalArgumentException |
IllegalArgumentException 전용 |
assertThatIllegalStateException |
IllegalStateException 전용 |
isInstanceOf |
예외 타입 확인 |
hasMessage |
예외 메시지 정확히 일치하는지 확인 |
hasMessageContaining |
예외 메시지에 특정 문자열 포함 여부 확인 |
hasMessageStartingWith |
예외 메시지 시작 부분 확인 |
hasMessageEndingWith |
예외 메시지 끝 부분 확인 |
hasMessageMatching |
예외 메시지 정규표현식 일치 확인 |
hasCause |
원인 예외가 있는지 확인 |
hasNoCause |
원인 예외가 없는지 확인 |
hasCauseInstanceOf |
원인 예외의 타입 확인 |
hasRootCause |
최상위 원인 예외 확인 |
hasRootCauseInstanceOf |
최상위 원인 예외 타입 확인 |
코드 예제
@Test
void exceptionBasicAssertions() {
// 기본 예외 검증
assertThatThrownBy(() -> {
throw new IllegalArgumentException("Invalid parameter");
})
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Invalid parameter")
.hasNoCause();
// 메시지 포함 검증
assertThatThrownBy(() -> {
throw new RuntimeException("User not found: ID=123");
})
.isInstanceOf(RuntimeException.class)
.hasMessageContaining("not found")
.hasMessageContaining("ID=123")
.hasMessageStartingWith("User")
.hasMessageEndingWith("123");
}
@Test
void exceptionShortcutMethods() {
// 자주 사용하는 예외 타입의 단축 메서드
assertThatNullPointerException()
.isThrownBy(() -> {
String str = null;
str.length();
})
.withMessage("Cannot invoke \"String.length()\" because \"str\" is null");
assertThatIllegalArgumentException()
.isThrownBy(() -> {
throw new IllegalArgumentException("Age must be positive");
})
.withMessageContaining("positive");
assertThatIllegalStateException()
.isThrownBy(() -> {
throw new IllegalStateException("Service not initialized");
});
}
@Test
void exceptionCauseAssertions() {
IOException cause = new IOException("Connection failed");
RuntimeException exception = new RuntimeException("Service error", cause);
assertThatThrownBy(() -> {
throw exception;
})
.isInstanceOf(RuntimeException.class)
.hasMessage("Service error")
.hasCause(cause)
.hasCauseInstanceOf(IOException.class)
.hasRootCause(cause)
.hasRootCauseInstanceOf(IOException.class);
}
@Test
void exceptionChainAssertions() {
SQLException rootCause = new SQLException("Database connection lost");
DataAccessException midCause = new DataAccessException("Query failed", rootCause);
ServiceException topException = new ServiceException("Service unavailable", midCause);
assertThatThrownBy(() -> {
throw topException;
})
.isInstanceOf(ServiceException.class)
.hasCauseInstanceOf(DataAccessException.class)
.hasRootCauseInstanceOf(SQLException.class)
.hasRootCauseMessage("Database connection lost");
}
@Test
void noExceptionAssertions() {
// 예외가 발생하지 않음을 검증
assertThatCode(() -> {
int result = 1 + 1;
}).doesNotThrowAnyException();
// 특정 코드 블록이 안전함을 검증
assertThatCode(() -> {
String str = "test";
str.length();
}).doesNotThrowAnyException();
}
@Test
void customExceptionAssertions() {
class CustomException extends Exception {
private final int errorCode;
CustomException(String message, int errorCode) {
super(message);
this.errorCode = errorCode;
}
public int getErrorCode() {
return errorCode;
}
}
assertThatExceptionOfType(CustomException.class)
.isThrownBy(() -> {
throw new CustomException("Custom error", 500);
})
.satisfies(ex -> {
assertThat(ex.getMessage()).isEqualTo("Custom error");
assertThat(ex.getErrorCode()).isEqualTo(500);
});
}
객체 비교
객체 비교 메서드
| 메서드 |
설명 |
usingRecursiveComparison |
필드 값 재귀적 전수 비교 |
ignoringFields |
특정 필드 제외하고 비교 |
ignoringFieldsOfTypes |
특정 타입 필드 제외하고 비교 |
ignoringAllOverriddenEquals |
equals 메서드를 무시하고 필드 비교 |
comparingOnlyFields |
특정 필드만 비교 |
isEqualToComparingFieldByField |
필드별로 equals 사용하여 비교 (deprecated) |
isEqualToIgnoringGivenFields |
특정 필드 제외하고 비교 (deprecated) |
코드 예제
@Test
void objectRecursiveComparison() {
class Address {
private String city;
private String street;
Address(String city, String street) {
this.city = city;
this.street = street;
}
}
class User {
private Long id;
private String name;
private Address address;
private LocalDateTime createdAt;
User(Long id, String name, Address address, LocalDateTime createdAt) {
this.id = id;
this.name = name;
this.address = address;
this.createdAt = createdAt;
}
}
Address address1 = new Address("Seoul", "Gangnam");
Address address2 = new Address("Seoul", "Gangnam");
User user1 = new User(1L, "John", address1, LocalDateTime.now());
User user2 = new User(1L, "John", address2, LocalDateTime.now());
// 재귀적으로 모든 필드 비교
assertThat(user1)
.usingRecursiveComparison()
.isEqualTo(user2);
}
@Test
void objectIgnoringFields() {
class User {
private Long id;
private String name;
private String email;
private LocalDateTime createdAt;
User(Long id, String name, String email, LocalDateTime createdAt) {
this.id = id;
this.name = name;
this.email = email;
this.createdAt = createdAt;
}
}
User user1 = new User(1L, "John", "john@example.com", LocalDateTime.now());
User user2 = new User(2L, "John", "john@example.com", LocalDateTime.now().plusHours(1));
// id와 createdAt 필드 제외하고 비교
assertThat(user1)
.usingRecursiveComparison()
.ignoringFields("id", "createdAt")
.isEqualTo(user2);
// 특정 타입의 모든 필드 제외
assertThat(user1)
.usingRecursiveComparison()
.ignoringFieldsOfTypes(Long.class, LocalDateTime.class)
.isEqualTo(user2);
}
@Test
void objectComparingOnlyFields() {
class Product {
private Long id;
private String name;
private BigDecimal price;
private String description;
private LocalDateTime createdAt;
Product(Long id, String name, BigDecimal price, String description, LocalDateTime createdAt) {
this.id = id;
this.name = name;
this.price = price;
this.description = description;
this.createdAt = createdAt;
}
}
Product product1 = new Product(1L, "Laptop", new BigDecimal("1000.00"), "Gaming laptop", LocalDateTime.now());
Product product2 = new Product(999L, "Laptop", new BigDecimal("1000.00"), "Different description", LocalDateTime.now().plusDays(1));
// name과 price 필드만 비교
assertThat(product1)
.usingRecursiveComparison()
.comparingOnlyFields("name", "price")
.isEqualTo(product2);
}
@Test
void objectWithCollections() {
class Order {
private Long id;
private List<String> items;
private Map<String, Integer> quantities;
Order(Long id, List<String> items, Map<String, Integer> quantities) {
this.id = id;
this.items = items;
this.quantities = quantities;
}
}
List<String> items1 = Arrays.asList("Apple", "Banana");
List<String> items2 = Arrays.asList("Apple", "Banana");
Map<String, Integer> quantities1 = new HashMap<>();
quantities1.put("Apple", 5);
quantities1.put("Banana", 3);
Map<String, Integer> quantities2 = new HashMap<>();
quantities2.put("Apple", 5);
quantities2.put("Banana", 3);
Order order1 = new Order(1L, items1, quantities1);
Order order2 = new Order(2L, items2, quantities2);
// 컬렉션을 포함한 객체 비교 (id 제외)
assertThat(order1)
.usingRecursiveComparison()
.ignoringFields("id")
.isEqualTo(order2);
}
필터링과 추출
필터링/추출 메서드
| 메서드 |
설명 |
extracting |
객체 리스트에서 특정 필드만 추출 |
extracting (다중 필드) |
여러 필드를 tuple로 추출 |
flatExtracting |
중첩된 컬렉션을 평탄화하여 추출 |
filteredOn |
조건에 맞는 요소만 필터링 |
filteredOn (Predicate) |
Predicate로 필터링 |
filteredOnNull |
Null 값을 가진 요소만 필터링 |
filteredOnAssertions |
복잡한 조건으로 필터링 |
코드 예제
@Test
void extractingSingleField() {
class Member {
private String name;
private String role;
Member(String name, String role) {
this.name = name;
this.role = role;
}
public String getName() { return name; }
public String getRole() { return role; }
}
List<Member> members = Arrays.asList(
new Member("Alice", "ADMIN"),
new Member("Bob", "USER"),
new Member("Charlie", "USER")
);
// 단일 필드 추출
assertThat(members)
.extracting(Member::getName)
.contains("Alice", "Bob", "Charlie");
// 문자열로 필드명 지정
assertThat(members)
.extracting("name")
.containsExactly("Alice", "Bob", "Charlie");
}
@Test
void extractingMultipleFields() {
class User {
private String name;
private int age;
private String email;
User(String name, int age, String email) {
this.name = name;
this.age = age;
this.email = email;
}
public String getName() { return name; }
public int getAge() { return age; }
public String getEmail() { return email; }
}
List<User> users = Arrays.asList(
new User("Alice", 25, "alice@example.com"),
new User("Bob", 30, "bob@example.com")
);
// 여러 필드를 tuple로 추출
assertThat(users)
.extracting(User::getName, User::getAge)
.containsExactly(
tuple("Alice", 25),
tuple("Bob", 30)
);
}
@Test
void flatExtracting() {
class Order {
private String orderId;
private List<String> items;
Order(String orderId, List<String> items) {
this.orderId = orderId;
this.items = items;
}
public List<String> getItems() { return items; }
}
List<Order> orders = Arrays.asList(
new Order("ORD-1", Arrays.asList("Apple", "Banana")),
new Order("ORD-2", Arrays.asList("Cherry", "Date"))
);
// 중첩된 리스트를 평탄화하여 추출
assertThat(orders)
.flatExtracting(Order::getItems)
.containsExactly("Apple", "Banana", "Cherry", "Date");
}
@Test
void filtering() {
class Employee {
private String name;
private int age;
private String department;
Employee(String name, int age, String department) {
this.name = name;
this.age = age;
this.department = department;
}
public String getName() { return name; }
public int getAge() { return age; }
public String getDepartment() { return department; }
}
List<Employee> employees = Arrays.asList(
new Employee("Alice", 25, "IT"),
new Employee("Bob", 30, "HR"),
new Employee("Charlie", 35, "IT"),
new Employee("David", 28, "Sales")
);
// 특정 필드 값으로 필터링
assertThat(employees)
.filteredOn("department", "IT")
.extracting(Employee::getName)
.containsExactly("Alice", "Charlie");
// Predicate로 필터링
assertThat(employees)
.filteredOn(e -> e.getAge() > 30)
.extracting(Employee::getName)
.containsExactly("Charlie");
// 복합 조건 필터링
assertThat(employees)
.filteredOn(e -> e.getAge() > 25 && e.getDepartment().equals("IT"))
.hasSize(1)
.extracting(Employee::getName)
.containsExactly("Charlie");
}
@Test
void filteringWithAssertions() {
class Product {
private String name;
private BigDecimal price;
Product(String name, BigDecimal price) {
this.name = name;
this.price = price;
}
public String getName() { return name; }
public BigDecimal getPrice() { return price; }
}
List<Product> products = Arrays.asList(
new Product("Laptop", new BigDecimal("1000.00")),
new Product("Mouse", new BigDecimal("25.00")),
new Product("Keyboard", new BigDecimal("75.00"))
);
// 복잡한 Assertion 기반 필터링
assertThat(products)
.filteredOnAssertions(p ->
assertThat(p.getPrice()).isGreaterThan(new BigDecimal("50.00"))
)
.extracting(Product::getName)
.containsExactly("Laptop", "Keyboard");
}
@Test
void extractingAndFiltering() {
class Student {
private String name;
private int score;
private String grade;
Student(String name, int score, String grade) {
this.name = name;
this.score = score;
this.grade = grade;
}
public String getName() { return name; }
public int getScore() { return score; }
public String getGrade() { return grade; }
}
List<Student> students = Arrays.asList(
new Student("Alice", 95, "A"),
new Student("Bob", 85, "B"),
new Student("Charlie", 92, "A"),
new Student("David", 78, "C")
);
// 필터링 후 추출
assertThat(students)
.filteredOn("grade", "A")
.extracting(Student::getName, Student::getScore)
.containsExactly(
tuple("Alice", 95),
tuple("Charlie", 92)
);
// 점수 90점 이상인 학생 이름만 추출
assertThat(students)
.filteredOn(s -> s.getScore() >= 90)
.extracting(Student::getName)
.containsExactly("Alice", "Charlie");
}