diff --git a/README.md b/README.md index 91ffab6bb2..bc6eeef8c3 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,21 @@ src/main/java/lotto/ ### 1. 도메인 객체 구현 -#### 1.1 Lotto 객체 구현 +#### 1.2 Rank 열거형 구현 + +**정상 케이스:** +- [ ] 일치 개수와 보너스 일치 여부로 등수를 판별한다 +- [ ] 각 등수별 상금을 반환한다 +- [ ] 5등: 3개 일치 / 5,000원 +- [ ] 4등: 4개 일치 / 50,000원 +- [ ] 3등: 5개 일치 / 1,500,000원 +- [ ] 2등: 5개 + 보너스 일치 / 30,000,000원 +- [ ] 1등: 6개 일치 / 2,000,000,000원 +- [ ] 당첨되지 않으면 NONE 반환 + +**커밋:** `feat(domain): Rank 열거형 구현` + +#### 1.2 Lotto 객체 구현 **정상 케이스:** - [ ] 6개의 로또 번호를 저장한다 @@ -123,20 +137,6 @@ src/main/java/lotto/ **커밋:** `feat(domain): Lotto 객체 기본 기능 구현` -#### 1.2 Rank 열거형 구현 - -**정상 케이스:** -- [ ] 일치 개수와 보너스 일치 여부로 등수를 판별한다 -- [ ] 각 등수별 상금을 반환한다 -- [ ] 5등: 3개 일치 / 5,000원 -- [ ] 4등: 4개 일치 / 50,000원 -- [ ] 3등: 5개 일치 / 1,500,000원 -- [ ] 2등: 5개 + 보너스 일치 / 30,000,000원 -- [ ] 1등: 6개 일치 / 2,000,000,000원 -- [ ] 당첨되지 않으면 NONE 반환 - -**커밋:** `feat(domain): Rank 열거형 구현` - #### 1.3 WinningLotto 객체 구현 **정상 케이스:** diff --git a/src/main/java/lotto/domain/Lotto.java b/src/main/java/lotto/domain/Lotto.java new file mode 100644 index 0000000000..2d5dc54560 --- /dev/null +++ b/src/main/java/lotto/domain/Lotto.java @@ -0,0 +1,68 @@ +package lotto.domain; + +import java.util.HashSet; +import java.util.List; +import java.util.stream.Collectors; + +public class Lotto { + private static final int LOTTO_NUMBER_COUNT = 6; + private static final int MIN_LOTTO_NUMBER = 1; + private static final int MAX_LOTTO_NUMBER = 45; + + private final List numbers; + + public Lotto(List numbers) { + validate(numbers); + this.numbers = numbers.stream() + .sorted() + .collect(Collectors.toList()); + } + + private void validate(List numbers) { + validateNotNull(numbers); + validateSize(numbers); + validateDuplicate(numbers); + validateRange(numbers); + } + + private void validateNotNull(List numbers) { + if (numbers == null) { + throw new IllegalArgumentException("[ERROR] 로또 번호를 입력해주세요."); + } + } + + private void validateSize(List numbers) { + if (numbers.size() != LOTTO_NUMBER_COUNT) { + throw new IllegalArgumentException("[ERROR] 로또 번호는 6개여야 합니다."); + } + } + + private void validateDuplicate(List numbers) { + if (numbers.size() != new HashSet<>(numbers).size()) { + throw new IllegalArgumentException("[ERROR] 로또 번호는 중복될 수 없습니다."); + } + } + + private void validateRange(List numbers) { + boolean isOutOfRange = numbers.stream() + .anyMatch(number -> number < MIN_LOTTO_NUMBER || number > MAX_LOTTO_NUMBER); + + if (isOutOfRange) { + throw new IllegalArgumentException("[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다."); + } + } + + public List getNumbers() { + return numbers; + } + + public int countMatches(List winningNumbers) { + return (int) numbers.stream() + .filter(winningNumbers::contains) + .count(); + } + + public boolean contains(int number) { + return numbers.contains(number); + } +} \ No newline at end of file diff --git a/src/main/java/lotto/domain/WinningLotto.java b/src/main/java/lotto/domain/WinningLotto.java new file mode 100644 index 0000000000..c38ccaf75b --- /dev/null +++ b/src/main/java/lotto/domain/WinningLotto.java @@ -0,0 +1,56 @@ +package lotto.domain; + +import java.util.List; +import java.util.Objects; + +public class WinningLotto { + private static final int MIN_LOTTO_NUMBER = 1; + private static final int MAX_LOTTO_NUMBER = 45; + private final Lotto winningNumbers; + private final int bonusNumber; + + public WinningLotto(Lotto winningNumbers, int bonusNumber) { + validate(winningNumbers, bonusNumber); + this.winningNumbers = winningNumbers; + this.bonusNumber = bonusNumber; + } + + private void validate(Lotto winningNumbers, int bonusNumber) { + validateNotNull(winningNumbers); + validateBonusNumberRange(bonusNumber); + validateDuplicateWithWinningNumbers(winningNumbers, bonusNumber); + } + + private void validateNotNull(Lotto winningNumbers) { + if (Objects.isNull(winningNumbers)) { + throw new IllegalArgumentException("[ERROR] 당첨 번호를 입력해주세요."); + } + } + + private void validateBonusNumberRange(int bonusNumber) { + if (bonusNumber < MIN_LOTTO_NUMBER || bonusNumber > MAX_LOTTO_NUMBER) { + throw new IllegalArgumentException("[ERROR] 보너스 번호는 1부터 45 사이의 숫자여야 합니다."); + } + } + + private void validateDuplicateWithWinningNumbers(Lotto winningNumbers, int bonusNumber) { + if (winningNumbers.contains(bonusNumber)) { + throw new IllegalArgumentException("[ERROR] 보너스 번호는 당첨 번호와 중복될 수 없습니다."); + } + } + + /** + * 구매한 로또와 당첨 번호를 비교하여 등수를 반환합니다. + */ + public Rank match(Lotto userLotto) { + // 1. 일치하는 번호 개수 계산 + List numbers = winningNumbers.getNumbers(); + int matchCount = userLotto.countMatches(numbers); + + // 2. 보너스 번호 일치 여부 확인 + boolean matchBonus = userLotto.contains(bonusNumber); + + // 3. Rank Enum을 통해 등수 판별 + return Rank.valueOf(matchCount, matchBonus); + } +} \ No newline at end of file diff --git a/src/test/java/lotto/LottoTest.java b/src/test/java/lotto/LottoTest.java deleted file mode 100644 index 309f4e50ae..0000000000 --- a/src/test/java/lotto/LottoTest.java +++ /dev/null @@ -1,25 +0,0 @@ -package lotto; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -class LottoTest { - @Test - void 로또_번호의_개수가_6개가_넘어가면_예외가_발생한다() { - assertThatThrownBy(() -> new Lotto(List.of(1, 2, 3, 4, 5, 6, 7))) - .isInstanceOf(IllegalArgumentException.class); - } - - @DisplayName("로또 번호에 중복된 숫자가 있으면 예외가 발생한다.") - @Test - void 로또_번호에_중복된_숫자가_있으면_예외가_발생한다() { - assertThatThrownBy(() -> new Lotto(List.of(1, 2, 3, 4, 5, 5))) - .isInstanceOf(IllegalArgumentException.class); - } - - // TODO: 추가 기능 구현에 따른 테스트 코드 작성 -} diff --git a/src/test/java/lotto/domain/LottoTest.java b/src/test/java/lotto/domain/LottoTest.java new file mode 100644 index 0000000000..b13319038a --- /dev/null +++ b/src/test/java/lotto/domain/LottoTest.java @@ -0,0 +1,147 @@ +package lotto.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class LottoTest { + + @DisplayName("정상1: 로또 번호 6개를 저장한다") + @Test + void test1() { + List numbers = List.of(1, 2, 3, 4, 5, 6); + + Lotto lotto = new Lotto(numbers); // 전체 경로 제거 + assertThat(lotto.getNumbers()).hasSize(6); + assertThat(lotto.getNumbers()).containsExactly(1, 2, 3, 4, 5, 6); + } + + @DisplayName("정상2: 로또 번호는 1~45 범위의 중복되지 않는 숫자이다") + @Test + void test2() { + List numbers = List.of(1, 15, 23, 32, 41, 45); + + Lotto lotto = new Lotto(numbers); + assertThat(lotto.getNumbers()).allMatch(num -> num >= 1 && num <= 45); + } + + @DisplayName("정상3: 로또 번호를 오름차순으로 정렬하여 반환한다") + @Test + void test3() { + List numbers = List.of(45, 1, 32, 15, 23, 8); + + Lotto lotto = new Lotto(numbers); + assertThat(lotto.getNumbers()).containsExactly(1, 8, 15, 23, 32, 45); + } + + @DisplayName("정상4: 주어진 번호 리스트와 일치하는 개수를 반환한다") + @Test + void test4() { + Lotto lotto = new Lotto(List.of(1, 2, 3, 4, 5, 6)); + List winningNumbers = List.of(1, 2, 3, 7, 8, 9); + + int matchCount = lotto.countMatches(winningNumbers); + assertThat(matchCount).isEqualTo(3); + } + + @DisplayName("정상5: 일치하는 번호가 없을 때 0을 반환한다") + @Test + void test5() { + Lotto lotto = new Lotto(List.of(1, 2, 3, 4, 5, 6)); + List winningNumbers = List.of(7, 8, 9, 10, 11, 12); + + int matchCount = lotto.countMatches(winningNumbers); + assertThat(matchCount).isEqualTo(0); + } + + @DisplayName("정상6: 모든 번호가 일치할 때 6을 반환한다") + @Test + void test6() { + Lotto lotto = new Lotto(List.of(1, 2, 3, 4, 5, 6)); + List winningNumbers = List.of(1, 2, 3, 4, 5, 6); + + int matchCount = lotto.countMatches(winningNumbers); + assertThat(matchCount).isEqualTo(6); + } + + @DisplayName("정상7: 특정 번호 포함 여부를 확인한다 - 포함하는 경우") + @Test + void test7() { + Lotto lotto = new Lotto(List.of(1, 2, 3, 4, 5, 6)); + + assertThat(lotto.contains(1)).isTrue(); + assertThat(lotto.contains(3)).isTrue(); + assertThat(lotto.contains(6)).isTrue(); + } + + @DisplayName("정상8: 특정 번호 포함 여부를 확인한다 - 포함하지 않는 경우") + @Test + void test8() { + Lotto lotto = new Lotto(List.of(1, 2, 3, 4, 5, 6)); + + assertThat(lotto.contains(7)).isFalse(); + assertThat(lotto.contains(45)).isFalse(); + } + + @DisplayName("예외1: 로또 번호가 null이면 예외가 발생한다") + @Test + void test9() { + assertThatThrownBy(() -> new Lotto(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("[ERROR]"); + } + + @DisplayName("예외2: 로또 번호의 개수가 6개가 넘어가면 예외가 발생한다") + @Test + void test10() { + assertThatThrownBy(() -> new Lotto(List.of(1,2,3,4,5,6,7))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 로또 번호는 6개여야 합니다."); + } + + @DisplayName("예외3: 로또 번호의 개수가 6개 미만이면 예외가 발생한다") + @Test + void test11() { + assertThatThrownBy(() -> new Lotto(List.of(1,2,3,4,5))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 로또 번호는 6개여야 합니다."); + } + + @Tag("error") + @DisplayName("예외4: 로또 번호에 중복된 숫자가 있으면 예외가 발생한다") + @Test + void test12() { + assertThatThrownBy(() -> new Lotto(List.of(1,2,3,4,5,5))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 로또 번호는 중복될 수 없습니다."); + } + + @DisplayName("예외5: 로또 번호가 1보다 작으면 예외가 발생한다") + @Test + void test13() { + assertThatThrownBy(() -> new Lotto(List.of(0,1,2,3,4,5))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다."); + } + + @DisplayName("예외6: 로또 번호가 45보다 크면 예외가 발생한다") + @Test + void test14() { + assertThatThrownBy(() -> new Lotto(List.of(1,2,3,4,5,46))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다."); + } + + @DisplayName("예외7: 로또 번호가 음수이면 예외가 발생한다") + @Test + void test15() { + assertThatThrownBy(() -> new Lotto(List.of(-1,1,2,3,4,5))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다."); + } +} diff --git a/src/test/java/lotto/domain/RankTest.java b/src/test/java/lotto/domain/RankTest.java index 29e91688fe..3979fb8e3f 100644 --- a/src/test/java/lotto/domain/RankTest.java +++ b/src/test/java/lotto/domain/RankTest.java @@ -11,7 +11,7 @@ class RankTest { @DisplayName("1: 6개 일치하면 1등") @Test - void test1MatchSixNumbers() { + void test1() { Rank rank = Rank.valueOf(6, false); assertThat(rank).isEqualTo(Rank.FIRST); @@ -20,7 +20,7 @@ void test1MatchSixNumbers() { @DisplayName("2: 5개 일치하고 보너스볼 일치하면 2등") @Test - void test2MatchFiveNumbersWithBonus() { + void test2() { Rank rank = Rank.valueOf(5, true); assertThat(rank).isEqualTo(Rank.SECOND); @@ -29,7 +29,7 @@ void test2MatchFiveNumbersWithBonus() { @DisplayName("3: 5개 일치하고 보너스볼 불일치하면 3등") @Test - void test3MatchFiveNumbersWithoutBonus() { + void test3() { Rank rank = Rank.valueOf(5, false); assertThat(rank).isEqualTo(Rank.THIRD); @@ -38,7 +38,7 @@ void test3MatchFiveNumbersWithoutBonus() { @DisplayName("4: 4개 일치하면 4등") @Test - void test4MatchFourNumbers() { + void test4() { Rank rank = Rank.valueOf(4, false); assertThat(rank).isEqualTo(Rank.FOURTH); @@ -47,7 +47,7 @@ void test4MatchFourNumbers() { @DisplayName("5: 3개 일치하면 5등") @Test - void test5MatchThreeNumbers() { + void test5() { Rank rank = Rank.valueOf(3, false); assertThat(rank).isEqualTo(Rank.FIFTH); @@ -57,7 +57,7 @@ void test5MatchThreeNumbers() { @DisplayName("6: 2개 이하 일치하면 NONE") @ParameterizedTest @CsvSource({"0, false", "1, false", "2, false"}) - void test6MatchLessThanThree(int matchCount, boolean matchBonus) { + void test6(int matchCount, boolean matchBonus) { Rank rank = Rank.valueOf(matchCount, matchBonus); assertThat(rank).isEqualTo(Rank.NONE); @@ -66,7 +66,7 @@ void test6MatchLessThanThree(int matchCount, boolean matchBonus) { @DisplayName("7: 당첨 여부 확인") @Test - void test7IsWinning() { + void test7() { assertThat(Rank.FIRST.isWinning()).isTrue(); assertThat(Rank.SECOND.isWinning()).isTrue(); assertThat(Rank.THIRD.isWinning()).isTrue(); diff --git a/src/test/java/lotto/domain/WinningLottoTest.java b/src/test/java/lotto/domain/WinningLottoTest.java new file mode 100644 index 0000000000..ae043781ae --- /dev/null +++ b/src/test/java/lotto/domain/WinningLottoTest.java @@ -0,0 +1,134 @@ +package lotto.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class WinningLottoTest { + + private final Lotto validWinningLottoNumbers = new Lotto(List.of(1, 2, 3, 4, 5, 6)); + + @DisplayName("정상1: 당첨 번호 6개와 보너스 번호 1개를 저장한다") + @Test + void test1() { + // Given + int bonusNumber = 7; + + // When + WinningLotto winningLotto = new WinningLotto(validWinningLottoNumbers, bonusNumber); + + // Then (내부 필드에 대한 직접적인 getter는 없지만, 검증 로직 통과 여부로 확인) + assertThat(winningLotto).isNotNull(); + } + + @DisplayName("정상2: 로또와 비교하여 등수를 판별한다 - 1등") + @Test + void test2() { + // Given: 1, 2, 3, 4, 5, 6 (당첨 번호), 7 (보너스) + WinningLotto winningLotto = new WinningLotto(validWinningLottoNumbers, 7); + // When: 1, 2, 3, 4, 5, 6 (6개 일치) + Lotto userLotto = new Lotto(List.of(1, 2, 3, 4, 5, 6)); + + // Then + assertThat(winningLotto.match(userLotto)).isEqualTo(Rank.FIRST); + } + + @DisplayName("정상3: 로또와 비교하여 등수를 판별한다 - 2등") + @Test + void test3() { + // Given: 1, 2, 3, 4, 5, 6 (당첨 번호), 7 (보너스) + WinningLotto winningLotto = new WinningLotto(validWinningLottoNumbers, 7); + // When: 1, 2, 3, 4, 5, 7 (5개 일치, 보너스 일치) + Lotto userLotto = new Lotto(List.of(1, 2, 3, 4, 5, 7)); + + // Then + assertThat(winningLotto.match(userLotto)).isEqualTo(Rank.SECOND); + } + + @DisplayName("정상4: 로또와 비교하여 등수를 판별한다 - 3등") + @Test + void test4() { + // Given: 1, 2, 3, 4, 5, 6 (당첨 번호), 7 (보너스) + WinningLotto winningLotto = new WinningLotto(validWinningLottoNumbers, 7); + // When: 1, 2, 3, 4, 5, 8 (5개 일치, 보너스 불일치) + Lotto userLotto = new Lotto(List.of(1, 2, 3, 4, 5, 8)); + + // Then + assertThat(winningLotto.match(userLotto)).isEqualTo(Rank.THIRD); + } + + @DisplayName("정상5: 로또와 비교하여 등수를 판별한다 - 5등") + @Test + void test5() { + // Given: 1, 2, 3, 4, 5, 6 (당첨 번호), 7 (보너스) + WinningLotto winningLotto = new WinningLotto(validWinningLottoNumbers, 7); + // When: 1, 2, 3, 8, 9, 10 (3개 일치) + Lotto userLotto = new Lotto(List.of(1, 2, 3, 8, 9, 10)); + + // Then + assertThat(winningLotto.match(userLotto)).isEqualTo(Rank.FIFTH); + } + + @DisplayName("정상6: 로또와 비교하여 등수를 판별한다 - 꽝 (NONE)") + @ParameterizedTest(name = "{0}개 일치 시 NONE") + @CsvSource({"0", "1", "2"}) + void test6(int matchCount) { + // Given: 1, 2, 3, 4, 5, 6 (당첨 번호), 7 (보너스) + WinningLotto winningLotto = new WinningLotto(validWinningLottoNumbers, 7); + + // When: matchCount에 맞게 로또 생성 (보너스 번호와도 불일치하게) + List lottoNumbers = List.of(1, 2, 3, 4, 5, 6).subList(0, matchCount); + lottoNumbers = new java.util.ArrayList<>(lottoNumbers); + // 나머지 번호를 7, 8, ...로 채워서 보너스 번호(7)와도 겹치지 않게 함 + for (int i = 0; lottoNumbers.size() < 6; i++) { + lottoNumbers.add(8 + i); + } + Lotto userLotto = new Lotto(lottoNumbers); + + // Then + assertThat(winningLotto.match(userLotto)).isEqualTo(Rank.NONE); + } + + // 예외 테스트 + @DisplayName("예외1: 당첨 번호가 null이면 IllegalArgumentException 발생") + @Test + void test7() { + assertThatThrownBy(() -> new WinningLotto(null, 7)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("[ERROR] 당첨 번호를 입력해주세요."); + } + + @DisplayName("예외2: 보너스 번호가 당첨 번호와 중복되면 IllegalArgumentException 발생") + @Test + void test8() { + // 당첨 번호: 1, 2, 3, 4, 5, 6 + int bonusNumber = 6; // 중복 + assertThatThrownBy(() -> new WinningLotto(validWinningLottoNumbers, bonusNumber)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("[ERROR] 보너스 번호는 당첨 번호와 중복될 수 없습니다."); + } + + @DisplayName("예외3: 보너스 번호가 1보다 작으면 IllegalArgumentException 발생") + @Test + void test9() { + int bonusNumber = 0; + assertThatThrownBy(() -> new WinningLotto(validWinningLottoNumbers, bonusNumber)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("[ERROR] 보너스 번호는 1부터 45 사이의 숫자여야 합니다."); + } + + @DisplayName("예외4: 보너스 번호가 45보다 크면 IllegalArgumentException 발생") + @Test + void test10() { + int bonusNumber = 46; + assertThatThrownBy(() -> new WinningLotto(validWinningLottoNumbers, bonusNumber)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("[ERROR] 보너스 번호는 1부터 45 사이의 숫자여야 합니다."); + } +} \ No newline at end of file