날짜시간 검증

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");
}