Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 157 additions & 0 deletions __tests__/contoller/LottoGameController.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { LOTTO } from "../../src/constants/lottos.js";
import { ERROR_MESSAGE } from "../../src/constants/message.js";
import LottoGameController from "../../src/controller/LottoGameController.js";
import LottoService from "../../src/controller/LottoService.js";
import Lotto from "../../src/domain/Lotto.js";
import WinningNumbers from "../../src/domain/WinningNumbers.js";
import WinningRankCalculator from "../../src/utils/WinningRankCalculator.js";
import ConsoleView from "../../src/view/ConsoleView.js";

describe("ConsoleView에서 Lotto 사용자 입력에 대한 테스트", () => {
let view;
let lottoController;

beforeEach(() => {
view = new ConsoleView();
lottoController = new LottoGameController(view);
});

test("구입 금액은 특수 문자를 입력할 수 없다.", () => {
expect(() => {
lottoController.amount = "8,000";
}).toThrow(ERROR_MESSAGE.ONLY_NUMBER);
});

test("구입 금액 금액은 숫자를 제외한 문자는 들어갈 수 없다.", () => {
expect(() => {
lottoController.amount = "팔천원";
}).toThrow(ERROR_MESSAGE.ONLY_NUMBER);
});

test("로또 최소 구매 가격은 1000원이다.", () => {
expect(() => {
lottoController.amount = 700;
}).toThrow(ERROR_MESSAGE.MIN_PRICE);
});
test("로또 가격은 1000원 단위로 입력해야 한다.", () => {
expect(() => {
lottoController.amount = 1700;
}).toThrow(ERROR_MESSAGE.AMOUNT_UNIT);
});
});

describe("ConsoleView에서 당첨 번호와 보너스 번호 입력에 대한 테스트", () => {
let view;
let lottoController;

beforeEach(() => {
view = new ConsoleView();
lottoController = new LottoGameController(view);
});
test("당첨 번호는 숫자와 , 만 입력 가능하다.", () => {
expect(() => {
lottoController.winningNumbers = "1&가/";
}).toThrow(ERROR_MESSAGE.LOTTO_INVALID_FORMAT);
});

test("당첨 번호는 6개 이상 입력할 수 없다.", () => {
expect(() => {
lottoController.winningNumbers = "1, 4, 10, 22, 34, 43, 44";
}).toThrow(ERROR_MESSAGE.LOTTO_SIZE);
});

test("당첨 번호는 6개 미만 입력할 수 없다", () => {
expect(() => {
lottoController.winningNumbers = "1, 4, 10, 22, 34";
}).toThrow(ERROR_MESSAGE.LOTTO_SIZE);
});

test("당첨 번호는 최소 1이상이어야 한다.", () => {
expect(() => {
lottoController.winningNumbers = "0, 4, 10, 22, 34, 37";
}).toThrow(ERROR_MESSAGE.LOTTO_RANGE);
});

test("당첨 번호는 45를 초과할 수 없다.", () => {
expect(() => {
lottoController.winningNumbers = "0, 4, 10, 22, 34, 50";
}).toThrow(ERROR_MESSAGE.LOTTO_RANGE);
});

test("6개의 당첨 번호는 중복될 수 없다.", () => {
expect(() => {
lottoController.winningNumbers = "1, 4, 10, 22, 34, 34";
}).toThrow(ERROR_MESSAGE.LOTTO_DUPLICATE);
});

test("보너스 번호는 하나의 숫자를 입력해야 한다.", () => {
expect(() => {
lottoController.bonusNumber = null;
}).toThrow(ERROR_MESSAGE.EMPTY_INPUT);
});

test("보너스 번호는 숫자만 입력해야 한다..", () => {
expect(() => {
lottoController.bonusNumber = "a";
}).toThrow(ERROR_MESSAGE.ONLY_NUMBER);
});

test("보너스 번호는 최소 1이상이어야 한다.", () => {
expect(() => {
lottoController.bonusNumber = "0";
}).toThrow(ERROR_MESSAGE.LOTTO_RANGE);
});

test("보너스 번호는 45를 초과할 수 없다.", () => {
expect(() => {
lottoController.bonusNumber = 50;
}).toThrow(ERROR_MESSAGE.LOTTO_RANGE);
});

test("보너스 번호는 당첨 번호와 중복될 수 없다.", () => {
expect(() => {
lottoController.winningNumbers = "1, 4, 10, 22, 34, 43";
lottoController.bonusNumber = 1;
}).toThrow(ERROR_MESSAGE.BONUS_DUPLICATE);
});
});

describe("로또 번호와 당첨 번호 & 보너스 번호 비효 테스트", () => {
let view;
let lottoController;

beforeEach(() => {
view = new ConsoleView();
lottoController = new LottoGameController(view);
});

test("로또 번호가 당첨 번호 6개와 모두 일치하면 1등이다.", () => {
const winningNumbers = "1, 4, 10, 22, 34, 43";
const bonusNumber = "13";
const lotto = new Lotto([1, 4, 10, 22, 34, 43]);
const winningLotto = new WinningNumbers(winningNumbers, bonusNumber);
const rank = LottoService.calculateSingleLottoRank({ winningLotto, lotto });

expect(rank).toBe(1);
});

test("로또 번호와 당첨 번호가 5개 일치하고 보너스 번호도 일치하면 2등이다.", () => {
const winningNumbers = "1, 4, 10, 22, 34, 43";
const bonusNumber = "13";
const lotto = new Lotto([1, 4, 10, 22, 34, 13]);
const winningLotto = new WinningNumbers(winningNumbers, bonusNumber);
const rank = LottoService.calculateSingleLottoRank({ winningLotto, lotto });

expect(rank).toBe(2);
});

test("로또 번호와 당첨 번호가 5개 일치하고 보너스 번호도 일치하지 않으면 3등이다.", () => {
const winningNumbers = "1, 4, 10, 22, 34, 43";
const bonusNumber = "13";
const lotto = new Lotto([1, 4, 10, 22, 34, 45]);
const winningLotto = new WinningNumbers(winningNumbers, bonusNumber);
const rank = LottoService.calculateSingleLottoRank({ winningLotto, lotto });

expect(rank).toBe(3);
});
});
54 changes: 54 additions & 0 deletions __tests__/contoller/LottoService.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import LottoService from "../../src/controller/LottoService.js";

describe("수익률 계산 테스트", () => {
test("1등 1개 당첨시 수익률을 계산한다", () => {
const winningResult = new Map([
[1, 1],
[2, 0],
[3, 0],
[4, 0],
[5, 0],
]);

const profitRate = LottoService.calculateProfitRate({
winningResult,
amount: "8000",
});

expect(profitRate).toBe(25000000);
});

test("5등 2개 당첨시 수익률을 계산한다", () => {
const winningResult = new Map([
[1, 0],
[2, 0],
[3, 0],
[4, 0],
[5, 2],
]);

const profitRate = LottoService.calculateProfitRate({
winningResult,
amount: "10000",
});

expect(profitRate).toBe(100);
});

test("당첨되지 않으면 수익률은 0이다", () => {
const winningResult = new Map([
[1, 0],
[2, 0],
[3, 0],
[4, 0],
[5, 0],
]);

const profitRate = LottoService.calculateProfitRate({
winningResult,
amount: "5000",
});

expect(profitRate).toBe(0);
});
});
18 changes: 18 additions & 0 deletions __tests__/domain/Lotto.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import Lotto from "../../src/domain/Lotto.js";

describe("Lotto 클래스 테스트", () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

로또에서 요구 사항을 기준으로 부합하지 않은 값이 들어온다면, 예외 처리를 어떻게 할지도 테스트를 해야 하지 않을까 싶습니다! 예를 들어 Lotto 클래스에 46이나 0이라는 값이 인입된다면, 예외 처리를 해줘야 하지 않을까요?

오히려 컨트롤러 단에서는 Lotto에서 나는 오류를 잡아주는 것이 타당하지 않을까요? 왜냐하면, 컨트롤러에서 하는 일이 너무 많습니다. 피드백에서 다룬 내용은 최대한 작은 단위로 나누면서 역할과 책임을 분명하게 나누는 것에 있으니까요 🙂

test("당첨 번호와 일치하는 개수를 반환한다", () => {
const lotto = new Lotto([1, 2, 3, 4, 5, 6]);
const winningNumbers = [1, 2, 3, 10, 11, 12];

const matchCount = lotto.countMatches(winningNumbers);
expect(matchCount).toBe(3);
});

test("보너스 번호 포함 여부를 확인한다", () => {
const lotto = new Lotto([1, 2, 3, 4, 5, 6]);

expect(lotto.contains(1)).toBe(true);
expect(lotto.contains(10)).toBe(false);
});
});
27 changes: 27 additions & 0 deletions __tests__/domain/LottoGenerator.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { LOTTO } from "../../src/constants/lottos.js";
import LottoGenerator from "../../src/domain/LottoGenerator.js";

describe("LottoGenerator - 구매한 금액 만큼 로또 생성 테스트", () => {
test("로또 1장의 가격은 1,000원이다.", () => {
const price = 3000;
const lottos = LottoGenerator.issueLottoTicket(price / LOTTO.PRICE);
expect(lottos).toHaveLength(3);
});

test("로또 번호는 1~45 사이에서 랜덤으로 생성한다.", () => {
const lotto = LottoGenerator.issueLottoTicket(1)[0];
const lottoNumbers = lotto.lottoNumbers;

lottoNumbers.forEach((number) => {
expect(number).toBeGreaterThanOrEqual(LOTTO.MIN_RANGE);
expect(number).toBeLessThanOrEqual(LOTTO.MAX_RANGE);
});
});

test("로또 번호는 6개 생성한다.", () => {
const lotto = LottoGenerator.issueLottoTicket(1)[0];
const lottoNumbers = lotto.lottoNumbers;

expect(lottoNumbers).toHaveLength(6);
});
});
86 changes: 86 additions & 0 deletions docs/REQUIREMENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Step1

## View

- [x] 사용자는 금액을 입력한다.
- [x] 입력받는 금액은 숫자만 허용한다. ex) 8000
- _validation_ 금액이 양수인지? (음수, 빈값 검사)
- [x] 금액에 따른 구매 개수를 보여준다. ex) "8개를 구매했습니다."
- [x] 사용자는 당첨 번호 6개를 입력한다.
- _validation_ 입력받은 글자가 숫자와 쉼표인지?
- [x] 사용자는 보너스 번호 1개를 입력한다.
- _validation_ 입력받은 글자가 숫자인지?
- [ ] 당첨 통계를 출력한다.
- [ ] 수익률을 출력한다.

## Domain

- [x] 입력받은 숫자는 쉼표(,)로 구분한다.
- [x] 로또 1장의 가격은 1,000원이다.
- [x] 로또 번호는 1~45 사이에서 랜덤으로 생성한다.
- [x] 로또 번호는 6개 생성한다.

- _validation_ 중복 숫자 금지

- [x] 사용자가 입력한 로또 번호와 보너스 번호를 기준으로 당첨 순위를 매긴다.
- 1등: 6개 번호 일치 / 2,000,000,000원
- 2등: 5개 번호 + 보너스 번호 일치 / 30,000,000원
- 3등: 5개 번호 일치 / 1,500,000원
- 4등: 4개 번호 일치 / 50,000원
- 5등: 3개 번호 일치 / 5,000원
- [x] 당첨된 금액을 기준으로 수익률을 계산한다.

## Controller

- [x] 로또 구입 금액을 입력하면 구입 금액에 해당하는 만큼 로또를 발행해야 한다.
- [x] 사용자가 구매한 로또 번호와 당첨 번호를 비교해야 한다.

## 실행 결과 예시

```
> 구입금액을 입력해 주세요.8000
8개를 구매했습니다.
[8, 21, 23, 41, 42, 43]
[3, 5, 11, 16, 32, 38]
[7, 11, 16, 35, 36, 44]
[1, 8, 11, 31, 41, 42]
[13, 14, 16, 38, 42, 45]
[7, 11, 30, 40, 42, 43]
[2, 13, 22, 32, 38, 45]
[1, 3, 5, 14, 22, 45]

> 당첨 번호를 입력해 주세요. 1,2,3,4,5,6

> 보너스 번호를 입력해 주세요. 7

당첨 통계
--------------------
3개 일치 (5,000원) - 1개
4개 일치 (50,000원) - 0개
5개 일치 (1,500,000원) - 0개
5개 일치, 보너스 볼 일치 (30,000,000원) - 0개
6개 일치 (2,000,000,000원) - 0개
총 수익률은 62.5%입니다.
```

## ✅ 프로그래밍 요구 사항

**예측 가능하고, 실수를 방지할 수 있는 코드를 작성하기 위해 노력한다.**

- 변수 선언시 const 만 사용한다.
- 함수(또는 메서드)의 들여쓰기 depth는 1단계까지만 허용한다.
- 함수의 매개변수는 2개 이하여야 한다.
- 함수에서 부수 효과를 분리하고, 가능한 순수 함수를 많이 활용한다.

**테스트하기 쉬운 코드에 대해 고민하고, 문제를 작은 단위로 쪼개서 접근하는 방식을 연습한다.**

- 모든 기능을 TDD로 구현하는 것을 시도하여, 테스트 할 수 있는 도메인 로직에 대해서는 모두 단위 테스트가 존재해야 한다. (단, UI 로직은 제외)

#### 1단계

**모듈화와 객체 간에 로직을 재사용하는 방법에 대해 고민한다.**

- 로또 번호와 당첨 로또 번호의 유효성 검사시 발생하는 중복 코드를 제거해야 한다.
- 클래스(또는 객체)를 사용하는 경우, 프로퍼티를 외부에서 직접 꺼내지 않는다. 객체에 메시지를 보내도록 한다.
- getter를 금지하는 것이 아니라 말 그대로 프로퍼티 자체를 그대로 꺼내서 객체 바깥에서 직접 조작하는 등의 작업을 지양하자는 의미입니다 :) 객체 내부에서 알아서 할 수 있는 일은 객체가 스스로 할 수 있게 맡겨주세요.
- 클래스를 사용하는 경우, 3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
"build": "vite build",
"preview": "vite preview",
"test": "jest --no-cache",
"start-step1": "node src/step1-index.js",
"start-step2": "vite"
"step1": "node src/step1-index.js",
"step2": "vite"
},
"devDependencies": {
"@babel/cli": "^7.26.4",
Expand Down
16 changes: 16 additions & 0 deletions src/constants/lottos.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export const LOTTO = {
PRICE: 1000,
MIN_RANGE: 1,
MAX_RANGE: 45,
SIZE: 6,
SECOND_PRIZE_MATCH_COUNT: 5,
MIN_PRIZE_MATCH_COUNT: 3,
};

export const PRIZE_INFO = {
1: { matchCount: 6, bonus: false, prize: 2000000000 },
2: { matchCount: 5, bonus: true, prize: 30000000 },
3: { matchCount: 5, bonus: false, prize: 1500000 },
4: { matchCount: 4, bonus: false, prize: 50000 },
5: { matchCount: 3, bonus: false, prize: 5000 },
};
11 changes: 11 additions & 0 deletions src/constants/message.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export const ERROR_MESSAGE = {
ONLY_NUMBER: "숫자만 입력 가능합니다.",
MIN_PRICE: "로또 최소 구입 금액은 1000원 입니다.",
AMOUNT_UNIT: "로또 구입 금액은 1000원단위로 입력해야 합니다.",
LOTTO_RANGE: "번호는 1 ~ 45까지 입력해야 합니다.",
LOTTO_SIZE: "당첨 번호는 6개를 입력해야합니다.(쉼표로 구분)",
EMPTY_INPUT: "아무것도 입력하지 않았습니다.",
BONUS_DUPLICATE: "보너스 번호가 당첨 번호와 중복됩니다.",
LOTTO_DUPLICATE: "6개의 당첨 번호 중, 중복되는 숫자가 있습니다.",
LOTTO_INVALID_FORMAT: "당첨 번호는 숫자와 , 만 입력 가능합니다.",
};
Loading