Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[1단계 - 자동차 경주] 해리(최현웅) 미션 제출합니다. #260

Merged
merged 36 commits into from
Feb 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
510b652
chore: .gitignore 설정 추가
Yoonkyoungme Feb 14, 2024
fad42fb
chore: 커밋 공동 작업자 추가
Yoonkyoungme Feb 14, 2024
4185421
docs: 기능 목록 작성
Yoonkyoungme Feb 14, 2024
221af86
feat: 랜덤 숫자 생성 함수 구현
Yoonkyoungme Feb 14, 2024
a0c25b5
feat: 이동 여부를 결정하는 함수 구현
Yoonkyoungme Feb 14, 2024
4eae2d9
feat: 자동차 A가 자동차 B와 위치가 같은지 판단하는 함수 구현
Yoonkyoungme Feb 14, 2024
44a3957
feat: 자동차 A가 자동차 B와 위치보다 앞서가는지 결정하는 함수 구현
Yoonkyoungme Feb 14, 2024
da81312
feat: 각 라운드의 결과 만드는 함수 구현
Yoonkyoungme Feb 14, 2024
03f00d6
feat: 우승자 판단하는 함수 구현
Yoonkyoungme Feb 14, 2024
71bbcac
feat: Console 관련 모듈 (입력, 출력) 구현
Yoonkyoungme Feb 14, 2024
bac4164
feat: 경주할 자동차 이름을 입력받기
Yoonkyoungme Feb 14, 2024
9b39963
feat: 시도할 횟수는 입력 받기
Yoonkyoungme Feb 14, 2024
0e915c4
feat: 각 라운드 결과 출력
Yoonkyoungme Feb 14, 2024
da035c9
feat: 우승자 이름 출력
Yoonkyoungme Feb 14, 2024
12d4390
fix: makeRoundResult 함수에서 move 함수 호출 누락한 부분 수정
Yoonkyoungme Feb 14, 2024
649a7f6
feat: RaceController 구현 및 호출
Yoonkyoungme Feb 14, 2024
69e746f
feat: 자동차 이름 유효성 검사 로직 구현
Yoonkyoungme Feb 15, 2024
d17385b
feat: 시도할 횟수 유효성 검사 로직 구현
Yoonkyoungme Feb 15, 2024
50469b2
fix: retrun 되지 않았던 문제 수정
Yoonkyoungme Feb 15, 2024
32b2a98
modify: 메시지 상수화 및 사용
Yoonkyoungme Feb 15, 2024
77e3d58
rename: 파일명 변경
Yoonkyoungme Feb 15, 2024
5928ad6
test: 자동차 도메인 테스트
Yoonkyoungme Feb 15, 2024
81fabbe
refactor: findMaxPosition 메서드를 private으로 설정
Yoonkyoungme Feb 15, 2024
316b9ac
test: 자동차 경주 도메인 테스트
Yoonkyoungme Feb 15, 2024
51a4e6d
test: 자동차 경주 애플리케이션 테스트
Yoonkyoungme Feb 15, 2024
bc1f7e3
modify: 매직 넘버, 메시지 상수화 정의 및 사용
Yoonkyoungme Feb 15, 2024
e0a5761
modify: 객체 불변성 유지 유틸 함수 구현
Yoonkyoungme Feb 15, 2024
45ec6a9
remove: 사용 안 하는 파일 삭제
Yoonkyoungme Feb 15, 2024
4b03951
test: 랜덤 함수 모킹 방법 변경 & 코드 중복 제거
hwinkr Feb 16, 2024
504c368
chore(constants): 자동차 이름 유효 길이 상수, 경주 시도 횟수 에러 메시지 상수 추가
hwinkr Feb 16, 2024
bff98f2
feat(process input): 입력을 처리하는 유틸 함수 구현
hwinkr Feb 16, 2024
b8a3999
refactor(use util function): 입력을 처리하는 유틸 함수를 사용하도록 변경
hwinkr Feb 16, 2024
6657b5f
refactor(CarRace): 가장 멀리 간 자동차를 찾을 때 reduce를 사용해서 선언적으로 구현
hwinkr Feb 16, 2024
5453a0a
chore(use constants): 자동차 유효 길이 상수 사용하도록 변경
hwinkr Feb 16, 2024
07f8514
chore(split error message): 에러 메시지 세분화
hwinkr Feb 16, 2024
ec6ffdb
chore(change function name): makes -> make 함수 이름 수정
hwinkr Feb 16, 2024
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
node_modules/
dist/
.DS_Store
1 change: 1 addition & 0 deletions .gitmessage
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Co-authored-by: hwinkr <dnddl8280@naver.com>
98 changes: 98 additions & 0 deletions __tests__/Application.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import App from "../src/App";
import pickNumberInRange from "../src/utils/pickNumberInRange";
import Console from "../src/utils/Console";
import { ERROR_MESSAGES } from "../src/constants/car-race";

jest.mock("../src/utils/pickNumberInRange", () => {
return jest.fn();
});

const mockRandoms = (numbers) => {
numbers.forEach((number) => {
pickNumberInRange.mockReturnValueOnce(number);
});
};

const mockQuestions = (inputs) => {
Console.readLineAsync = jest.fn();

Console.readLineAsync.mockImplementation(() => {
const input = inputs.shift();
return Promise.resolve(input);
});
};

const getLogSpy = () => {
const logSpy = jest.spyOn(Console, "print");
logSpy.mockClear();

return logSpy;
};

describe("자동차 경주 애플리케이션 테스트", () => {
it("정상적인 전진-정지의 경우 올바르게 출력 되는지 테스트", async () => {
// given
const MOVING_FORWARD = 4;
const STOP = 3;
const randoms = [MOVING_FORWARD, STOP];

const inputs = ["harry,bong", "1"];
const harryOutput = ["harry : -"];
const bongOutput = ["bong :"];

const logSpy = getLogSpy();
// when
mockQuestions(inputs);
mockRandoms(randoms);

const app = new App();
await app.run();

// then
[...harryOutput, ...bongOutput].forEach((output) => {
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(output));
});
});
});

describe("자동차 경주 입력에 대한 예외 테스트", () => {
const TEST_CASES = [
{
inputs: ["harry,harry", "harry,bong", "5"],
expectedErrorMessage: ERROR_MESSAGES.carNameUniqueness,
},
{
inputs: ["harrrrrry,harry", "harry,bong", "5"],
expectedErrorMessage: ERROR_MESSAGES.carNameLength,
},
{
inputs: ["harry,bong", "a", "10"],
expectedErrorMessage: ERROR_MESSAGES.invalidNumberType,
},
{
inputs: ["harry,bong", "-1", "10"],
expectedErrorMessage: ERROR_MESSAGES.negativeTryCount,
},
];

let logSpy;

beforeEach(() => {
logSpy = getLogSpy();
});

test.each(TEST_CASES)(
"각 예외 상황에 맞는 에러 메시지를 콘솔에 출력하는지 테스트",
async ({ inputs, expectedErrorMessage }) => {
console.log(inputs, expectedErrorMessage);
mockQuestions(inputs);

const app = new App();
await app.run();

expect(logSpy).toHaveBeenCalledWith(
expect.stringContaining(expectedErrorMessage)
);
}
);
});
52 changes: 52 additions & 0 deletions __tests__/CarRace.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import CarRace from '../src/domains/CarRace';
import pickNumberInRange from '../src/utils/pickNumberInRange';

jest.mock('../src/utils/pickNumberInRange', () => {
return jest.fn();
});

const mockRandoms = (numbers) => {
numbers.reduce((acc, number) => {
return acc.mockReturnValueOnce(number);
}, pickNumberInRange);
};

describe('자동차 경주 도메인 테스트', () => {
let carRace;

beforeEach(() => {
carRace = new CarRace('harry,bong,pobi');
});

it('각 라운드의 결과를 올바르게 표시한다.', () => {
// given
mockRandoms([1, 4, 9]);
const expectedResult = {
harry: 0,
bong: 1,
pobi: 1,
};

// when
const roundResult = carRace.makesRoundResult();

// then
expect(roundResult).toEqual(expectedResult);
});

it('우승자를 올바르게 판단한다.', () => {
// given
mockRandoms([1, 4, 9, 7, 1, 3, 2, 8, 5]);
const TRY_COUNT = 3;
const expectedWinner = ['bong', 'pobi'];

// when
Array.from({ length: TRY_COUNT }, () => {
carRace.makesRoundResult();
});
const winners = carRace.judgeWinners();

// then
expect(winners).toEqual(expectedWinner);
});
});
88 changes: 88 additions & 0 deletions __tests__/CarTest.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import Car from '../src/domains/Car';
import pickNumberInRange from '../src/utils/pickNumberInRange';

jest.mock('../src/utils/pickNumberInRange', () => {
return jest.fn();
});

const mockRandoms = (numbers) => {
numbers.reduce((acc, number) => {
return acc.mockReturnValueOnce(number);
}, pickNumberInRange);
};

describe('자동차 도메인 테스트', () => {
let harryCar;

beforeEach(() => {
harryCar = new Car('harry');
});

it('숫자가 4이상인 경우에 자동차는 전진해야한다.', () => {
// given
mockRandoms([4, 6]);
const expectedPosition = 2;

// when
harryCar.move();
harryCar.move();

// then
expect(harryCar.getPosition()).toBe(expectedPosition);
});

it('숫자가 4미만인 경우 자동차는 전진할 수 없다.', () => {
// given
mockRandoms([1, 3]);
const expectedPosition = 0;

// when
harryCar.move();
harryCar.move();

// then
expect(harryCar.getPosition()).toBe(expectedPosition);
});

it('두 자동차의 위치가 같은 경우 true를 반환해야 한다.', () => {
// given
const bongCar = new Car('bong');
mockRandoms([4, 9]);

// when
harryCar.move();
bongCar.move();

// then
const isSame = harryCar.isSamePosition(bongCar);
expect(isSame).toBe(true);
});

it('두 자동차의 위치가 다른 경우 false 반환해야 한다.', () => {
// given
const bongCar = new Car('bong');
mockRandoms([3, 9]);

// when
harryCar.move();
bongCar.move();

// then
const isSame = harryCar.isSamePosition(bongCar);
expect(isSame).toBe(false);
});

it('자동차 도메인은 다른 자동차 도메인과 위치 비교를 할 수 있어야 한다.', () => {
// given
const bongCar = new Car('bong');
mockRandoms([1, 7]);

// when
harryCar.move();
bongCar.move();

// then
const isAheadOf = harryCar.isAheadOf(bongCar);
expect(isAheadOf).toBe(false);
});
});
31 changes: 31 additions & 0 deletions docs/REQUIREMENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
## 기능 목록

### 입력

- [x] 경주할 자동차 이름을 입력받기
- [x] 유효성 검사 1: 중복
- [x] 유효성 검사 2: 자동차 이름 5자 이하
- [x] 시도할 횟수는 입력받기
- [x] 유효성 검사 1: 정수

### 기능

#### Car

- [x] 이동 여부를 결정하는 함수 구현
- [x] 자동차 A가 자동차 B와 위치가 같은지 판단하는 함수 구현
- [x] 자동차 A가 자동차 B와 위치보다 앞서가는지 결정하는 함수 구현

#### Car Race

- [x] 각 라운드의 결과 만드는 함수 구현
- [x] 우승자 판단하는 함수 구현

#### 기타

- [x] 랜덤 숫자 생성 함수 구현

### 출력

- [x] 각 라운드 결과 출력
- [x] 우승자 이름 출력
15 changes: 15 additions & 0 deletions src/App.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import RaceController from './controller/RaceController';

class App {
#raceController;

constructor() {
this.#raceController = new RaceController();
}

async run() {
await this.#raceController.run();
}
}

export default App;
30 changes: 30 additions & 0 deletions src/constants/car-race.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
export const INPUT_QUERY = {
carNames:
"경주할 자동차 이름을 입력하세요(이름은 쉼표(,)를 기준으로 구분).\n",
tryCount: "시도할 횟수는 몇 회인가요?\n",
};

export const MESSAGES = {
result: "실행 결과",
winner: "최종 우승자 : ",
positionMark: "-",
};

export const ERROR_MESSAGES = {
prefix: "[ERROR]",
carNameLength: "경주할 자동차 이름은 1자 이상 5자 이하만 가능합니다.",
carNameUniqueness: "자동차 이름은 중복될 수 없습니다.",
negativeTryCount: "시도할 횟수는 1이상의 숫자만 가능합니다.",
invalidNumberRange: "최솟값은 최대값보다 작거나 같아야 합니다.",
invalidNumberType: "숫자만 입력 가능합니다.",
};

export const RULES = {
minRandomNumber: 1,
maxRandomNumber: 9,
minCarNameLength: 1,
maxCarNameLength: 5,
moveStandard: 4,
initialPosition: 0,
movingUnit: 1,
};
68 changes: 68 additions & 0 deletions src/controller/RaceController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import CarRace from "../domains/CarRace.js";

import InputView from "../views/InputView.js";
import OutputView from "../views/OutputView.js";

import carNamesValidator from "../validators/carNamesValidator";
import tryCountValidator from "../validators/tryCountValidator";
import { MESSAGES } from "../constants/car-race.js";
import processInputWithRetry from "../utils/processInputWithRetry.js";

class RaceController {
#carRace;

constructor() {
this.#carRace = null;
}

async #processCarNames() {
const carNames = processInputWithRetry(
InputView.readCarNames.bind(InputView),
carNamesValidator.validate.bind(carNamesValidator),
OutputView.printMessage.bind(OutputView)
);

return carNames;
}

async #processTryCount() {
const carNames = processInputWithRetry(
InputView.readTryCount.bind(InputView),
tryCountValidator.validate.bind(tryCountValidator),
OutputView.printMessage.bind(OutputView)
);

return carNames;
}

async #initCarRace() {
const carNames = await this.#processCarNames();
this.#carRace = new CarRace(carNames);

const tryCount = await this.#processTryCount();
return tryCount;
}

async #playCarRace(tryCount) {
OutputView.printMessage(MESSAGES.result);

Array.from({ length: tryCount }, () => {
const roundResult = this.#carRace.makesRoundResult();
OutputView.printRoundResult(roundResult);
OutputView.printBlankLine();
});
}
Comment on lines +49 to +54

Choose a reason for hiding this comment

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

nit) (nit 은 꼭 반영하지 않아도 되고, 참고차 말씀드리는내용입니다)
이렇게 단순 반복에는 for 을 사용해도 좋을거 같아요
Array.from을 사용하게 되면, 불필요한 배열 생성이 되기 때문입니다.


#announceWinners() {
const winners = this.#carRace.judgeWinners();
OutputView.printWinners(winners);
}

async run() {
const tryCount = await this.#initCarRace();
await this.#playCarRace(tryCount);
this.#announceWinners();
}
}

export default RaceController;
Loading