diff --git a/README.md b/README.md index b168a180..8d9c2a37 100644 --- a/README.md +++ b/README.md @@ -1 +1,139 @@ -# javascript-planetlotto-precourse +# 요구 사항 정리 내역 (13시 15분 작성) + +## 예제 + +```text +구입금액을 입력해 주세요. +1000 + +2개를 구매했습니다. +[8, 11, 13, 21, 22] +[1, 3, 6, 14, 22] + +당첨 번호를 입력해 주세요. +1, 2, 3, 4, 5 + +보너스 번호 번호를 입력해 주세요. +6 + +당첨 통계 +--- +5개 일치 (100,000,000원) - 0개 +4개 일치, 보너스 번호 일치 (10,000,000원) - 0개 +4개 일치 (1,500,000원) - 0개 +3개 일치, 보너스 번호 일치 (500,000원) - 0개 +2개 일치, 보너스 번호 일치 (5,000원) - 1개 +0개 일치 (0원) - 1개 +``` + +## 기능 요구 사항 + +- TDD 등 개발 방식이 드라나게 +- `InputView` `OutputView`의 기존 메서드 수정 X + +### 게임 로직 + +- 로또 + - 번호는 1~30 + - 중복되진 않는 5개의 숫자를 뽑음 + - 당첨 번호는 중복되지 않는 5개의 숫자 + 보너스 번호 1개 + - 개당 500원 +- 당첨 + - 1등 : 5개 번호 / 100_000_000 + - 2등 : 4개 번호 + 보너스 번호 / 10_000_000 + - 3등 : 4개 번호 / 1_500_000 + - 4등 : 3개 번호 + 보너스 / 500_000 + - 5등 : 2개 번호 + 보너스 / 5_000 +- 예외 처리 + - `[ERROR]` 메시지와 함께 에러 발생 + - 해당 지점부터 다시 입력 + +## 기능 요구 사항 + +### 입력 + +- 1차 입력 + - 구입 금액 +- 2차 입력 + - 당첨 번호 + - `, 쉼표`로 구분 +- 3차 입력 + - 보너스 번호 + +> 예외 발생시 해당 지점부터 재입력 + +### 출력 + +- 1차 출력 + - 로또 구매 갯수 + - 로또 번호 + - 숫자를 오름차순으로 정렬 + - 배열로 보여줌 +- 2차 출력 + - 당첨 통계 + +## 도전 과제 + +### 리펙토링 + +- 작동은 유지 +- 코드 품질을 높이는 방향 +- 기능 확장에 용이한 구조 + +# 개인 도전 목표 (15시 11분 작성) + +## 현재 상황 + +15시 현재 우선 테스트 코드를 모두 통과하는 기능을 구현했습니다. + +그러나 현재 기능 추가에 용이한 구조가 아니라고 생각합니다. + +### 고려되는 부분 + +- `만약 로또 금액이 500원이 아니라 1000원으로 바뀌어도 괜찮은가` +- `사용자가 로또 금액 단위의 금액을 입력하지 않으면 예외처리가 과연 맞는가` +- `로또 숫자가 0~100까지 가능하게 바뀔 수 있는가` +- `로또가 5개의 숫자가 아니라 6개의 숫자를 뽑아도 괜찮은가` + +여기에 더해서 현재 코드가 그 동안 지켜왔던 규칙을 지키고 있는지도 확인 해야 한다. + +- `함수의 길이가 15줄을 넘어가는가` +- `들여쓰기 depth가 3을 넘어가는가` +- `각각의 모듈의 테스트 코드를 잘 작성했는가` +- `테스트 코드에서 잡지 못하는 엣지 케이스가 있는가` + +## 도전 목표 + +### 목표 1 + +로또 조건이 변경되도 실행될 대응이 가능한 코드를 만들자 + +`constants.js`의 수정을 통해 변경에 용이한 구조를 만들자 + +- 로또 숫자 범위 변경 가능 +- 로또 가격 변경 가능 +- 사용자가 로또 범위 외의 돈을 입력했을 때 정상 작동 로직 추가 + +### 목표 2 + +그 동안 지켜왔던 JavaScript 코드 규칙을 지켰는지 확인하자 + +- 함수 길이 +- `if/else` 지양 +- 들여쓰기 depth 3 이하 + +### 목표 3 + +- 각각의 비즈니스 로직에 해당하는 테스트 코드를 잘 작성했는지 확인 +- 테스트 하지 못한 엣지 케이스 확인 + +### 완료 항목 (16:30 작성) + +- `Random` 모듈이 문제가 생길 경우 예외 처리 진행 +- 로또 관련 상수값 분리 완료 +- 로또 단위가 아닌 금액을 입력해도 가능하도록 수정 +- 부족한 테스트 코드 추가 + - `App.test.js` - 정상 입력 엔드 투 엔드 테스트 추가 + - `User.test.js` - 부족한 금액의 경우 테스트 추가 + - `Lotto.test.js` - `Random`모듈이 문제가 생겼을 경우를 위한 테스트 추가 +- `if/else` 문 제거 \ No newline at end of file diff --git a/__tests__/ApplicationTest.js b/__tests__/ApplicationTest.js index 5b25d52d..504b3ea5 100644 --- a/__tests__/ApplicationTest.js +++ b/__tests__/ApplicationTest.js @@ -1,46 +1,5 @@ import App from "../src/App.js"; -import { MissionUtils } from "@woowacourse/mission-utils"; - -const mockQuestions = (inputs) => { - MissionUtils.Console.readLineAsync = jest.fn(); - - MissionUtils.Console.readLineAsync.mockImplementation(() => { - const input = inputs.shift(); - - return Promise.resolve(input); - }); -}; - -const mockRandoms = (numbers) => { - MissionUtils.Random.pickUniqueNumbersInRange = jest.fn(); - numbers.reduce((acc, number) => { - return acc.mockReturnValueOnce(number); - }, MissionUtils.Random.pickUniqueNumbersInRange); -}; - -const getLogSpy = () => { - const logSpy = jest.spyOn(MissionUtils.Console, "print"); - logSpy.mockClear(); - return logSpy; -}; - -const runException = async (input) => { - // given - const logSpy = getLogSpy(); - - const RANDOM_NUMBERS_TO_END = [1, 2, 3, 4, 5]; - const INPUT_NUMBERS_TO_END = ["500", "1,2,3,4,5", "6"]; - - mockRandoms([RANDOM_NUMBERS_TO_END]); - mockQuestions([input, ...INPUT_NUMBERS_TO_END]); - - // when - const app = new App(); - await app.run(); - - // then - expect(logSpy).toHaveBeenCalledWith(expect.stringContaining("[ERROR]")); -}; +import {getLogSpy, mockQuestions, mockRandoms, runException} from "../testUtils.js"; describe("로또 테스트", () => { beforeEach(() => { diff --git a/doc/checkList.md b/doc/checkList.md new file mode 100644 index 00000000..9adffd05 --- /dev/null +++ b/doc/checkList.md @@ -0,0 +1,5 @@ +## View 관련 추가 조건 + +당첨 결과는 `Map` 자료 구조 이용 + +에러의 경우 `printErrorMessage` 메서드 이용 \ No newline at end of file diff --git a/src/App.js b/src/App.js index 091aa0a5..fab99cda 100644 --- a/src/App.js +++ b/src/App.js @@ -1,5 +1,46 @@ +import {InputView, OutputView} from "./view.js"; +import {User} from "./User/User.js"; +import {Game} from "./Game/Game.js"; +import {Calculator} from "./Calculator/Calculator.js"; + class App { - async run() {} + #user; + #game; + #calculator; + + async run() { + this.#user = await this.#inputMoney(); + OutputView.printPurchasedLottos(this.#user.getMyLottos()); + this.#game = await this.#inputLottoNumbers(); + this.#calculator = new Calculator(...this.#game.getWinNumInfo(), this.#user.getMyLottos()); + OutputView.printResult(this.#calculator.getResult()); + } + + async #inputMoney() { + while (true) { + try{ + const money = await InputView.askAmountMoney(); + return new User(money); + }catch (e) { + OutputView.printErrorMessage(e.message); + } + } + } + + async #inputLottoNumbers() { + while (true) { + try { + const winNum = await InputView.askWinningLotto(); + const bonusNum = await InputView.askBonusNumber(); + + return new Game(winNum, bonusNum); + }catch (e) { + OutputView.printErrorMessage(e.message); + } + } + } } export default App; + +console.log(parseInt('100m', 10)); \ No newline at end of file diff --git a/src/App.test.js b/src/App.test.js new file mode 100644 index 00000000..c8a36114 --- /dev/null +++ b/src/App.test.js @@ -0,0 +1,45 @@ +import App from "../src/App.js"; +import {getLogSpy, mockQuestions, mockRandoms} from "../testUtils.js"; +import {LOTTO} from "./constants.js"; + +describe("로또 테스트", () => { + beforeEach(() => { + jest.restoreAllMocks(); + }); + + test("기능 테스트", async () => { + const logSpy = getLogSpy(); + + mockRandoms([ + [1, 2, 3, 4, 5], + [1, 2, 3, 4, 6], + [1, 2, 3, 6, 7], + [1, 2, 6, 7, 8], + ]); + mockQuestions([String(LOTTO.PRICE * 4), "1,2,3,4,5", "6"]); + + const app = new App(); + await app.run(); + + const logs = [ + "4개를 구매했습니다.", + "[1, 2, 3, 4, 5]", + "[1, 2, 3, 4, 6]", + "[1, 2, 3, 6, 7]", + "[1, 2, 6, 7, 8]", + "당첨 통계", + "5개 일치 (100,000,000원) - 1개", + "4개 일치, 보너스 번호 일치 (10,000,000원) - 1개", + "4개 일치 (1,500,000원) - 0개", + "3개 일치, 보너스 번호 일치 (500,000원) - 1개", + "2개 일치, 보너스 번호 일치 (5,000원) - 1개", + "0개 일치 (0원) - 0개", + ]; + + logs.forEach((log) => { + expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(log)); + }); + }); + + +}); diff --git a/src/Calculator/Calculator.js b/src/Calculator/Calculator.js new file mode 100644 index 00000000..168b562e --- /dev/null +++ b/src/Calculator/Calculator.js @@ -0,0 +1,57 @@ +export class Calculator { + #winNum; + #bonus; + #lottos; + #result = new Map(); + + constructor(winNum, bonus, lottos) { + this.#winNum = new Set(winNum); + this.#bonus = bonus; + this.#lottos = lottos; + + this.#calcLottos(); + } + + getResult() { + const copyResult = new Map(); + for (const [key, value] of this.#result.entries()) { + copyResult.set(key, value); + } + return copyResult; + } + + #calcLottos() { + this.#lottos.forEach(lotto => { + const result = this.#calcLottoPrice(lotto); + if (this.#result.has(result)) { + this.#result.set(result, this.#result.get(result) + 1); + return; + } + this.#result.set(result, 1); + }); + } + + #calcLottoPrice(lottoNum) { + const [cnt, bonus] = this.#countLottoNum(lottoNum); + if (cnt === 5) return 1; + if (cnt === 4 && bonus) return 2; + if (cnt === 4) return 3; + if (cnt === 3 && bonus) return 4; + if (cnt === 2 && bonus) return 5; + return 0; + } + + #countLottoNum(lottoNums) { + let cnt = 0; + let bonus = false; + lottoNums.forEach(num => { + if (this.#winNum.has(num)) { + cnt++; + } + if (num === this.#bonus) { + bonus = true; + } + }); + return [cnt, bonus]; + } +} \ No newline at end of file diff --git a/src/Calculator/Calculator.test.js b/src/Calculator/Calculator.test.js new file mode 100644 index 00000000..aa21c208 --- /dev/null +++ b/src/Calculator/Calculator.test.js @@ -0,0 +1,21 @@ +import {Calculator} from "./Calculator.js"; + +describe('Calculator 정상 테스트', () => { + test('1등 3등 0등', () => { + const winNum = [1, 2, 3, 4, 5]; + const bonus = 6; + const myLottos = [ + [1, 2, 3, 4, 5], + [1, 2, 3, 4, 10], + [1, 11, 12, 13, 14] + ]; + const answer = new Map(); + answer.set(0, 1); + answer.set(1, 1); + answer.set(3, 1); + + const calculator = new Calculator(winNum, bonus, myLottos); + + expect(calculator.getResult()).toEqual(answer); + }); +}); \ No newline at end of file diff --git a/src/Game/Game.js b/src/Game/Game.js new file mode 100644 index 00000000..e8998335 --- /dev/null +++ b/src/Game/Game.js @@ -0,0 +1,29 @@ +import {ERROR_MSG, LOTTO} from "../constants.js"; + +export class Game { + #winNUm; + #bonusNum; + + constructor(winNum, bonusNum) { + this.#validate(winNum, bonusNum); + this.#winNUm = winNum.sort((a, b) => a - b); + this.#bonusNum = bonusNum; + } + + getWinNumInfo() { + const winNum = [...this.#winNUm]; + return [winNum, this.#bonusNum]; + } + + #validate(winNum, bonusNum) { + const winNumSet = new Set(winNum); + winNum.forEach(num => { + if (isNaN(num)) throw new Error(ERROR_MSG.LOTTO_MUST_NUM); + if (num < LOTTO.NUM_START || num > LOTTO.NUM_END) throw new Error(ERROR_MSG.NUM_OUT_RANGE); + }); + if (isNaN(bonusNum)) throw new Error(ERROR_MSG.BONUS_MUST_NUM); + if (winNumSet.size !== LOTTO.LENGTH) throw new Error(ERROR_MSG.NUM_DUPLICATE); + if (winNumSet.has(bonusNum)) throw new Error(ERROR_MSG.NUM_DUPLICATE); + if (bonusNum < LOTTO.NUM_START || bonusNum > LOTTO.NUM_END) throw new Error(ERROR_MSG.NUM_OUT_RANGE); + } +} \ No newline at end of file diff --git a/src/Game/Game.test.js b/src/Game/Game.test.js new file mode 100644 index 00000000..d8511a80 --- /dev/null +++ b/src/Game/Game.test.js @@ -0,0 +1,55 @@ +import {Game} from "./Game.js"; +import {ERROR_MSG, LOTTO} from "../constants.js"; + +describe('Game 정상 입력', () => { + test('[1,2,3,4,MAX]', () => { + const winNum = [1, 2, 3, 4, LOTTO.NUM_END]; + const bonus = 6; + + const game = new Game(winNum, bonus); + + expect(game.getWinNumInfo()).toEqual([winNum, bonus]); + }); +}); + +describe('Game 예외처리', () => { + test('범위를 벗어난 로또 숫자', () => { + const winNum = [1, 2, 3, 4, LOTTO.NUM_END + 1]; + const bonus = 10; + + expect(() => new Game(winNum, bonus)).toThrow(ERROR_MSG.NUM_OUT_RANGE); + }); + + test('범위를 벗어난 보너스 숫자', () => { + const winNum = [1, 2, 3, 4, 5]; + const bonus = LOTTO.NUM_END + 1; + + expect(() => new Game(winNum, bonus)).toThrow(ERROR_MSG.NUM_OUT_RANGE); + }); + + test('중복된 숫자의 로또 번호', () => { + const winNum = [1, 2, 3, 4, 1]; + const bonus = 10; + + expect(() => new Game(winNum, bonus)).toThrow(ERROR_MSG.NUM_DUPLICATE); + }); + + test('중복된 번호의 보너스 번호', () => { + const winNum = [1, 2, 3, 4, 5]; + const bonus = 1; + expect(() => new Game(winNum, bonus)).toThrow(ERROR_MSG.NUM_DUPLICATE); + }); + + test('숫자가 아닌 로또 번호', () => { + const winNum = [1, 2, 3, 4, 'a']; + const bonus = 10; + expect(() => new Game(winNum, bonus)).toThrow(ERROR_MSG.LOTTO_MUST_NUM); + }); + + test('숫자가 아닌 보너스 번호', () => { + const winNum = [1, 2, 3, 4, 5]; + const bonus = 'a'; + expect(() => new Game(winNum, bonus)).toThrow(ERROR_MSG.BONUS_MUST_NUM); + + }); +}); \ No newline at end of file diff --git a/src/Lotto/Lotto.js b/src/Lotto/Lotto.js new file mode 100644 index 00000000..6ca82f92 --- /dev/null +++ b/src/Lotto/Lotto.js @@ -0,0 +1,29 @@ +import {Random} from "@woowacourse/mission-utils"; +import {ERROR_MSG, LOTTO} from "../constants.js"; + +export class Lotto { + #nums; + + constructor() { + const nums = this.#makeRandomNumbers(); + this.#validate(nums); + this.#nums = nums; + } + + getNum() { + return [...this.#nums]; + } + + #makeRandomNumbers() { + return Random.pickUniqueNumbersInRange(LOTTO.NUM_START, LOTTO.NUM_END, LOTTO.LENGTH).sort((a, b) => a - b); + } + + #validate(nums) { + const numSet = new Set(nums); + nums.forEach(num => { + if (isNaN(num)) throw new Error(ERROR_MSG.RANDOM_STRING); + if (num < LOTTO.NUM_START || num > LOTTO.NUM_END) throw new Error(ERROR_MSG.RANDOM_OUT_RANGE); + }); + if (numSet.size !== LOTTO.LENGTH) throw new Error(ERROR_MSG.RANDOM_DUPLICATE); + } +} diff --git a/src/Lotto/Lotto.test.js b/src/Lotto/Lotto.test.js new file mode 100644 index 00000000..27823e0c --- /dev/null +++ b/src/Lotto/Lotto.test.js @@ -0,0 +1,51 @@ +import {mockRandoms} from "../../testUtils.js"; +import {Lotto} from "./Lotto.js"; +import {ERROR_MSG, LOTTO} from "../constants.js"; + +describe('Lotto test', () => { + test('정상 작동 1,2,3,4,5', () => { + const randoms = [[1, 2, 3, 4, 5]]; + const nums = [1, 2, 3, 4, 5]; + + mockRandoms(randoms); + const lotto = new Lotto(); + + expect(lotto.getNum()).toEqual(nums); + }); + + test('Lotto num sorted', () => { + const random = [[10, 9, 8, 7, 6]]; + const nums = [6, 7, 8, 9, 10]; + + mockRandoms(random); + const lotto = new Lotto(); + + expect(lotto.getNum()).toEqual(nums); + }); +}); + +describe('Lotto 예외처리', () => { + test('Random 함수가 비정상일 경우 - 중복', () => { + const random = [[1, 1, 2, 3, 4]]; + + mockRandoms(random); + + expect(() => new Lotto()).toThrow(ERROR_MSG.RANDOM_DUPLICATE); + }); + + test('Random 함수가 비정상일 경우 - 문자열', () => { + const random = [[1, 'a', 2, 3, 4]]; + + mockRandoms(random); + + expect(() => new Lotto()).toThrow(ERROR_MSG.RANDOM_STRING); + }); + + test('Random 함수가 비정상일 경우 - 범위 밖 숫자', () => { + const random = [[1, LOTTO.NUM_END + 1, 2, 3, 4]]; + + mockRandoms(random); + + expect(() => new Lotto()).toThrow(ERROR_MSG.RANDOM_OUT_RANGE); + }); +}); \ No newline at end of file diff --git a/src/User/User.js b/src/User/User.js new file mode 100644 index 00000000..adb243ac --- /dev/null +++ b/src/User/User.js @@ -0,0 +1,28 @@ +import {ERROR_MSG, LOTTO} from "../constants.js"; +import {Lotto} from "../Lotto/Lotto.js"; + +export class User { + #Lottos = []; + + constructor(money) { + this.#validate(money); + this.#buyLotto(money); + } + + getMyLottos() { + return this.#Lottos.map(lotto => lotto.getNum()); + } + + #buyLotto(money) { + const lottoAmount = Math.floor(money / LOTTO.PRICE); + for (let i = 0; i < lottoAmount; i++) { + const lotto = new Lotto(); + this.#Lottos.push(lotto); + } + } + + #validate(money) { + if (isNaN(money)) throw new Error(ERROR_MSG.MONEY_MUST_NUM); + if (money < 0 || money < LOTTO.PRICE) throw new Error(ERROR_MSG.MONEY_TOO_LOW); + } +} diff --git a/src/User/User.test.js b/src/User/User.test.js new file mode 100644 index 00000000..05d44c63 --- /dev/null +++ b/src/User/User.test.js @@ -0,0 +1,53 @@ +import {User} from "./User.js"; +import {ERROR_MSG, LOTTO} from "../constants.js"; +import {mockRandoms} from "../../testUtils.js"; + +describe('User 정상 기능 테스트', () => { + test('정상 입력 3개', () => { + const random = [ + [1, 2, 3, 4, 5], + [10, 11, 12, 13, 14], + [5, 6, 7, 8, 9] + ]; + const money = LOTTO.PRICE * 3; + + mockRandoms(random); + const user = new User(money); + + expect(user.getMyLottos()).toEqual(random); + }); + + test('나눠 떨어지지 않는 금액', () => { + const random = [ + [1, 2, 3, 4, 5], + [10, 11, 12, 13, 14], + [5, 6, 7, 8, 9] + ]; + const money = LOTTO.PRICE * 3 + (LOTTO.PRICE - 1); + + mockRandoms(random); + const user = new User(money); + + expect(user.getMyLottos()).toEqual(random); + }); +}); + +describe('User 예외 처리', () => { + test('money Nan', () => { + const money = 'a'; + + expect(() => new User(money)).toThrow(ERROR_MSG.MONEY_MUST_NUM); + }); + + test('money too low', () => { + const money = LOTTO.PRICE - 1; + + expect(() => new User(money)).toThrow(ERROR_MSG.MONEY_TOO_LOW); + }); + + test('money is minus', () => { + const money = -LOTTO.PRICE; + + expect(() => new User(money)).toThrow(ERROR_MSG.MONEY_TOO_LOW); + }); +}); \ No newline at end of file diff --git a/src/constants.js b/src/constants.js new file mode 100644 index 00000000..982d473a --- /dev/null +++ b/src/constants.js @@ -0,0 +1,18 @@ +export const ERROR_MSG = { + MONEY_MUST_NUM: '구매금액은 숫자여야 합니다.', + LOTTO_MUST_NUM: '당첨 번호는 숫자여야 합니다.', + BONUS_MUST_NUM: '보너스 번호는 숫자여야 합니다.', + NUM_DUPLICATE: '로또 번호는 중복이 있으면 안됩니다.', + NUM_OUT_RANGE: '로또 번호는 1~30까지만 가능합니다.', + MONEY_TOO_LOW: '로또 1개 가격 이상의 값만 입력해주세요.', + RANDOM_DUPLICATE: 'Random 함수에 문제가 생겼습니다. (중복 발생)', + RANDOM_STRING: 'Random 함수에 문제가 생겼습니다. (문자열 생성)', + RANDOM_OUT_RANGE: 'Random 함수에 문제가 생겼습니다. (범위 밖 숫자 생성)', +}; + +export const LOTTO = { + PRICE: 500, + LENGTH: 5, + NUM_START: 1, + NUM_END: 30, +}; \ No newline at end of file diff --git a/src/view.js b/src/view.js index ae6afd9c..eeb8b33a 100644 --- a/src/view.js +++ b/src/view.js @@ -13,6 +13,15 @@ const InputView = { return num; }, + async askAmountMoney() { + const input = await MissionUtils.Console.readLineAsync('구입금액을 입력해 주세요.\n'); + const num = parseInt(input, 10); + if (Number.isNaN(num) || isNaN(input)) { + throw new Error('구매금액은 숫자여야 합니다.'); + } + return num; + }, + /** * @returns {number[]} */ diff --git a/testUtils.js b/testUtils.js new file mode 100644 index 00000000..949a3513 --- /dev/null +++ b/testUtils.js @@ -0,0 +1,44 @@ +import App from "./src/App.js"; +import {MissionUtils} from "@woowacourse/mission-utils"; +import {LOTTO} from "./src/constants.js"; + +export const mockQuestions = (inputs) => { + MissionUtils.Console.readLineAsync = jest.fn(); + + MissionUtils.Console.readLineAsync.mockImplementation(() => { + const input = inputs.shift(); + + return Promise.resolve(input); + }); +}; + +export const mockRandoms = (numbers) => { + MissionUtils.Random.pickUniqueNumbersInRange = jest.fn(); + numbers.reduce((acc, number) => { + return acc.mockReturnValueOnce(number); + }, MissionUtils.Random.pickUniqueNumbersInRange); +}; + +export const getLogSpy = () => { + const logSpy = jest.spyOn(MissionUtils.Console, "print"); + logSpy.mockClear(); + return logSpy; +}; + +export const runException = async (input) => { + // given + const logSpy = getLogSpy(); + + const RANDOM_NUMBERS_TO_END = [1, 2, 3, 4, 5]; + const INPUT_NUMBERS_TO_END = [LOTTO.PRICE, "1,2,3,4,5", "6"]; + + mockRandoms([RANDOM_NUMBERS_TO_END]); + mockQuestions([input, ...INPUT_NUMBERS_TO_END]); + + // when + const app = new App(); + await app.run(); + + // then + expect(logSpy).toHaveBeenCalledWith(expect.stringContaining("[ERROR]")); +};