Skip to content
Open
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
78 changes: 78 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,79 @@
# javascript-racingcar-precourse

## **자동차 경주** (Racingcar)

> 우아한테크코스 8기
2주차 과제
>

</br>

### 🍥 기능 요구 사항

---

각 세부 기능은 TDD의 Red-Green-Refactor 사이클을 따릅니다.

기능별 단위 테스트 코드 작성 및 실제 코드 작성을 커밋 단위로 관리합니다.

1. **입출력 처리 (Input Handling)**
- 입출력 처리에 대한 테스트를 구현한다.
- `Console.readLineAsync()`를 통해 사용자 입력을 비동기적으로 받는다.
- 입력 요청 메시지 및 에러 메세지를 핸들링할 수 있는 `InputView`, `OutputView`의 기본 구조를 구현한다.
2. **자동차 도메인 모델 구현 (Car)**
- Car 처리에 대한 테스트를 구현한다.
- name, position 상태를 가지는 `Car` 클래스를 생성한다.
- 무작위 값을 받아, 4 이상인 경우 position을 1 증가 시키는 기능을 구현한다.
3. **입력 검증 (Input Validation)**
- 입력 검증 로직에 대한 테스트를 구현한다.
- 자동차 입력값에 대한 검증 로직을 구현한다
- 입력값이 null 또는 undefined인 경우 “[ERROR] 잘못된 입력입니다.”를 출력한다.
- 입력 문자열 내에 , 이외의 특수 문자가 포함된 경우 “[ERROR] 잘못된 입력입니다.”를 출력한다.
- 하나의 자동차 이름이 5자 초과인 경우 “[ERROR] 자동차 이름 길이 조정이 필요합니다.”를 출력한다.
- 시도 횟수 입력값에 대한 검증 로직을 구현한다
- 시도할 횟수 입력에 음수가 포함된 경우 “[ERROR] 음수는 허용되지 않습니다.”를 출력한다.
- 시도할 횟수 입력에 숫자가 아닌 문자가 포함된 경우 “[ERROR] 숫자로 입력해야 합니다.”를 출력한다.
4. **게임 로직 구현 (RacingGame)**
- 게임 로직에 대한 테스트를 구현한다.
- N대의 자동차를 관리하는 기능을 구현한다.
- 단일 라운드를 실행하는 기능을 구현한다.
- 라운드별 실행 결과를 반환하는 기능을 구현한다.
- 최종 우승자 판별하는 기능을 구현한다.
5. **결과 출력 및 기능 통합 (Output)**
- 자동차 이름 입력 기능을 구현한다.
- 시도 횟수 입력 기능을 구현한다.
- 라운드를 반복할 수 있는 게임 루프 기능을 구현한다.
- 라운드별 실행 결과를 출력한다.
- 시도 횟수가 모두 종료되면 최종 우승자를 출력한다.
- 전체 애플리케이션의 에러 처리를 적절히 연동한다.

### 🍥 커밋 컨벤션

---

**AngularJS Commit Message Convention**을 따릅니다.

기능 구현 단위와 Commit 단위는 일치해야 하며,
README 문서 작성, 리팩터링, test 결과 반영 등 추가적인 Commit이 이루어질 수 있습니다.

| **타입** | **의미** |
| --- | --- |
| feat | 새로운 기능 추가 |
| fix | 버그 수정 |
| refactor | 코드 리팩토링 |
| test | 테스트 코드 추가/수정 |
| style | 코드 포맷 변경 |
| docs | 문서 수정 |

```c
feat: onUrlChange event

Added new event :
- forward popstate event if available
- forward hashchange event if popstate not available
- do polling when neither popstate nor hashchange available
```

### 🍥 회고

---
101 changes: 101 additions & 0 deletions __tests__/CarTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { MissionUtils } from '@woowacourse/mission-utils';
import Car from '../src/Car';
import { MOVE_THRESHOLD, RANDOM_MIN, RANDOM_MAX } from '../src/Constants';

const mockRandoms = (numbers) => {
MissionUtils.Random.pickNumberInRange = jest.fn();

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

describe('Car 클래스 테스트', () => {

test('생성자: name과 0의 position으로 Car 인스턴스를 생성한다.', () => {
// given
const carName = 'pobi';

// when
const car = new Car(carName);

// then
expect(car.name).toBe(carName);
expect(car.position).toBe(0);
});

test('move: 랜덤 값이 4 (MOVE_THRESHOLD) 이상일 경우 position이 1 증가한다.', () => {
// given
const MOVING_FORWARD = MOVE_THRESHOLD;
mockRandoms([MOVING_FORWARD]);
const car = new Car('pobi');

// when
car.move();

// then
expect(car.position).toBe(1);
expect(MissionUtils.Random.pickNumberInRange).toHaveBeenCalledWith(
RANDOM_MIN,
RANDOM_MAX
);
});

test('move: 랜덤 값이 4 (MOVE_THRESHOLD) 미만일 경우 position이 증가하지 않는다.', () => {
// given
const STOP = MOVE_THRESHOLD - 1;
mockRandoms([STOP]);
const car = new Car('woni');

// when
car.move();

// then
expect(car.position).toBe(0);
expect(MissionUtils.Random.pickNumberInRange).toHaveBeenCalledWith(
RANDOM_MIN,
RANDOM_MAX
);
});

test('move: move를 여러 번 호출 시 누적된 position을 올바르게 계산한다.', () => {
// given
const MOVING_FORWARD = 4;
const STOP = 3;

mockRandoms([MOVING_FORWARD, STOP, RANDOM_MAX]);
const car = new Car('jun');

// when
car.move();
car.move();
car.move();

// then
expect(car.position).toBe(2);
});

test("getStateString : position이 0일 때 올바른 문자열을 반환한다.", () => {
// given
const car = new Car("woni");
car.position = 0;

// when
const stateString = car.getStateString();

// then
expect(stateString).toBe("woni : ");
});

test("getStateString : position이 0보다 클 때 올바른 문자열을 반환한다.", () => {
// given
const car = new Car("pobi");
car.position = 3;

// when
const stateString = car.getStateString();

// then
expect(stateString).toBe("pobi : ---");
});
});
43 changes: 43 additions & 0 deletions __tests__/InputViewTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { MissionUtils } from '@woowacourse/mission-utils';
import InputView from '../src/InputView';

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

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

describe('InputView 테스트', () => {
test('readCarNames: 올바른 프롬프트 메시지를 출력하고 입력값을 반환한다.', async () => {
// given
const inputs = ['pobi,woni,jun'];
mockQuestions(inputs);

// when
const input = await InputView.readCarNames();

// then
expect(MissionUtils.Console.readLineAsync).toHaveBeenCalledWith(
'경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)\n'
);
expect(input).toBe('pobi,woni,jun');
});

test('readGameRounds: 올바른 프롬프트 메시지를 출력하고 입력값을 반환한다.', async () => {
// given
const inputs = ['5'];
mockQuestions(inputs);

// when
const input = await InputView.readGameRounds();

// then
expect(MissionUtils.Console.readLineAsync).toHaveBeenCalledWith(
'시도할 횟수는 몇 회인가요?\n'
);
expect(input).toBe('5');
});
});
61 changes: 61 additions & 0 deletions __tests__/OutputViewTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { MissionUtils } from '@woowacourse/mission-utils';
import OutputView from '../src/OutputView';

const getLogSpy = () => {
const logSpy = jest.spyOn(MissionUtils.Console, 'print');
logSpy.mockClear();
return logSpy;
};

describe('OutputView 테스트', () => {
test('printWinner: 단독 우승자일 경우 올바르게 출력한다.', () => {
// given
const logSpy = getLogSpy();
const winners = ['pobi'];

// when
OutputView.printWinner(winners);

// then
expect(logSpy).toHaveBeenCalledWith('\n최종 우승자 : pobi');
});

test('printWinner: 공동 우승자일 경우 쉼표로 구분하여 출력한다.', () => {
// given
const logSpy = getLogSpy();
const winners = ['pobi', 'jun'];

// when
OutputView.printWinner(winners);

// then
expect(logSpy).toHaveBeenCalledWith('\n최종 우승자 : pobi, jun');
});

test('printGameResult: 실행 결과 헤더와 라운드 결과를 순서대로 출력한다.', () => {
// given
const logSpy = getLogSpy();
const roundResult = 'pobi : -\nwoni : --';

// when
OutputView.printGameResult(roundResult);

// then
const calls = logSpy.mock.calls;
expect(calls[0][0]).toBe('\n실행 결과');
expect(calls[1][0]).toBe(roundResult);
expect(logSpy).toHaveBeenCalledTimes(2);
});

test('printError: 전달된 에러 메시지를 그대로 출력한다.', () => {
// given
const logSpy = getLogSpy();
const errorMessage = '[ERROR] 잘못된 입력입니다.';

// when
OutputView.printError(errorMessage);

// then
expect(logSpy).toHaveBeenCalledWith(errorMessage);
});
});
72 changes: 72 additions & 0 deletions __tests__/RacingGameTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { MissionUtils } from '@woowacourse/mission-utils';
import RacingGame from '../src/RacingGame.js';
import Car from '../src/Car.js';

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

describe('RacingGame 테스트', () => {
test('생성자: carNames 배열로 Car 인스턴스 배열을 생성한다.', () => {
// given
const carNames = ['pobi', 'woni'];
// when
const game = new RacingGame(carNames);
// then
expect(game.cars).toHaveLength(2);
expect(game.cars[0]).toBeInstanceOf(Car);
expect(game.cars[0].name).toBe('pobi');
});

test('runRound: 모든 자동차의 move 메서드를 호출하고 라운드 결과를 반환한다.', () => {
// given
const carNames = ['pobi', 'woni'];
const game = new RacingGame(carNames);
// pobi 전진(4), woni 정지(3)
mockRandoms([4, 3]);

// when
game.runRound();
const resultString = game.getRoundResult();

// then
expect(game.cars[0].position).toBe(1);
expect(game.cars[1].position).toBe(0);
expect(resultString).toBe('pobi : -\nwoni : ');
});

test('getWinners: 가장 많이 전진한 우승자를 반환한다 (단독 우승).', () => {
// given
const carNames = ['pobi', 'woni', 'jun'];
const game = new RacingGame(carNames);
// pobi: 1, woni: 2, jun: 1
game.cars[0].position = 1;
game.cars[1].position = 2;
game.cars[2].position = 1;

// when
const winners = game.getWinners();

// then
expect(winners).toEqual(['woni']);
});

test('getWinners: 가장 많이 전진한 우승자를 반환한다 (공동 우승).', () => {
// given
const carNames = ['pobi', 'woni', 'jun'];
const game = new RacingGame(carNames);
// pobi: 2, woni: 1, jun: 2
game.cars[0].position = 2;
game.cars[1].position = 1;
game.cars[2].position = 2;

// when
const winners = game.getWinners();

// then
expect(winners).toEqual(['pobi', 'jun']);
});
});
Loading