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

[클린코드 7기 김동진] 자동차 경주 미션 STEP 4 #318

Open
wants to merge 14 commits into
base: terrydkim
Choose a base branch
from
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
35 changes: 16 additions & 19 deletions __tests__/Car.test.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { ERROR_MESSAGES } from "../src/utils/constants.js";
import Car from "../src/domain/Car.js";
import { CAR_NAMES } from "../src/utils/constants.js";

describe("자동차 세팅 테스트 ", () => {
describe("자동차는", () => {
let car;
beforeAll(() => {
car = new Car("NEXT");
beforeEach(() => {
car = new Car(CAR_NAMES.NEXT);
});

it("이름을 가진다", () => {
expect(car.getName()).toEqual("NEXT");
expect(car.getName()).toEqual(CAR_NAMES.NEXT);
});

it("위치 값을 가지며, 초기 값은 0이다.", () => {
Expand All @@ -19,7 +21,7 @@ describe("자동차 세팅 테스트 ", () => {

describe("자동차 동작 테스트", () => {
it(" 자동차는 전진할 수 있으며 한 번에 1만큼 전진한다.", () => {
const car = new Car("NEXT");
const car = new Car(CAR_NAMES.NEXT);

car.moveForward();

Expand All @@ -29,46 +31,41 @@ describe("자동차 동작 테스트", () => {

describe("자동차에 이름을 부여할 수 있다.", () => {
it("자동차는 생성 시 부여한 이름을 가진다.", () => {
const name = "GV80";
const car = new Car(name);
expect(car.getName()).toBe(name);
const car = new Car(CAR_NAMES.GV80);
expect(car.getName()).toBe(CAR_NAMES.GV80);
});

it("자동차는 여러 대가 생성될 수 있다.", () => {
const carNames = ["GV80", "G70"];
const cars = carNames.map((car) => new Car(car));
expect(cars.map((car) => car.getName())).toEqual(carNames);
const carArray = [CAR_NAMES.GV80, CAR_NAMES.G70];
const cars = carArray.map((car) => new Car(car));
expect(cars.map((car) => car.getName())).toEqual(carArray);
});
});

describe("자동차 이름 유효성 테스트", () => {
const INVALID_CAR_NAME = "잘못된 자동차 이름입니다.";

it("이름이 1자 미만이면 에러를 던진다.", () => {
const car = () => new Car("");

expect(car).toThrow(Error);
expect(car).toThrow(INVALID_CAR_NAME);
expect(car).toThrow(ERROR_MESSAGES.INVALID_CAR_NAME);
});

it("이름이 1자면 유효성 검사에 통과한다.", () => {
const name = "N";
const car = () => new Car(name);
const car = () => new Car(CAR_NAMES.N);

expect(car).not.toThrow();
});

it("이름이 5자이면 유효성 검사에 통과한다.", () => {
const name = "MYCAR";
const car = () => new Car(name);
const car = () => new Car(CAR_NAMES.M_CAR);

expect(car).not.toThrow();
});

it("이름이 6자 이상이면 에러를 던진다.", () => {
const car = () => new Car("NEXTSTEP");
const car = () => new Car(CAR_NAMES.NEXT_STEP);

expect(car).toThrow(Error);
expect(car).toThrow(INVALID_CAR_NAME);
expect(car).toThrow(ERROR_MESSAGES.INVALID_CAR_NAME);
});
});
67 changes: 67 additions & 0 deletions __tests__/Input.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import Input from "../src/view/Input";
import { CAR_NAMES } from "../src/utils/constants";

describe("Input 유효성 메서드 테스트", () => {
let input;
beforeEach(() => {
input = new Input();
});

describe("이름 안에 공백이 포함된 경우", () => {
it("이름에 공백이 있으면 false를 반환한다.", () => {
const validate = input.isValidNotSpace([CAR_NAMES.NAME_SPACE]);
expect(validate).toBeFalsy();
});

it("이름에 공백이 없으면 true를 반환한다.", () => {
const validate = input.isValidNotSpace([CAR_NAMES.NAME]);
expect(validate).toBeTruthy();
});
});

describe("SplitBy 메서드", () => {
it("쉼표로 구분된 문자열을 배열로 변환한다.", () => {
const result = input.splitBy("G70,GV80,NEXT_STEP", ",");
expect(result).toEqual([CAR_NAMES.G70, CAR_NAMES.GV80, CAR_NAMES.NEXT_STEP]);
});

it("-로 구분된 문자열을 배열로 변환한다.", () => {
const result = input.splitBy("G70-GV80-NEXT_STEP", "-");
expect(result).toEqual([CAR_NAMES.G70, CAR_NAMES.GV80, CAR_NAMES.NEXT_STEP]);
});

it("이름의 앞 뒤 공백을 제거하고 배열로 변환한다.", () => {
const result = input.splitBy(" G70,GV80 , NEXT_STEP ", ",");
expect(result).toEqual([CAR_NAMES.G70, CAR_NAMES.GV80, CAR_NAMES.NEXT_STEP]);
});
});

describe("경주 횟수 입력 유효성 검사", () => {
it("사용자가 양수를 입력하면 true를 반환한다.", () => {
const result = input.isValidInteger(1);
expect(result).toBeTruthy();
});

describe("사용자가 양수가 아닌", () => {
it("0 입력 시 false를 반환한다.", () => {
const result = input.isValidInteger(0);
expect(result).toBeFalsy();
});

it("음수 입력 시 false를 반환한다.", () => {
const result = input.isValidInteger(-1);
expect(result).toBeFalsy();
});

it("유리수 입력 시 false를 반환한다.", () => {
const result = input.isValidInteger(1.11);
expect(result).toBeFalsy();
});

it("String 입력 시 false를 반환한다.", () => {
const result = input.isValidInteger("일");
expect(result).toBeFalsy();
});
});
});
});
49 changes: 17 additions & 32 deletions __tests__/Race.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import Car from "../src/domain/Car";
import Race from "../src/domain/Race";
import { CAR_NAMES, ROUNDS } from "../src/utils/constants";

describe("경주 테스트", () => {
describe("기본 경주 테스트", () => {
let cars;
let race;

Expand All @@ -15,45 +16,29 @@ describe("경주 테스트", () => {
expect(race.cars).toEqual(cars);
});

it("경주는 5회로 고정하여 진행한다.", () => {
const DEFAULT_ROUNDS = 5;
expect(race.rounds).toBe(DEFAULT_ROUNDS);
it("경주는 기본 5회로 진행한다.", () => {
expect(race.rounds).toBe(Race.DEFAULT_ROUNDS);
});
});

describe("자동차는 1회당 한 칸씩 전진한다.", () => {
const MOVE_DISTANCE = 1;
let result;

beforeEach(() => {
race.start();
result = race.result;
describe("경주 결과 테스트", () => {
it("경주가 끝나면 결과를 반환한다.", () => {
expect(race.start()).toEqual(race.result);
});

it("1회에는 한 칸 이동한다.", () => {
const FIRST_ROUND = 1;

const firstRound = result.find(
(round) => round.round === FIRST_ROUND * MOVE_DISTANCE
);
const allCarsFirstMovedLocation = firstRound.cars.every(
(car) => car.location === FIRST_ROUND
);
it("우승자 조회시 우승자 목록을 반환한다.", () => {
race.start();

expect(allCarsFirstMovedLocation).toBeTruthy();
expect(race.getWinners().length).toBeGreaterThan(0);
});
});
});

it("5회에는 다섯 칸 이동한다.", () => {
const TOTAL_ROUND = 5;

const finalRound = result.find(
(round) => round.round === TOTAL_ROUND * MOVE_DISTANCE
);
const allCarsFinalLocation = finalRound.cars.every(
(car) => car.location === TOTAL_ROUND * MOVE_DISTANCE
);
describe("사용자 입력 경주 테스트", () => {
it("사용자가 10을 입력하면 경주를 10회 진행한다.", () => {
const cars = [new Car(CAR_NAMES.G70), new Car(CAR_NAMES.GV80)];
const race = new Race(cars, ROUNDS.TEN);

expect(allCarsFinalLocation).toBeTruthy();
});
expect(race.rounds).toBe(ROUNDS.TEN);
});
});
14 changes: 8 additions & 6 deletions docs/REQUIREMENTS.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
## 요구사항

- [ ] 자동차에 이름을 부여할 수 있다. 전진하는 자동차를 출력할 때 자동차 이름을 같이 출력한다.
- [ ] 자동차 이름은 쉼표(,)를 기준으로 구분하며 이름은 5자 이하만 가능하다.
- [ ] 자동차 경주는 5회로 고정하여 진행한다.
- [ ] 자동차는 1회당 1칸씩 전진한다
- [ ] 회차를 거듭할 때마다 자동차가 지나간 궤적을 출력한다(실행 예시 참고).
- [ ] 사용자가 잘못된 입력 값을 작성한 경우 프로그램을 종료한다.
- [X] 자동차에 이름을 부여할 수 있다. 전진하는 자동차를 출력할 때 자동차 이름을 같이 출력한다.
- [X] 자동차 이름은 쉼표(,)를 기준으로 구분하며 이름은 5자 이하만 가능하다.
- [X] 자동차 경주 횟수는 사용자에게 입력 받는다.
- [X] 자동차 경주는 사용자 입력이 없다면 5회로 고정하여 진행한다.
- [X] 전진하는 조건은 0에서 9 사이에서 무작위 값을 구한 후 무작위 값이 4 이상일 경우이다.
- [X] 자동차는 1회당 1칸씩 전진한다
- [X] 회차를 거듭할 때마다 자동차가 지나간 궤적을 출력한다(실행 예시 참고).
- [X] 사용자가 잘못된 입력 값을 작성한 경우 프로그램을 종료한다.
2 changes: 1 addition & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export default [
},
rules: {
eqeqeq: ["error", "always"],
"no-unused-vars": "error",
"no-unused-vars": "warn",
"no-var": "error",
"no-else-return": "error",
},
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"vite": "^6.1.0"
},
"jest": {
"verbose": true,
"transform": {
"^.+\\.js$": "babel-jest"
}
Expand Down
34 changes: 0 additions & 34 deletions src/Input.js

This file was deleted.

13 changes: 11 additions & 2 deletions src/domain/Car.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { ERROR_MESSAGES, NUMBERS } from "../utils/constants.js";

class Car {
static INVALID_CAR_NAME = "잘못된 자동차 이름입니다.";
#name;
#location = 0;

constructor(name) {
if (!this.isValidName(name)) {
throw new Error(Car.INVALID_CAR_NAME);
throw new Error(ERROR_MESSAGES.INVALID_CAR_NAME);
}
this.#name = name;
}
Expand All @@ -22,9 +23,17 @@ class Car {
this.#location += 1;
}

movingCondition() {
return this.getRandomNumber() >= NUMBERS.THRESHOLD;
Copy link

Choose a reason for hiding this comment

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

NUMBERS라는 이름과 접근자명은 다소 포괄적으로 보이는 데요,

NUMBERS.THRESHOLD - '차가 움직일 수 있는 조건'의 역할을 가지고 있는데, 이것이 이름에 내포되어 있지 않아 사용처에서 어떻게 사용되는지로만 유추할 수 있어 보여요.
NUMBERS.MAX_RANGE - 마찬가지로 어떠한 부분에 대한 max range인지 선언부만 봐서는 알기 힘들지 않을까요?

}

isValidName(name) {
return name.length >= 1 && name.length <= 5;
}

getRandomNumber() {
return Math.floor(Math.random() * NUMBERS.MAX_RANGE);
}
Comment on lines +34 to +36
Copy link

Choose a reason for hiding this comment

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

랜덤한 숫자를 얻는 동작은 특정 도메인과 연관되기보다 유틸의 성격이 더 강하지 않을까요? 따로 분리하는 게 어떨까요?

}

export default Car;
43 changes: 31 additions & 12 deletions src/domain/Race.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,47 @@ class Race {
rounds;
result = [];

constructor(cars) {
constructor(cars, rounds = Race.DEFAULT_ROUNDS) {
this.cars = cars;
this.rounds = Race.DEFAULT_ROUNDS;
this.rounds = rounds;
}

moveCars(cars) {
cars.map((car) => car.moveForward());
cars.map((car) => {
if (car.movingCondition()) {
car.moveForward();
}
});
}

// 자동차는 1회에 1칸씩 이동
start() {
for (let round = 1; round <= this.rounds; round++) {
this.moveCars(this.cars);

this.result.push({
round: round,
cars: this.cars.map((car) => ({
name: car.getName(),
location: car.getLocation(),
})),
});
this.recordRoundResult(round, this.cars);
Comment on lines 22 to +23
Copy link

Choose a reason for hiding this comment

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

moveCars, recordRoundResult에서도 this.rounds, this.cars에 접근할 수 있는데, 인자를 따로 받는 이유는 무엇일까요?

}
return this.result;
}

recordRoundResult(round, cars) {
this.result.push({
round: round,

cars: cars.map((car) => ({
name: car.getName(),
location: car.getLocation(),
})),
});
}

getWinners() {
const locations = this.cars.map((car) => car.getLocation());
const maxLocation = Math.max(...locations);

const winnersCar = this.cars.filter(
(car) => car.getLocation() === maxLocation
);

return winnersCar.map((car) => car.getName());
}
}

Expand Down
Loading