diff --git a/src/main/java/bowling/GameController.java b/src/main/java/bowling/GameController.java new file mode 100644 index 0000000000..7fc9932c01 --- /dev/null +++ b/src/main/java/bowling/GameController.java @@ -0,0 +1,24 @@ +package bowling; + +import bowling.domain.Frames; +import bowling.domain.Player; +import bowling.domain.ResultLines; +import bowling.view.InputView; +import bowling.view.OutputView; + +public class GameController { + + public static void main(String[] args) { + Frames frames = new Frames(); + Player player = InputView.getPlayer(); + + OutputView.printFrameResult(new ResultLines(player, frames.results())); + + while (!frames.gameFinished()) { + frames.bowl(InputView.getNextPin(frames.currentFrameNumber())); + + OutputView.printFrameResult(new ResultLines(player, frames.results())); + } + } + +} diff --git a/src/main/java/bowling/README.md b/src/main/java/bowling/README.md new file mode 100644 index 0000000000..51b282f147 --- /dev/null +++ b/src/main/java/bowling/README.md @@ -0,0 +1,84 @@ +# 볼링 2단계 기능 요구사항 +## Pin +* 볼링공을 굴렸을 때 쓰러뜨린 핀의 개수(amount)를 가짐 +* 현재 amount가 전체 핀의 개수와 일치하는지 확인하는 isMax() +* 자신이 가진 amount와 인자로 주어진 핀의 amount를 비교해 핀 전체를 쓰려뜨렸는지 확인하는 isClear() + +## Player + +## Frame +* NormalFrame와 FinalFrame에서 구현되는 인터페이스 +* bowl(Pin pin), isFinished(), nextFrame()을 갖는다 + +## NormalFrame +* 상태 값(Status)을 가짐 +* bowl() 메소드로 게임을 진행 -> Status에게 위임 +* isFinished()로 게임이 종료되었는지 확인 -> Status 클래스에게 판단을 위임 +* nextFrame() 으로 다음 frame을 반환 + * 게임이 종료되었을 경우 새로운 프레임, 게임이 진행중이면 기존 프레임 반환 + +## FinalFrame +* 상태 값(Status)의 리스트(statuses)를 가짐 +* 10번째 프레임에서는 Strike/Spare 시 총 3번의 bowl() 기회가 주어짐 + * bowlCount 변수를 가져 예외 로직 처리 +* ifFinished()에서 bowlCount가 3이면, 무조건 true + * bowlCount가 2면, Miss가 있을 경우에만 true + * 나머지(bawlCount <= 1 || Miss가 없는 BowlCount == 2) 는 false +* nextFrame()은 Exception 발생 (마지막 프레임) + +## Frames +* Frame의 일급컬렉션 (볼링 게임이 진행되는 동안 최대 10개의 Frame을 관리) +* bowl() 메소드로 게임을 진행 -> Frame에게 위임 +* 전체 볼링 게임 종료 여부 gameFinished() + +## 상태 클래스 +* 필요에 따라 pins를 가져 게임 진행 상태에 대한 정보를 기록 +* isFinished() 메소드로 종료 여부를 판단 +* bowl() 메소드로 게임을 진행 + * 현재 상태와 인자(Pins)에 따라 다른 상태로 변환됨 + +* Status(interface) + * Running(abstract) + * Ready -> Frame의 최초 상태 + * FirstPin -> 한 번 bowl()을 했으나 아직 한 Frame이 종료되지 않은 상태 + * Finished(abstract) + * Miss + * Strike + * Spare + +--- +# 볼링 3단계 요구사항 +## 리팩토링 요구사항 +* NormalFrame, FinalFrame의 공통 로직을 AbstractFrame 으로 추출 + * Normal, Final에서 Abstract를 호출할 때 값을 다르게 주어 공통 로직을 활용 +## 기능 요구사항 +* 점수 계산 로직 구현하기 + * **스트라이크**면 이후 2번의 투구 점수를 합산해야 하며, + * **스페어**면 이후 1번의 투구 점수를 합산해야 한다. + +### Frame +* 점수 계산은 Frame에서 수행 + * 점수 계산을 하기 위해선 **이후 Frame**에 접근 가능해야 한다 + * `Frame next` -> private로 두어도 같은 클래스 내이므로 계속해서 다음 프레임에 접근 가능 + * Score에서 점수 가능 여부를 판단하고, 다음 Frame까지를 고려해 점수를 계산해야 한다면 다음 Frame에게 점수 계산을 위임한다. + * 현재 프레임의 점수가 계산가능하면, 바로 계산해서 반환 + * 현재 프레임의 점수 계산 `calculateScore()` + * 점수가 계산 가능하지 않다면, 위임 + * 이전 프레임으로부터 위임받은 **이전 프레임의** 점수 계산 `calculateLastFrameScore()` -> 먼저 `state.calculateScore(Score lastScore)` 호출 + * -> next Frame의 상태가 `Finished`면, 1. 점수를 반환하거나 2. 계속해서 next Frame으로 요청 전달 + * -> 상태가 `Finished`가 아니면서 현재 Frame의 `calculateScore(Score lastScore)` 결과가 계산 불가능하다면 `Optional.empty()` 반환 (Finished가 아니어도 우선 현재 상태를 Score에 반영해봐야함에 주의) + +### Score +* `현재까지의 점수 - currentScore`, `점수 계산 가능까지 남은 bowl() 횟수 - leftBowlCount`를 저장 +* `bowl(int pinCount)` -> pinCount만큼 currentScore을 증가시키고, leftBowlCount를 감소시킴 +* `canCalculateScore()` -> 현 상태에서 점수 계산 가능 여부를 반환 +* `score()` -> 점수가 계산 가능할 경우, 현재 점수를 반환 + +### State <-> Score +* Score 생성 시, bowl() 시 State에 저장된 `fallenPin` 값이 필요 + * 현재는 외부에서 접근 불가 (toString()으로 출력에서만 사용) +* State에 `score()` 과 같은 메소드를 만들어 State의 상태를 활용해 Score를 생성할 수 있도록 구현해보기 +* 또, Score의 상태를 업데이트해 새로운 Score를 반환하는 메소드도 제공하기 - `calculateScore(Score lastScore)` + * 예를 들어, Miss, Spare는 Score의 bowl()을 2회 호출해야 하고 + * Strike, FirstPin은 Score의 bowl()을 1회 호출해야 함 + diff --git a/src/main/java/bowling/domain/AbstractFrame.java b/src/main/java/bowling/domain/AbstractFrame.java new file mode 100644 index 0000000000..915d767fa1 --- /dev/null +++ b/src/main/java/bowling/domain/AbstractFrame.java @@ -0,0 +1,61 @@ +package bowling.domain; + +import bowling.domain.state.Ready; +import bowling.domain.state.Status; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public abstract class AbstractFrame implements Frame { + + protected final List statuses; + protected final int frameNumber; + + AbstractFrame(int maxBowlCount, int frameNumber) { + statuses = new ArrayList<>(maxBowlCount); + statuses.add(new Ready()); + + this.frameNumber = frameNumber; + } + + @Override + public void bowl(Pin pin) { + Status currentStatus = currentStatus(); + statuses.remove(currentStatusIndex()); + + statuses.add(currentStatus.bowl(pin)); + } + + @Override + public int frameNumber() { + return frameNumber; + } + + protected void assertFinished() { + if (isFinished()) { + throw new IllegalStateException("현재 프레임에서는 더 이상 게임을 진행할 수 없습니다."); + } + } + + protected Status currentStatus() { + return statuses.get(currentStatusIndex()); + } + + protected int currentStatusIndex() { + return statuses.size() - 1; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + AbstractFrame that = (AbstractFrame) o; + return frameNumber == that.frameNumber && Objects.equals(statuses, that.statuses); + } + + @Override + public int hashCode() { + return Objects.hash(statuses, frameNumber); + } +} diff --git a/src/main/java/bowling/domain/FinalFrame.java b/src/main/java/bowling/domain/FinalFrame.java new file mode 100644 index 0000000000..bd0f13ad5c --- /dev/null +++ b/src/main/java/bowling/domain/FinalFrame.java @@ -0,0 +1,103 @@ +package bowling.domain; + +import bowling.domain.state.Miss; +import bowling.domain.state.Ready; +import bowling.domain.state.Status; + +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +public class FinalFrame extends AbstractFrame { + + public static final int MAX_BOWLCOUNT = 3; + public static final int FRAMENUMBER = 10; + public static final String MESSAGE_DELIMITER = "|"; + + private int bowlCount = 0; + + FinalFrame(int maxBowlCount, int frameNumber) { + super(maxBowlCount, frameNumber); + } + + public static Frame init() { + return new FinalFrame(MAX_BOWLCOUNT, FRAMENUMBER); + } + + @Override + public void bowl(Pin pin) { + assertFinished(); + + if (currentStatus().isFinished()) { + statuses.add(new Ready()); + } + + super.bowl(pin); + bowlCount++; + } + + @Override + public boolean isFinished() { + if (bowlCount == MAX_BOWLCOUNT) { + return true; + } + if (bowlCount == MAX_BOWLCOUNT - 1) { + return statuses.stream() + .anyMatch(status -> status instanceof Miss); + } + return false; + } + + @Override + public Frame nextFrame() { + throw new IllegalStateException("마지막 프레임입니다. 다음 프레임을 생성할 수 없습니다."); + } + + @Override + public Optional calculateScore() { + if (!isFinished()) { + return Optional.empty(); + } + + Score score = statuses.get(0).score(); + for (Status state : statuses.subList(1, statuses.size())) { + score = state.calculateScore(score); + } + return Optional.of(score.score()); + } + + @Override + public Optional calculateLastFrameScore(Score lastScore) { + Score newScore = lastScore; + for (Status state : statuses) { + newScore = state.calculateScore(newScore); + } + + if (newScore.canCalculateScore()) { + return Optional.of(newScore.score()); + } + + return Optional.empty(); + } + + @Override + public String toString() { + return statuses.stream() + .map(Object::toString) + .collect(Collectors.joining(MESSAGE_DELIMITER)); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + FinalFrame that = (FinalFrame) o; + return bowlCount == that.bowlCount; + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), bowlCount); + } +} diff --git a/src/main/java/bowling/domain/Frame.java b/src/main/java/bowling/domain/Frame.java new file mode 100644 index 0000000000..94476c817b --- /dev/null +++ b/src/main/java/bowling/domain/Frame.java @@ -0,0 +1,19 @@ +package bowling.domain; + +import java.util.Optional; + +public interface Frame { + + void bowl(Pin pin); + + boolean isFinished(); + + Frame nextFrame(); + + int frameNumber(); + + Optional calculateScore(); + + Optional calculateLastFrameScore(Score lastScore); + +} diff --git a/src/main/java/bowling/domain/Frames.java b/src/main/java/bowling/domain/Frames.java new file mode 100644 index 0000000000..0c96fba4f0 --- /dev/null +++ b/src/main/java/bowling/domain/Frames.java @@ -0,0 +1,61 @@ +package bowling.domain; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class Frames { + + public static final int MIN_FRAMENUMBER = 1; + public static final int MAX_FRAMENUMBER = 10; + + private final List frames; + + public Frames() { + this(initFrames()); + } + + public Frames(List frames) { + this.frames = frames; + } + + private static List initFrames() { + List frames = new ArrayList<>(); + frames.add(NormalFrame.init(1)); + return frames; + } + + public int currentFrameNumber() { + return currentFrame().frameNumber(); + } + + public void bowl(Pin pin) { + currentFrame().bowl(pin); + nextFrame(); + } + + private void nextFrame() { + if (frames.size() == MAX_FRAMENUMBER) { + return; + } + if (currentFrame().isFinished()) { + frames.add(currentFrame().nextFrame()); + } + } + + public boolean gameFinished() { + if (frames.size() < MAX_FRAMENUMBER) { + return false; + } + return currentFrame().isFinished(); + } + + private Frame currentFrame() { + return frames.get(frames.size() - 1); + } + + public List results() { + return Collections.unmodifiableList(frames); + } + +} diff --git a/src/main/java/bowling/domain/NormalFrame.java b/src/main/java/bowling/domain/NormalFrame.java new file mode 100644 index 0000000000..133286adf2 --- /dev/null +++ b/src/main/java/bowling/domain/NormalFrame.java @@ -0,0 +1,97 @@ +package bowling.domain; + +import java.util.Optional; + +public class NormalFrame extends AbstractFrame { + + public static final int MAX_BOWLCOUNT = 1; + public static final int MAX_FRAMENUMBER = 9; + + private Frame next; + + NormalFrame(int maxBowlCount, int frameNumber) { + super(maxBowlCount, frameNumber); + } + + public static Frame init(int frameNumber) { + validate(frameNumber); + return new NormalFrame(MAX_BOWLCOUNT, frameNumber); + } + + private static void validate(int frameNumber) { + if (frameNumber < Frames.MIN_FRAMENUMBER) { + throw new IllegalArgumentException("Frame은 1번부터 시작합니다."); + } + if (frameNumber > MAX_FRAMENUMBER) { + throw new IllegalArgumentException("NormalFrame은 9번까지만 존재합니다."); + } + } + + @Override + public void bowl(Pin pin) { + assertFinished(); + + super.bowl(pin); + } + + @Override + public Optional calculateScore() { + if (!isFinished()) { + return Optional.empty(); + } + + Score score = currentStatus().score(); + if (score.canCalculateScore()) { + return Optional.of(score.score()); + } + + if (next == null) { + return Optional.empty(); + } + return next.calculateLastFrameScore(score); + } + + public Optional calculateLastFrameScore(Score lastScore) { + Score newScore = currentStatus().calculateScore(lastScore); + if (newScore.canCalculateScore()) { + return Optional.of(newScore.score()); + } + + if (!isFinished()) { + return Optional.empty(); + } + + if (next == null) { + return Optional.empty(); + } + return next.calculateLastFrameScore(newScore); + } + + @Override + public boolean isFinished() { + return currentStatus().isFinished(); + } + + @Override + public Frame nextFrame() { + if (!isFinished()) { + return this; + } + if (frameNumber < MAX_FRAMENUMBER) { + Frame next = NormalFrame.init(frameNumber + 1); + this.next = next; + return next; + } + Frame next = FinalFrame.init(); + this.next = next; + return next; + } + + @Override + public String toString() { + return currentStatus().toString(); + } + + + +} diff --git a/src/main/java/bowling/domain/Pin.java b/src/main/java/bowling/domain/Pin.java new file mode 100644 index 0000000000..3a10ee6be2 --- /dev/null +++ b/src/main/java/bowling/domain/Pin.java @@ -0,0 +1,63 @@ +package bowling.domain; + +import java.util.Objects; + +public class Pin { + + public static final int MIN_AMOUNT = 0; + public static final int MAX_AMOUNT = 10; + public static final String GUTTER_MESSAGE = "-"; + + private final int amount; + + public Pin(int amount) { + validate(amount); + this.amount = amount; + } + + private void validate(int amount) { + if (amount < MIN_AMOUNT) { + throw new IllegalArgumentException("볼링 핀의 개수는 0개 이상이어야 합니다."); + } + if (amount > MAX_AMOUNT) { + throw new IllegalArgumentException("볼링 핀의 개수는 10개 이하여야 합니다."); + } + } + + public int amount() { + return amount; + } + + public boolean isMax() { + return amount == MAX_AMOUNT; + } + + public boolean isClear(Pin pin) { + int newAmount = amount + pin.amount; + if (newAmount > MAX_AMOUNT) { + throw new IllegalArgumentException("볼링 핀의 전체 개수는 10개 입니다."); + } + return newAmount == MAX_AMOUNT; + } + + @Override + public String toString() { + if (amount == MIN_AMOUNT) { + return GUTTER_MESSAGE; + } + return String.valueOf(amount); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Pin pin = (Pin) o; + return amount == pin.amount; + } + + @Override + public int hashCode() { + return Objects.hash(amount); + } +} diff --git a/src/main/java/bowling/domain/Player.java b/src/main/java/bowling/domain/Player.java new file mode 100644 index 0000000000..9ead22d596 --- /dev/null +++ b/src/main/java/bowling/domain/Player.java @@ -0,0 +1,24 @@ +package bowling.domain; + +public class Player { + + public static final int PLAYER_NAME_LENGTH = 3; + + private final String name; + + public Player(String name) { + validateName(name); + this.name = name; + } + + private void validateName(String name) { + if (name.length() != PLAYER_NAME_LENGTH) { + throw new IllegalArgumentException("플레이어의 이름은 3글자여야 합니다."); + } + } + + public String name() { + return name; + } + +} diff --git a/src/main/java/bowling/domain/ResultLines.java b/src/main/java/bowling/domain/ResultLines.java new file mode 100644 index 0000000000..4e54fee0fb --- /dev/null +++ b/src/main/java/bowling/domain/ResultLines.java @@ -0,0 +1,71 @@ +package bowling.domain; + +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public class ResultLines { + + public static final String NAME_MESSAGE = " NAME "; + public static final String BLANK_MESSAGE = ""; + public static final String NUMBER_FORMAT = "%02d"; + public static final int RESULT_MAX_SIZE = Frames.MAX_FRAMENUMBER + 1; + + static final List FRAME_NUMBERS; + private final LinkedList frameResults; + private final LinkedList frameScores; + + static { + FRAME_NUMBERS = new ArrayList<>(); + FRAME_NUMBERS.add(NAME_MESSAGE); + FRAME_NUMBERS.addAll(IntStream.rangeClosed(Frames.MIN_FRAMENUMBER, Frames.MAX_FRAMENUMBER) + .mapToObj(s -> String.format(NUMBER_FORMAT, s)) + .map(String::valueOf) + .collect(Collectors.toList())); + } + + public ResultLines(Player player, List frames) { + frameResults = createFrameResults(player, frames); + frameScores = createFrameScores(frames); + } + + private LinkedList createFrameScores(List frames) { + LinkedList frameScores = frames.stream() + .map(Frame::calculateScore) + .filter(Optional::isPresent) + .map(Optional::get) + .map(String::valueOf) + .collect(Collectors.toCollection(LinkedList::new)); + frameScores.addFirst(BLANK_MESSAGE); + + return makeResultsSizeMax(frameScores, frameScores.size()); + } + + private LinkedList createFrameResults(Player player, List frames) { + LinkedList frameResults = frames.stream() + .map(Object::toString) + .collect(Collectors.toCollection(LinkedList::new)); + frameResults.addFirst(player.name()); + + return makeResultsSizeMax(frameResults, frameResults.size()); + } + + private LinkedList makeResultsSizeMax(LinkedList results, int currentResultsSize) { + for (int i = 0; i < (RESULT_MAX_SIZE - currentResultsSize); i++) { + results.addLast(BLANK_MESSAGE); + } + return results; + } + + public List frameNumbers() { + return Collections.unmodifiableList(FRAME_NUMBERS); + } + + public List frameResults() { + return Collections.unmodifiableList(frameResults); + } + + public List frameScores() { + return Collections.unmodifiableList(frameScores); + } +} diff --git a/src/main/java/bowling/domain/Score.java b/src/main/java/bowling/domain/Score.java new file mode 100644 index 0000000000..74139303d6 --- /dev/null +++ b/src/main/java/bowling/domain/Score.java @@ -0,0 +1,69 @@ +package bowling.domain; + +import java.util.Objects; + +public class Score { + + public static final int MAX_INITIAL_SCORE = 10; + public static final int STRIKE_LEFT_BOWL_COUNT = 2; + public static final int SPARE_LEFT_BOWL_COUNT = 1; + public static final int MISS_LEFT_BOWL_COUNT = 0; + + private final int currentScore; + private final int leftBowlCount; + + public Score(int initialScore, int leftBowlCount) { + validateInitialScore(initialScore); + this.currentScore = initialScore; + this.leftBowlCount = leftBowlCount; + } + + private static void validateInitialScore(int initialScore) { + if (initialScore < 0) { + throw new IllegalArgumentException("점수는 0점보다 작을 수 없습니다."); + } + } + + public static Score ofStrike() { + return new Score(MAX_INITIAL_SCORE, STRIKE_LEFT_BOWL_COUNT); + } + + public static Score ofSpare() { + return new Score(MAX_INITIAL_SCORE, SPARE_LEFT_BOWL_COUNT); + } + + public static Score ofMiss(int currentScore) { + return new Score(currentScore, MISS_LEFT_BOWL_COUNT); + } + + public Score bowl(int fallenPinCount) { + if (leftBowlCount == 0) { + return this; + } + return new Score(currentScore + fallenPinCount, leftBowlCount - 1); + } + + public boolean canCalculateScore() { + return leftBowlCount == 0; + } + + public int score() { + if (canCalculateScore()) { + return currentScore; + } + throw new IllegalStateException("현재는 점수를 계산할 수 없는 상태입니다!"); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Score score = (Score) o; + return currentScore == score.currentScore && leftBowlCount == score.leftBowlCount; + } + + @Override + public int hashCode() { + return Objects.hash(currentScore, leftBowlCount); + } +} diff --git a/src/main/java/bowling/domain/state/Finished.java b/src/main/java/bowling/domain/state/Finished.java new file mode 100644 index 0000000000..522bf68862 --- /dev/null +++ b/src/main/java/bowling/domain/state/Finished.java @@ -0,0 +1,17 @@ +package bowling.domain.state; + +import bowling.domain.Pin; + +abstract public class Finished implements Status { + + @Override + public boolean isFinished() { + return true; + } + + @Override + public Status bowl(Pin pin) { + throw new IllegalStateException("현재 상태에서는 더 이상 게임을 진행할 수 없습니다."); + } + +} diff --git a/src/main/java/bowling/domain/state/FirstPin.java b/src/main/java/bowling/domain/state/FirstPin.java new file mode 100644 index 0000000000..dfea2177d6 --- /dev/null +++ b/src/main/java/bowling/domain/state/FirstPin.java @@ -0,0 +1,45 @@ +package bowling.domain.state; + +import bowling.domain.Pin; +import bowling.domain.Score; + +import java.util.Objects; + +public class FirstPin extends Running { + + private final Pin firstPin; + + public FirstPin(Pin firstPin) { + this.firstPin = firstPin; + } + + @Override + public Status bowl(Pin secondPin) { + if (firstPin.isClear(secondPin)) { + return new Spare(firstPin); + } + return new Miss(firstPin, secondPin); + } + + @Override + public Score calculateScore(Score lastScore) { + return lastScore.bowl(firstPin.amount()); + } + + @Override + public String toString() { + return firstPin.toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null) return false; + return getClass() == o.getClass(); + } + + @Override + public int hashCode() { + return Objects.hash(); + } +} diff --git a/src/main/java/bowling/domain/state/Miss.java b/src/main/java/bowling/domain/state/Miss.java new file mode 100644 index 0000000000..152e0f5298 --- /dev/null +++ b/src/main/java/bowling/domain/state/Miss.java @@ -0,0 +1,49 @@ +package bowling.domain.state; + +import bowling.domain.Pin; +import bowling.domain.Score; + +import java.util.Objects; + +public class Miss extends Finished { + + public static final String MISS_MESSAGE = "|"; + + private final Pin firstPin; + private final Pin secondPin; + + public Miss(Pin firstPin, Pin secondPin) { + this.firstPin = firstPin; + this.secondPin = secondPin; + } + + @Override + public Score score() { + return Score.ofMiss(firstPin.amount() + secondPin.amount()); + } + + @Override + public Score calculateScore(Score lastScore) { + return lastScore + .bowl(firstPin.amount()) + .bowl(secondPin.amount()); + } + + @Override + public String toString() { + return firstPin + MISS_MESSAGE + secondPin; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Miss miss = (Miss) o; + return Objects.equals(firstPin, miss.firstPin) && Objects.equals(secondPin, miss.secondPin); + } + + @Override + public int hashCode() { + return Objects.hash(firstPin, secondPin); + } +} diff --git a/src/main/java/bowling/domain/state/Ready.java b/src/main/java/bowling/domain/state/Ready.java new file mode 100644 index 0000000000..9349c0e87b --- /dev/null +++ b/src/main/java/bowling/domain/state/Ready.java @@ -0,0 +1,41 @@ +package bowling.domain.state; + +import bowling.domain.Pin; +import bowling.domain.Score; + +import java.util.Objects; + +public class Ready extends Running { + + public static final String READY_MESSAGE = ""; + + @Override + public Status bowl(Pin pin) { + if (pin.isMax()) { + return new Strike(); + } + return new FirstPin(pin); + } + + @Override + public Score calculateScore(Score lastScore) { + return lastScore; + } + + @Override + public String toString() { + return READY_MESSAGE; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null) return false; + return getClass() == o.getClass(); + } + + @Override + public int hashCode() { + return Objects.hash(); + } +} diff --git a/src/main/java/bowling/domain/state/Running.java b/src/main/java/bowling/domain/state/Running.java new file mode 100644 index 0000000000..0eaef1e2a0 --- /dev/null +++ b/src/main/java/bowling/domain/state/Running.java @@ -0,0 +1,19 @@ +package bowling.domain.state; + +import bowling.domain.Score; + +abstract public class Running implements Status { + + public static final String CANNOT_CALCULATE_SCORE_ERROR_MESSAGE = "현재 상태에서는 점수를 계산할 수 없습니다."; + + @Override + public Score score() { + throw new IllegalStateException(CANNOT_CALCULATE_SCORE_ERROR_MESSAGE); + } + + @Override + public boolean isFinished() { + return false; + } + +} diff --git a/src/main/java/bowling/domain/state/Spare.java b/src/main/java/bowling/domain/state/Spare.java new file mode 100644 index 0000000000..982c67202e --- /dev/null +++ b/src/main/java/bowling/domain/state/Spare.java @@ -0,0 +1,47 @@ +package bowling.domain.state; + +import bowling.domain.Pin; +import bowling.domain.Score; + +import java.util.Objects; + +public class Spare extends Finished { + + public static final String SPARE_MESSAGE = "|/"; + + private final Pin firstPin; + + public Spare(Pin firstPin) { + this.firstPin = firstPin; + } + + @Override + public Score score() { + return Score.ofSpare(); + } + + @Override + public Score calculateScore(Score lastScore) { + return lastScore + .bowl(firstPin.amount()) + .bowl(Pin.MAX_AMOUNT - firstPin.amount()); + } + + @Override + public String toString() { + return firstPin + SPARE_MESSAGE; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Spare spare = (Spare) o; + return Objects.equals(firstPin, spare.firstPin); + } + + @Override + public int hashCode() { + return Objects.hash(firstPin); + } +} diff --git a/src/main/java/bowling/domain/state/Status.java b/src/main/java/bowling/domain/state/Status.java new file mode 100644 index 0000000000..1f207dc21e --- /dev/null +++ b/src/main/java/bowling/domain/state/Status.java @@ -0,0 +1,16 @@ +package bowling.domain.state; + +import bowling.domain.Pin; +import bowling.domain.Score; + +public interface Status { + + boolean isFinished(); + + Status bowl(Pin pin); + + Score score(); + + Score calculateScore(Score lastScore); + +} diff --git a/src/main/java/bowling/domain/state/Strike.java b/src/main/java/bowling/domain/state/Strike.java new file mode 100644 index 0000000000..4b1fdfd051 --- /dev/null +++ b/src/main/java/bowling/domain/state/Strike.java @@ -0,0 +1,41 @@ +package bowling.domain.state; + +import bowling.domain.Pin; +import bowling.domain.Score; + +import java.util.Objects; + +public class Strike extends Finished { + + public static final String STRIKE_MESSAGE = "X"; + + public Strike() { + } + + @Override + public Score score() { + return Score.ofStrike(); + } + + @Override + public Score calculateScore(Score lastScore) { + return lastScore.bowl(Pin.MAX_AMOUNT); + } + + @Override + public String toString() { + return STRIKE_MESSAGE; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null) return false; + return getClass() == o.getClass(); + } + + @Override + public int hashCode() { + return Objects.hash(); + } +} diff --git a/src/main/java/bowling/view/InputView.java b/src/main/java/bowling/view/InputView.java new file mode 100644 index 0000000000..aa485af2ac --- /dev/null +++ b/src/main/java/bowling/view/InputView.java @@ -0,0 +1,22 @@ +package bowling.view; + +import bowling.domain.Pin; +import bowling.domain.Player; + +import java.util.Scanner; + +public class InputView { + + private static final Scanner sc = new Scanner(System.in); + + public static Player getPlayer() { + System.out.print("플레이어 이름은(3 english letters)?: "); + return new Player(sc.nextLine()); + } + + public static Pin getNextPin(int currentFrameNumber) { + System.out.print(currentFrameNumber + "프레임 투구 : "); + return new Pin(sc.nextInt()); + } + +} diff --git a/src/main/java/bowling/view/OutputView.java b/src/main/java/bowling/view/OutputView.java new file mode 100644 index 0000000000..0e33042830 --- /dev/null +++ b/src/main/java/bowling/view/OutputView.java @@ -0,0 +1,38 @@ +package bowling.view; + +import bowling.domain.ResultLines; + +import java.util.List; + +public class OutputView { + + public static final String FRAME_DELIMETER = "|"; + + public static void printFrameResult(ResultLines results) { + printResult(results.frameNumbers()); + System.out.println(); + printResult(results.frameResults()); + System.out.println(); + printResult(results.frameScores()); + + System.out.println(); + System.out.println(); + } + + private static void printResult(List results) { + printFrameDelimeter(); + for (String result : results) { + System.out.print(formatMessage(result)); + printFrameDelimeter(); + } + } + + private static void printFrameDelimeter() { + System.out.print(FRAME_DELIMETER); + } + + private static String formatMessage(String message) { + return String.format("%6s", String.format("%-4s", message)); + } + +} diff --git a/src/main/java/qna/README.md b/src/main/java/qna/README.md new file mode 100644 index 0000000000..eaae3e4804 --- /dev/null +++ b/src/main/java/qna/README.md @@ -0,0 +1,14 @@ +# QnA 서비스 리팩토링 요구사항 +- [x] `QnAService` 의 `게시글 작성자가 본인인지 검증` 로직을 `Question`으로 이동 + - [x] 검증 로직 테스트 +- [x] `Question` 의 `List`을 일급 콜렉션 `Answers`로 변환 + - [x] `Answers` 로 `QnAService`의 `다른 사람이 작성한 댓글이 있는지 검증` 로직을 이동 + - [x] 검증 로직 테스트 +- [x] `Answers` 의 검증 로직을 `Question`의 검증 로직에 병합 +- [x] `DeleteHistory`의 일급 콜렉션 `DeleteHistories`를 생성 + - [x] 테스트 작성 +- [x] `QnAService` 의 `Answer을 삭제` 하는 로직을 `Answers`로 이동 + - [x] 테스트 작성 +- [x] `QnAService` 의 `Question을 삭제` 하는 로직을 `Question`으로 이동 + - [x] 테스트 작성 +- [x] `Question` 의 `delete()` 로직에서 위 로직을 모두 수행하고, 질문과 답글이 모두 포함된 `DeleteHistories` 를 반환하도록 변경 \ No newline at end of file diff --git a/src/main/java/qna/domain/Answer.java b/src/main/java/qna/domain/Answer.java index 548b71ed71..375afe3fc3 100644 --- a/src/main/java/qna/domain/Answer.java +++ b/src/main/java/qna/domain/Answer.java @@ -1,9 +1,11 @@ package qna.domain; +import qna.CannotDeleteException; import qna.NotFoundException; import qna.UnAuthorizedException; import javax.persistence.*; +import java.time.LocalDateTime; @Entity public class Answer extends AbstractEntity { @@ -43,19 +45,10 @@ public Answer(Long id, User writer, Question question, String contents) { this.contents = contents; } - public Answer setDeleted(boolean deleted) { - this.deleted = deleted; - return this; - } - public boolean isDeleted() { return deleted; } - public boolean isOwner(User writer) { - return this.writer.equals(writer); - } - public User getWriter() { return writer; } @@ -68,6 +61,21 @@ public void toQuestion(Question question) { this.question = question; } + public DeleteHistories delete(DeleteHistories deleteHistories, LocalDateTime dateTime) { + deleted = true; + return deleteHistories.add(new DeleteHistory(ContentType.ANSWER, getId(), writer, dateTime)); + } + + void validateOwner(User loginUser) throws CannotDeleteException { + if (!isOwner(loginUser)) { + throw new CannotDeleteException("다른 사람이 쓴 답변이 있어 삭제할 수 없습니다."); + } + } + + private boolean isOwner(User writer) { + return this.writer.equals(writer); + } + @Override public String toString() { return "Answer [id=" + getId() + ", writer=" + writer + ", contents=" + contents + "]"; diff --git a/src/main/java/qna/domain/Answers.java b/src/main/java/qna/domain/Answers.java new file mode 100644 index 0000000000..edf6c2ed79 --- /dev/null +++ b/src/main/java/qna/domain/Answers.java @@ -0,0 +1,46 @@ +package qna.domain; + +import org.hibernate.annotations.Where; +import qna.CannotDeleteException; + +import javax.persistence.CascadeType; +import javax.persistence.Embeddable; +import javax.persistence.OneToMany; +import javax.persistence.OrderBy; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@Embeddable +public class Answers { + + @OneToMany(mappedBy = "question", cascade = CascadeType.ALL) + @Where(clause = "deleted = false") + @OrderBy("id ASC") + private final List answers = new ArrayList<>(); + + public Answers() { + } + + public void add(Answer answer) { + answers.add(answer); + } + + public DeleteHistories deleteAll(DeleteHistories deleteHistories, LocalDateTime dateTime) { + for (Answer answer : answers) { + deleteHistories = answer.delete(deleteHistories, dateTime); + } + return deleteHistories; + } + + public void validateOwner(User loginUser) throws CannotDeleteException { + for (Answer answer : answers) { + answer.validateOwner(loginUser); + } + } + + public List answers() { + return Collections.unmodifiableList(answers); + } +} diff --git a/src/main/java/qna/domain/DeleteHistories.java b/src/main/java/qna/domain/DeleteHistories.java new file mode 100644 index 0000000000..89c7309adf --- /dev/null +++ b/src/main/java/qna/domain/DeleteHistories.java @@ -0,0 +1,28 @@ +package qna.domain; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class DeleteHistories { + private final List deleteHistories; + + public DeleteHistories() { + this.deleteHistories = new ArrayList<>(); + } + + public DeleteHistories(List deleteHistories) { + this.deleteHistories = new ArrayList<>(deleteHistories); + } + + public DeleteHistories add(DeleteHistory deleteHistory) { + List newDeleteHistories = new ArrayList<>(deleteHistories); + newDeleteHistories.add(deleteHistory); + + return new DeleteHistories(newDeleteHistories); + } + + public List histories() { + return Collections.unmodifiableList(deleteHistories); + } +} diff --git a/src/main/java/qna/domain/Question.java b/src/main/java/qna/domain/Question.java index 1e8bb11251..83e1678ace 100644 --- a/src/main/java/qna/domain/Question.java +++ b/src/main/java/qna/domain/Question.java @@ -1,10 +1,9 @@ package qna.domain; -import org.hibernate.annotations.Where; +import qna.CannotDeleteException; import javax.persistence.*; -import java.util.ArrayList; -import java.util.List; +import java.time.LocalDateTime; @Entity public class Question extends AbstractEntity { @@ -18,10 +17,8 @@ public class Question extends AbstractEntity { @JoinColumn(foreignKey = @ForeignKey(name = "fk_question_writer")) private User writer; - @OneToMany(mappedBy = "question", cascade = CascadeType.ALL) - @Where(clause = "deleted = false") - @OrderBy("id ASC") - private List answers = new ArrayList<>(); + @Embedded + private Answers answers = new Answers(); private boolean deleted = false; @@ -39,24 +36,6 @@ public Question(long id, String title, String contents) { this.contents = contents; } - public String getTitle() { - return title; - } - - public Question setTitle(String title) { - this.title = title; - return this; - } - - public String getContents() { - return contents; - } - - public Question setContents(String contents) { - this.contents = contents; - return this; - } - public User getWriter() { return writer; } @@ -71,21 +50,36 @@ public void addAnswer(Answer answer) { answers.add(answer); } - public boolean isOwner(User loginUser) { - return writer.equals(loginUser); + public boolean isDeleted() { + return deleted; } - public Question setDeleted(boolean deleted) { - this.deleted = deleted; - return this; + public Answers getAnswers() { + return answers; } - public boolean isDeleted() { - return deleted; + public DeleteHistories delete(User loginUser, LocalDateTime dateTime) throws CannotDeleteException { + validateOwner(loginUser); + + DeleteHistories deleteHistories = delete(new DeleteHistories(), dateTime); + deleteHistories = answers.deleteAll(deleteHistories, dateTime); + return deleteHistories; } - public List getAnswers() { - return answers; + DeleteHistories delete(DeleteHistories deleteHistories, LocalDateTime dateTime) { + deleted = true; + return deleteHistories.add(new DeleteHistory(ContentType.QUESTION, getId(), writer, dateTime)); + } + + void validateOwner(User loginUser) throws CannotDeleteException { + if (!isOwner(loginUser)) { + throw new CannotDeleteException("질문을 삭제할 권한이 없습니다."); + } + answers.validateOwner(loginUser); + } + + private boolean isOwner(User writer) { + return this.writer.equals(writer); } @Override diff --git a/src/main/java/qna/service/QnAService.java b/src/main/java/qna/service/QnAService.java index 66821cd9c2..7cdf36a8b8 100644 --- a/src/main/java/qna/service/QnAService.java +++ b/src/main/java/qna/service/QnAService.java @@ -10,8 +10,6 @@ import javax.annotation.Resource; import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; @Service("qnaService") public class QnAService { @@ -35,24 +33,8 @@ public Question findQuestionById(Long id) { @Transactional public void deleteQuestion(User loginUser, long questionId) throws CannotDeleteException { Question question = findQuestionById(questionId); - if (!question.isOwner(loginUser)) { - throw new CannotDeleteException("질문을 삭제할 권한이 없습니다."); - } - - List answers = question.getAnswers(); - for (Answer answer : answers) { - if (!answer.isOwner(loginUser)) { - throw new CannotDeleteException("다른 사람이 쓴 답변이 있어 삭제할 수 없습니다."); - } - } - - List deleteHistories = new ArrayList<>(); - question.setDeleted(true); - deleteHistories.add(new DeleteHistory(ContentType.QUESTION, questionId, question.getWriter(), LocalDateTime.now())); - for (Answer answer : answers) { - answer.setDeleted(true); - deleteHistories.add(new DeleteHistory(ContentType.ANSWER, answer.getId(), answer.getWriter(), LocalDateTime.now())); - } - deleteHistoryService.saveAll(deleteHistories); + DeleteHistories deleteHistories = question.delete(loginUser, LocalDateTime.now()); + + deleteHistoryService.saveAll(deleteHistories.histories()); } } diff --git a/src/test/java/bowling/domain/FinalFrameTest.java b/src/test/java/bowling/domain/FinalFrameTest.java new file mode 100644 index 0000000000..a83fb2f3a9 --- /dev/null +++ b/src/test/java/bowling/domain/FinalFrameTest.java @@ -0,0 +1,156 @@ +package bowling.domain; + +import bowling.domain.state.Miss; +import bowling.domain.state.Spare; +import org.junit.jupiter.api.Test; + +import java.util.Optional; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + +class FinalFrameTest { + + @Test + void FinalFrame은_bowlCount_3_frameNumber_10으로_init_된다() { + assertThat(FinalFrame.init()).isEqualTo(new FinalFrame(3, 10)); + } + + @Test + void 프레임_번호_얻기() { + assertThat(FinalFrame.init().frameNumber()).isEqualTo(10); + } + + @Test + void 다음_프레임_얻기() { + assertThatThrownBy(() -> FinalFrame.init().nextFrame()) + .isInstanceOf(IllegalStateException.class); + } + + @Test + void 게임진행_1회() { + Frame frame = FinalFrame.init(); + frame.bowl(new Pin(5)); + + assertThat(frame.isFinished()).isFalse(); + assertThat(frame.toString()).isEqualTo("5"); + } + + @Test + void 게임진행_2회_strike_spare가_없는_경우() { + Frame frame = FinalFrame.init(); + frame.bowl(new Pin(5)); + frame.bowl(new Pin(1)); + + assertThat(frame.isFinished()).isTrue(); + assertThat(frame.toString()).isEqualTo("5" + Miss.MISS_MESSAGE + "1"); + } + + @Test + void 게임진행_2회_strike() { + Frame frame = FinalFrame.init(); + frame.bowl(new Pin(10)); + frame.bowl(new Pin(1)); + + assertThat(frame.isFinished()).isFalse(); + assertThat(frame.toString()).isEqualTo("X" + FinalFrame.MESSAGE_DELIMITER + "1"); + } + + @Test + void 게임진행_2회_spare() { + Frame frame = FinalFrame.init(); + frame.bowl(new Pin(9)); + frame.bowl(new Pin(1)); + + assertThat(frame.isFinished()).isFalse(); + assertThat(frame.toString()).isEqualTo("9" + Spare.SPARE_MESSAGE); + } + + @Test + void 게임진행_3회() { + Frame frame = FinalFrame.init(); + frame.bowl(new Pin(9)); + frame.bowl(new Pin(1)); + frame.bowl(new Pin(0)); + + assertThat(frame.isFinished()).isTrue(); + assertThat(frame.toString()).isEqualTo("9" + + Spare.SPARE_MESSAGE + + FinalFrame.MESSAGE_DELIMITER + + Pin.GUTTER_MESSAGE); + } + + @Test + void 점수계산_계산불가_Spare가_있는_경우() { + Frame frame = FinalFrame.init(); + assertThat(frame.calculateScore()).isEqualTo(Optional.empty()); + + frame.bowl(new Pin(1)); + assertThat(frame.calculateScore()).isEqualTo(Optional.empty()); + + frame.bowl(new Pin(9)); + assertThat(frame.calculateScore()).isEqualTo(Optional.empty()); + } + + @Test + void 점수계산_계산불가_Strike가_있는_경우() { + Frame frame = FinalFrame.init(); + assertThat(frame.calculateScore()).isEqualTo(Optional.empty()); + + frame.bowl(new Pin(10)); + assertThat(frame.calculateScore()).isEqualTo(Optional.empty()); + + frame.bowl(new Pin(0)); + assertThat(frame.calculateScore()).isEqualTo(Optional.empty()); + } + + @Test + void 점수계산_계산가능_Strike나_Spare가_없는_경우() { + Frame frame = FinalFrame.init(); + frame.bowl(new Pin(5)); + frame.bowl(new Pin(4)); + + assertThat(frame.calculateScore()).isEqualTo(Optional.of(9)); + } + + @Test + void 점수계산_계산가능_Strike가_있는_경우() { + Frame frame = FinalFrame.init(); + frame.bowl(new Pin(10)); + frame.bowl(new Pin(5)); + frame.bowl(new Pin(4)); + + assertThat(frame.calculateScore()).isEqualTo(Optional.of(19)); + } + + @Test + void 점수계산_계산가능_Spare가_있는_경우() { + Frame frame = FinalFrame.init(); + frame.bowl(new Pin(5)); + frame.bowl(new Pin(5)); + frame.bowl(new Pin(4)); + + assertThat(frame.calculateScore()).isEqualTo(Optional.of(14)); + } + + @Test + void NormalFrame에서_위임된_점수계산_Strike() { + Frame frame = FinalFrame.init(); + assertThat(frame.calculateLastFrameScore(Score.ofStrike())).isEqualTo(Optional.empty()); + + frame.bowl(new Pin(5)); + assertThat(frame.calculateLastFrameScore(Score.ofStrike())).isEqualTo(Optional.empty()); + + frame.bowl(new Pin(5)); + assertThat(frame.calculateLastFrameScore(Score.ofStrike())).isEqualTo(Optional.of(20)); + } + + @Test + void NormalFrame에서_위임된_점수계산_Spare() { + Frame frame = FinalFrame.init(); + assertThat(frame.calculateLastFrameScore(Score.ofSpare())).isEqualTo(Optional.empty()); + + frame.bowl(new Pin(5)); + assertThat(frame.calculateLastFrameScore(Score.ofSpare())).isEqualTo(Optional.of(15)); + } +} \ No newline at end of file diff --git a/src/test/java/bowling/domain/FrameFactory.java b/src/test/java/bowling/domain/FrameFactory.java new file mode 100644 index 0000000000..235b4c45e9 --- /dev/null +++ b/src/test/java/bowling/domain/FrameFactory.java @@ -0,0 +1,12 @@ +package bowling.domain; + +public class FrameFactory { + + public static Frame frameImplProvider(int frameNumber) { + if (frameNumber == 10) { + return FinalFrame.init(); + } + return NormalFrame.init(frameNumber); + } + +} diff --git a/src/test/java/bowling/domain/FramesTest.java b/src/test/java/bowling/domain/FramesTest.java new file mode 100644 index 0000000000..504e4d9564 --- /dev/null +++ b/src/test/java/bowling/domain/FramesTest.java @@ -0,0 +1,45 @@ +package bowling.domain; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class FramesTest { + + public static final int GAME_FINISHING_BOWL_TRIES = 12; + + @Test + void 게임종료여부_종료되지않은_상태() { + assertThat(framesProvider(1).gameFinished()).isFalse(); + } + + @Test + void 게임종료여부_종료된_상태() { + assertThat(framesProvider(GAME_FINISHING_BOWL_TRIES).gameFinished()).isTrue(); + } + + @Test + void 게임진행_종료되지않은_상태() { + Frames frames = new Frames(); + assertThat(frames.results()).hasSize(1); + + frames.bowl(new Pin(Pin.MAX_AMOUNT)); + assertThat(frames.results()).hasSize(2); + } + + @Test + void 게임진행_종료된_상태() { + Frames frames = framesProvider(GAME_FINISHING_BOWL_TRIES); + assertThatThrownBy(() -> frames.bowl(new Pin(Pin.MAX_AMOUNT))) + .isInstanceOf(IllegalStateException.class); + } + + private Frames framesProvider(int bowlTries) { + Frames frames = new Frames(); + for (int i = 0; i < bowlTries; i++) { + frames.bowl(new Pin(Pin.MAX_AMOUNT)); + } + return frames; + } +} diff --git a/src/test/java/bowling/domain/NormalFrameTest.java b/src/test/java/bowling/domain/NormalFrameTest.java new file mode 100644 index 0000000000..727398ec88 --- /dev/null +++ b/src/test/java/bowling/domain/NormalFrameTest.java @@ -0,0 +1,167 @@ +package bowling.domain; + +import bowling.domain.state.Miss; +import bowling.domain.state.Spare; +import bowling.domain.state.Strike; +import org.junit.jupiter.api.Test; + +import java.util.Optional; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + +class NormalFrameTest { + + @Test + void NormalFrame은_maxBowlCount_1로_init_된다() { + assertThat(NormalFrame.init(1)).isEqualTo(new NormalFrame(1, 1)); + } + + @Test + void 생성_framenumber_1번_미만() { + assertThatThrownBy(() -> NormalFrame.init(0)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void 생성_framenumber_9번_초과() { + assertThatThrownBy(() -> NormalFrame.init(10)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void firstPin() { + Frame frame = NormalFrame.init(1); + frame.bowl(new Pin(5)); + + assertThat(frame.isFinished()).isFalse(); + assertThat(frame.toString()).isEqualTo("5"); + } + + @Test + void strike() { + Frame frame = NormalFrame.init(1); + frame.bowl(new Pin(10)); + + assertThat(frame.isFinished()).isTrue(); + assertThat(frame.toString()).isEqualTo(Strike.STRIKE_MESSAGE); + } + + @Test + void spare() { + Frame frame = NormalFrame.init(1); + frame.bowl(new Pin(4)); + frame.bowl(new Pin(6)); + + assertThat(frame.isFinished()).isTrue(); + assertThat(frame.toString()).isEqualTo("4" + Spare.SPARE_MESSAGE); + } + + @Test + void miss_non_gutter() { + Frame frame = NormalFrame.init(1); + frame.bowl(new Pin(4)); + frame.bowl(new Pin(5)); + + assertThat(frame.isFinished()).isTrue(); + assertThat(frame.toString()).isEqualTo("4" + Miss.MISS_MESSAGE + "5"); + } + + @Test + void miss_gutter() { + Frame frame = NormalFrame.init(1); + frame.bowl(new Pin(4)); + frame.bowl(new Pin(0)); + + assertThat(frame.isFinished()).isTrue(); + assertThat(frame.toString()).isEqualTo("4" + Miss.MISS_MESSAGE + Pin.GUTTER_MESSAGE); + } + + @Test + void 다음_프레임_얻기_현재_프레임이_종료되지않은_경우() { + Frame frame = NormalFrame.init(1); + frame.bowl(new Pin(5)); + + assertThat(frame.nextFrame().frameNumber()).isEqualTo(1); + assertThat(frame.nextFrame()).isSameAs(frame); + } + + @Test + void 다음_프레임_얻기_현재_프레임이_종료된_경우() { + Frame frame = NormalFrame.init(1); + frame.bowl(new Pin(10)); + + assertThat(frame.nextFrame().frameNumber()).isEqualTo(2); + } + + @Test + void 다음_프레임_얻기_NormalFrame() { + Frame frame = NormalFrame.init(8); + frame.bowl(new Pin(10)); + + assertThat(frame.nextFrame()).isInstanceOf(NormalFrame.class); + assertThat(frame.nextFrame().frameNumber()).isEqualTo(9); + } + + @Test + void 다음_프레임_얻기_FinalFrame() { + Frame frame = NormalFrame.init(9); + frame.bowl(new Pin(10)); + + assertThat(frame.nextFrame()).isInstanceOf(FinalFrame.class); + assertThat(frame.nextFrame().frameNumber()).isEqualTo(10); + } + + @Test + void 점수계산_계산불가_Running() { + Frame frame = NormalFrame.init(1); + assertThat(frame.calculateScore()).isEqualTo(Optional.empty()); + + frame.bowl(new Pin(5)); + assertThat(frame.calculateScore()).isEqualTo(Optional.empty()); + } + + @Test + void 점수계산_계산불가_Strike() { + Frame frame = NormalFrame.init(1); + frame.bowl(new Pin(10)); + + assertThat(frame.calculateScore()).isEqualTo(Optional.empty()); + } + + @Test + void 점수계산_계산불가_Spare() { + Frame frame = NormalFrame.init(1); + frame.bowl(new Pin(5)); + frame.bowl(new Pin(5)); + + assertThat(frame.calculateScore()).isEqualTo(Optional.empty()); + } + + @Test + void 점수계산_계산가능_Miss() { + Frame frame = NormalFrame.init(1); + frame.bowl(new Pin(5)); + frame.bowl(new Pin(4)); + + assertThat(frame.calculateScore()).isEqualTo(Optional.of(9)); + } + + @Test + void 점수계산_계산가능_Spare() { + Frame frame = NormalFrame.init(1); + frame.bowl(new Pin(5)); + + assertThat(frame.calculateLastFrameScore(Score.ofSpare())).isEqualTo(Optional.of(15)); + } + + @Test + void 점수계산_계산가능_Strike() { + Frame frame = NormalFrame.init(1); + frame.bowl(new Pin(5)); + assertThat(frame.calculateLastFrameScore(Score.ofStrike())).isEqualTo(Optional.empty()); + + frame.bowl(new Pin(3)); + assertThat(frame.calculateLastFrameScore(Score.ofStrike())).isEqualTo(Optional.of(18)); + } +} diff --git a/src/test/java/bowling/domain/PinTest.java b/src/test/java/bowling/domain/PinTest.java new file mode 100644 index 0000000000..cd333dbb93 --- /dev/null +++ b/src/test/java/bowling/domain/PinTest.java @@ -0,0 +1,48 @@ +package bowling.domain; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class PinTest { + + @Test + void 생성_0개_미만() { + assertThatThrownBy(() -> new Pin(-1)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void 생성_10개_초과() { + assertThatThrownBy(() -> new Pin(11)) + .isInstanceOf(IllegalArgumentException.class); + } + + @ParameterizedTest + @CsvSource(value = {"9,false", "10,true"}) + void 볼링_핀이_최대값을_가지는지_확인(int amount, boolean expected) { + assertThat(new Pin(amount).isMax()).isEqualTo(expected); + } + + @ParameterizedTest + @CsvSource(value = {"5,5,true", "0,10,true", "0,0,false", "0,9,false"}) + void 볼링_핀의_합계가_최대값이_되는지_확인(int firstPinAmount, int secondPinAmount, boolean expected) { + assertThat(new Pin(firstPinAmount).isClear(new Pin(secondPinAmount))).isEqualTo(expected); + } + + @Test + void 볼링_핀의_합계가_최대값을_초과하는_경우() { + assertThatThrownBy(() -> new Pin(5).isClear(new Pin(6))) + .isInstanceOf(IllegalArgumentException.class); + } + + @ParameterizedTest + @CsvSource(value = {"0, -", "1, 1", "10, 10"}) + void 볼링_핀_개수_toString(int amount, String expected) { + assertThat(new Pin(amount).toString()).isEqualTo(expected); + } + +} \ No newline at end of file diff --git a/src/test/java/bowling/domain/PlayerTest.java b/src/test/java/bowling/domain/PlayerTest.java new file mode 100644 index 0000000000..55619f4901 --- /dev/null +++ b/src/test/java/bowling/domain/PlayerTest.java @@ -0,0 +1,23 @@ +package bowling.domain; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class PlayerTest { + + @Test + void 생성_valid() { + assertThat(new Player("HSH").name()).isEqualTo("HSH"); + } + + @ParameterizedTest + @ValueSource(strings = {"abcd", "ab"}) + void 생성_invalid(String name) { + assertThatThrownBy(() -> new Player(name)) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/src/test/java/bowling/domain/ResultLinesTest.java b/src/test/java/bowling/domain/ResultLinesTest.java new file mode 100644 index 0000000000..caba5982b3 --- /dev/null +++ b/src/test/java/bowling/domain/ResultLinesTest.java @@ -0,0 +1,55 @@ +package bowling.domain; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class ResultLinesTest { + + public static final String PLAYER_NAME_HSH = "HSH"; + + @Test + void firstline_생성() { + assertThat(ResultLines.FRAME_NUMBERS.get(0)).isEqualTo(ResultLines.NAME_MESSAGE); + assertThat(ResultLines.FRAME_NUMBERS).hasSize(11); + } + + @ParameterizedTest + @ValueSource(ints = {0, 1, 9, 10}) + void frameResults_최대개수만큼_생성(int frameResultsLength) { + assertThat(resultsLinesProvider(PLAYER_NAME_HSH, frameResultsLength).frameResults()) + .hasSize(11); + } + + @ParameterizedTest + @ValueSource(ints = {0, 1, 9, 10}) + void frameScores_최대개수만큼_생성(int frameResultsLength) { + assertThat(resultsLinesProvider(PLAYER_NAME_HSH, frameResultsLength).frameScores()) + .hasSize(11); + } + + @Test + void player_이름이_결과리스트의_첫번째원소에_들어가야_한다() { + assertThat(resultsLinesProvider(PLAYER_NAME_HSH, 1).frameResults().get(0)) + .isEqualTo(PLAYER_NAME_HSH); + } + + private ResultLines resultsLinesProvider(String playerName, int frameResultsLength) { + if (frameResultsLength > Frames.MAX_FRAMENUMBER) { + throw new IllegalArgumentException("프레임은 최대 10개까지만 생성 가능합니다."); + } + + List frames = new ArrayList<>(); + for (int frameNumber = 1; frameNumber <= frameResultsLength; frameNumber++) { + frames.add(FrameFactory.frameImplProvider(frameNumber)); + } + + return new ResultLines(new Player(playerName), frames); + } + +} \ No newline at end of file diff --git a/src/test/java/bowling/domain/ScoreTest.java b/src/test/java/bowling/domain/ScoreTest.java new file mode 100644 index 0000000000..c7d44add4a --- /dev/null +++ b/src/test/java/bowling/domain/ScoreTest.java @@ -0,0 +1,61 @@ +package bowling.domain; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.*; + +class ScoreTest { + + @Test + void Strike_초기_Score_생성() { + assertThat(Score.ofStrike()).isEqualTo(new Score(10, 2)); + } + + @Test + void Spare_초기_Score_생성() { + assertThat(Score.ofSpare()).isEqualTo(new Score(10, 1)); + } + + @Test + void Miss_초기_Score_생성() { + assertThat(Score.ofMiss(5)).isEqualTo(new Score(5, 0)); + } + + @Test + void 생성_invalid() { + assertThatThrownBy(() -> new Score(-1, 0)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void 생성_valid() { + assertThatCode(() -> new Score(0, 0)).doesNotThrowAnyException(); + assertThat(new Score(10, 0).score()).isEqualTo(10); + } + + @Test + void 게임진행_계산불가() { + Score score = new Score(10, 2).bowl(10); + + assertThat(score).isEqualTo(new Score(20, 1)); + assertThat(score.canCalculateScore()).isFalse(); + assertThatThrownBy(score::score) + .isInstanceOf(IllegalStateException.class); + } + + @Test + void 게임진행_계산가능() { + Score score = new Score(10, 1).bowl(10); + + assertThat(score).isEqualTo(new Score(20, 0)); + assertThat(score.canCalculateScore()).isTrue(); + assertThat(score.score()).isEqualTo(20); + } + + @Test + void 게임진행_leftBowlCount_가_0인_경우() { + Score score = new Score(10, 0).bowl(10); + + assertThat(score).isEqualTo(new Score(10, 0)); + } +} \ No newline at end of file diff --git a/src/test/java/bowling/domain/state/FirstPinTest.java b/src/test/java/bowling/domain/state/FirstPinTest.java new file mode 100644 index 0000000000..8b4f010f7d --- /dev/null +++ b/src/test/java/bowling/domain/state/FirstPinTest.java @@ -0,0 +1,50 @@ +package bowling.domain.state; + +import bowling.domain.Pin; +import bowling.domain.Score; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + + +class FirstPinTest { + + @Test + void 종료_여부_판단() { + assertThat(new FirstPin(new Pin(0)).isFinished()).isFalse(); + } + + @ParameterizedTest + @CsvSource(value = {"0, 10", "1, 9"}) + void 게임진행_Spare(int firstPinAmount, int secondPinAmount) { + assertThat(new FirstPin(new Pin(firstPinAmount)).bowl(new Pin(secondPinAmount))) + .isInstanceOf(Spare.class); + } + + @ParameterizedTest + @CsvSource(value = {"0, 9", "0, 0"}) + void 게임진행_Miss(int firstPinAmount, int secondPinAmount) { + assertThat(new FirstPin(new Pin(firstPinAmount)).bowl(new Pin(secondPinAmount))) + .isInstanceOf(Miss.class); + } + + @Test + void 메시지_출력() { + assertThat(new FirstPin(new Pin(5)).toString()).isEqualTo("5"); + } + + @Test + void Score_생성() { + assertThatThrownBy(() -> new FirstPin(new Pin(5)).score()) + .isInstanceOf(IllegalStateException.class); + } + + @Test + void Score_계산() { + assertThat(new FirstPin(new Pin(5)).calculateScore(new Score(10, 1))) + .isEqualTo(new Score(15, 0)); + } +} \ No newline at end of file diff --git a/src/test/java/bowling/domain/state/MissTest.java b/src/test/java/bowling/domain/state/MissTest.java new file mode 100644 index 0000000000..2d1f0fead0 --- /dev/null +++ b/src/test/java/bowling/domain/state/MissTest.java @@ -0,0 +1,44 @@ +package bowling.domain.state; + +import bowling.domain.Pin; +import bowling.domain.Score; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + +public class MissTest { + + @Test + void 종료_여부_판단() { + assertThat(new Miss(new Pin(0), new Pin(0)).isFinished()).isTrue(); + } + + @Test + void 게임진행_예외발생() { + assertThatThrownBy(() -> new Miss(new Pin(0), new Pin(0)).bowl(new Pin(0))) + .isInstanceOf(IllegalStateException.class); + } + + @ParameterizedTest + @CsvSource(value = {"0, 0, -, -", "5, 4, 5, 4", "5, 0, 5, -"}) + void 메시지_출력(int firstPinAmount, int secondPinAmount, String expectedFirst, String expectedSecond) { + assertThat(new Miss(new Pin(firstPinAmount), new Pin(secondPinAmount)).toString()) + .isEqualTo(expectedFirst + Miss.MISS_MESSAGE + expectedSecond); + } + + @Test + void Score_생성() { + assertThat(new Miss(new Pin(1), new Pin(2)).score()) + .isEqualTo(new Score(3, 0)); + } + + @Test + void Score_계산() { + assertThat(new Miss(new Pin(1), new Pin(2)).calculateScore(new Score(10, 2))) + .isEqualTo(new Score(13, 0)); + } + +} diff --git a/src/test/java/bowling/domain/state/ReadyTest.java b/src/test/java/bowling/domain/state/ReadyTest.java new file mode 100644 index 0000000000..71af1f205b --- /dev/null +++ b/src/test/java/bowling/domain/state/ReadyTest.java @@ -0,0 +1,48 @@ +package bowling.domain.state; + +import bowling.domain.Pin; +import bowling.domain.Score; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class ReadyTest { + + @Test + void 종료_여부_판단() { + assertThat(new Ready().isFinished()).isFalse(); + } + + @Test + void 게임진행_Strike() { + assertThat(new Ready().bowl(new Pin(10))) + .isInstanceOf(Strike.class); + } + + @ParameterizedTest + @ValueSource(ints = {0, 1, 9}) + void 게임진행_FirstPin(int amount) { + assertThat(new Ready().bowl(new Pin(amount))) + .isInstanceOf(FirstPin.class); + } + + @Test + void 메시지_출력() { + assertThat(new Ready().toString()).isEqualTo(Ready.READY_MESSAGE); + } + + @Test + void Score_생성() { + assertThatThrownBy(() -> new Ready().score()) + .isInstanceOf(IllegalStateException.class); + } + + @Test + void Score_계산() { + assertThatThrownBy(() -> new Ready().calculateScore(Score.ofStrike())) + .isInstanceOf(IllegalStateException.class); + } +} \ No newline at end of file diff --git a/src/test/java/bowling/domain/state/SpareTest.java b/src/test/java/bowling/domain/state/SpareTest.java new file mode 100644 index 0000000000..307f214902 --- /dev/null +++ b/src/test/java/bowling/domain/state/SpareTest.java @@ -0,0 +1,39 @@ +package bowling.domain.state; + +import bowling.domain.Pin; +import bowling.domain.Score; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + +public class SpareTest { + + @Test + void 종료_여부_판단() { + assertThat(new Spare(new Pin(0)).isFinished()).isTrue(); + } + + @Test + void 게임진행_예외발생() { + assertThatThrownBy(() -> new Spare(new Pin(0)).bowl(new Pin(0))) + .isInstanceOf(IllegalStateException.class); + } + + @Test + void 메시지_출력() { + assertThat(new Spare(new Pin(5)).toString()).isEqualTo("5" + Spare.SPARE_MESSAGE); + } + + @Test + void Score_생성() { + assertThat(new Spare(new Pin(5)).score()).isEqualTo(Score.ofSpare()); + } + + @Test + void Score_계산() { + assertThat(new Spare(new Pin(5)).calculateScore(new Score(10, 2))) + .isEqualTo(new Score(20, 0)); + } + +} diff --git a/src/test/java/bowling/domain/state/StrikeTest.java b/src/test/java/bowling/domain/state/StrikeTest.java new file mode 100644 index 0000000000..4d63722594 --- /dev/null +++ b/src/test/java/bowling/domain/state/StrikeTest.java @@ -0,0 +1,38 @@ +package bowling.domain.state; + +import bowling.domain.Pin; +import bowling.domain.Score; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + +public class StrikeTest { + + @Test + void 종료_여부_판단() { + assertThat(new Strike().isFinished()).isTrue(); + } + + @Test + void 게임진행_예외발생() { + assertThatThrownBy(() -> new Strike().bowl(new Pin(0))) + .isInstanceOf(IllegalStateException.class); + } + + @Test + void 메시지_출력() { + assertThat(new Strike().toString()).isEqualTo(Strike.STRIKE_MESSAGE); + } + + @Test + void Score_생성() { + assertThat(new Strike().score()).isEqualTo(Score.ofStrike()); + } + + @Test + void Score_계산() { + assertThat(new Strike().calculateScore(new Score(10, 1))) + .isEqualTo(new Score(20, 0)); + } +} diff --git a/src/test/java/qna/domain/AnswerTest.java b/src/test/java/qna/domain/AnswerTest.java index d858181e31..54c53ccd84 100644 --- a/src/test/java/qna/domain/AnswerTest.java +++ b/src/test/java/qna/domain/AnswerTest.java @@ -1,6 +1,43 @@ package qna.domain; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import qna.CannotDeleteException; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.*; + public class AnswerTest { - public static final Answer A1 = new Answer(UserTest.JAVAJIGI, QuestionTest.Q1, "Answers Contents1"); - public static final Answer A2 = new Answer(UserTest.SANJIGI, QuestionTest.Q1, "Answers Contents2"); + public static Answer A1 = new Answer(UserTest.JAVAJIGI, QuestionTest.Q1, "Answers Contents1"); + public static Answer A2 = new Answer(UserTest.SANJIGI, QuestionTest.Q1, "Answers Contents2"); + + @AfterEach + void tearDown() { + A1 = new Answer(UserTest.JAVAJIGI, QuestionTest.Q1, "Answers Contents1"); + A2 = new Answer(UserTest.SANJIGI, QuestionTest.Q1, "Answers Contents2"); + } + + @Test + void validation_작성자_본인_여부_정상() { + assertThatCode(() -> A1.validateOwner(UserTest.JAVAJIGI)) + .doesNotThrowAnyException(); + } + + @Test + void validation_작성자_본인_여부_오류() { + assertThatThrownBy(() -> A1.validateOwner(UserTest.SANJIGI)) + .isInstanceOf(CannotDeleteException.class); + } + + @Test + void 댓글_삭제() { + LocalDateTime localDateTime = LocalDateTime.now(); + DeleteHistories deleteHistories = A1.delete(new DeleteHistories(), localDateTime); + + assertThat(A1.isDeleted()).isTrue(); + assertThat(deleteHistories.histories()).containsExactly( + new DeleteHistory(ContentType.ANSWER, A1.getId(), A1.getWriter(), localDateTime) + ); + } } diff --git a/src/test/java/qna/domain/AnswersTest.java b/src/test/java/qna/domain/AnswersTest.java new file mode 100644 index 0000000000..c89f5db075 --- /dev/null +++ b/src/test/java/qna/domain/AnswersTest.java @@ -0,0 +1,41 @@ +package qna.domain; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThat; +import static qna.domain.AnswerTest.A1; +import static qna.domain.AnswerTest.A2; + +class AnswersTest { + + private Answers answers; + + @BeforeEach + void setUp() { + answers = new Answers(); + answers.add(A1); + answers.add(A2); + } + + @Test + void add() { + assertThat(answers.answers()).containsExactly(A1, A2); + } + + @Test + void 답글_전쳬_삭제() { + LocalDateTime localDateTime = LocalDateTime.now(); + DeleteHistories deleteHistories = answers.deleteAll(new DeleteHistories(), LocalDateTime.now()); + + assertThat(A1.isDeleted()).isTrue(); + assertThat(A2.isDeleted()).isTrue(); + assertThat(deleteHistories.histories()).containsExactly( + new DeleteHistory(ContentType.ANSWER, A1.getId(), A1.getWriter(), localDateTime), + new DeleteHistory(ContentType.ANSWER, A2.getId(), A2.getWriter(), localDateTime) + ); + } + +} \ No newline at end of file diff --git a/src/test/java/qna/domain/DeleteHistoriesTest.java b/src/test/java/qna/domain/DeleteHistoriesTest.java new file mode 100644 index 0000000000..af4bb35791 --- /dev/null +++ b/src/test/java/qna/domain/DeleteHistoriesTest.java @@ -0,0 +1,27 @@ +package qna.domain; + +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThat; + +class DeleteHistoriesTest { + + private static final DeleteHistory DH1 = new DeleteHistory( + ContentType.QUESTION, + QuestionTest.Q1.getId(), + QuestionTest.Q1.getWriter(), + LocalDateTime.now() + ); + + @Test + void 원소추가시_새로운_객체_반환() { + DeleteHistories deleteHistories1 = new DeleteHistories(); + DeleteHistories deleteHistories2 = deleteHistories1.add(DH1); + + assertThat(deleteHistories1.histories()).hasSize(0); + assertThat(deleteHistories2.histories()).hasSize(1); + assertThat(deleteHistories1).isNotSameAs(deleteHistories2); + } +} \ No newline at end of file diff --git a/src/test/java/qna/domain/QuestionTest.java b/src/test/java/qna/domain/QuestionTest.java index b48c9a2209..60f9dfd281 100644 --- a/src/test/java/qna/domain/QuestionTest.java +++ b/src/test/java/qna/domain/QuestionTest.java @@ -1,6 +1,60 @@ package qna.domain; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import qna.CannotDeleteException; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.*; +import static qna.domain.AnswerTest.A1; +import static qna.domain.AnswerTest.A2; + public class QuestionTest { - public static final Question Q1 = new Question("title1", "contents1").writeBy(UserTest.JAVAJIGI); - public static final Question Q2 = new Question("title2", "contents2").writeBy(UserTest.SANJIGI); + public static Question Q1 = new Question("title1", "contents1").writeBy(UserTest.JAVAJIGI); + + @AfterEach + void tearDown() { + Q1 = new Question("title1", "contents1").writeBy(UserTest.JAVAJIGI); + } + + @Test + void validation_작성자_본인_여부_정상() { + assertThatCode(() -> Q1.validateOwner(UserTest.JAVAJIGI)) + .doesNotThrowAnyException(); + } + + @Test + void validation_작성자_본인_여부_오류() { + assertThatThrownBy(() -> Q1.validateOwner(UserTest.SANJIGI)) + .isInstanceOf(CannotDeleteException.class); + } + + @Test + void 게시물_삭제() { + DeleteHistories deleteHistories = Q1.delete(new DeleteHistories(), LocalDateTime.now()); + + assertThat(Q1.isDeleted()).isTrue(); + assertThat(deleteHistories.histories()).hasSize(1); + } + + @Test + void 게시물_및_댓글_삭졔_정상() throws CannotDeleteException { + Q1.addAnswer(A1); + LocalDateTime localDateTime = LocalDateTime.now(); + DeleteHistories deleteHistories = Q1.delete(UserTest.JAVAJIGI, localDateTime); + + assertThat(deleteHistories.histories()).containsExactly( + new DeleteHistory(ContentType.QUESTION, Q1.getId(), Q1.getWriter(), localDateTime), + new DeleteHistory(ContentType.ANSWER, A1.getId(), A1.getWriter(), localDateTime) + ); + } + + @Test + void 게시물_및_댓글_삭졔_오류() { + Q1.addAnswer(A2); + + assertThatThrownBy(() -> Q1.delete(UserTest.JAVAJIGI, LocalDateTime.now())) + .isInstanceOf(CannotDeleteException.class); + } }