[워밍업 클럽 4기 - 백엔드] 4주차 발자국
3개월 전
개인일정이 있어 늦었지만 작성해봅니다!
과제 피드백 반영
일반과제
Day 18 미션 중 DRY가 무조건 안 좋은 것은 아니며 댓글에 대한 테스트이기 때문에 맥락이해를 떨어뜨릴 수 있는
게시글, 사용자 관련 로직은 BeforeEach로 분리해도 이해에 지장을 주지않는다는 것을 이해하고 적용.
지뢰찾기
https://github.com/jsween5723/readable-code/pull/1
일반 생성자를 꼭 private으로 반영 해야하는 것은 아니다.
Board(int rowCount, int columnCount, int mineCount) {
this.mineCount = mineCount;
this.cells = new Cell[rowCount][columnCount];
this.rowCount = rowCount;
this.columnCount = columnCount;
initNormalCell();
initMineCell();
}
테스트 코드 작성시 맥락을 이해하기 어렵다면 단락을 나눈다
@Test
@DisplayName("깃발이 달렸거나 이미 열린 셀은 열리지 않는다.")
void openCellWithCoordinatesn() {
//given
BoardConfig config = MineSweeperGameLevel.BEGINNER.boardConfig;
Board board = spy(Board.withConfig(config));
Coordinate flaggedCoordinate = new Coordinate(1, 1);
Cell flagCell = spy(Cell.normalCell());
flagCell.toggleFlag();
when(board.get(flaggedCoordinate)).thenReturn(flagCell);
Coordinate openedCoordinate = new Coordinate(1, 2);
Cell openedCell = spy(Cell.normalCell());
openedCell.open();
when(board.get(openedCoordinate)).thenReturn(openedCell);
//when
board.open(flaggedCoordinate);
board.open(openedCoordinate);
//then
verify(flagCell, never()).open();
verify(openedCell, times(1)).open();
}
너무 많지 않다면 Nested는 오히려 테스트 코드의 가독성을 떨어뜨린다.
if 블록과 비슷하다.
추가적인 리팩토링
public 메소드를 상위로 올리고 주요도 순으로 재배치함.
public void open(Coordinate coordinate) {}
public void toggleFlag(Coordinate coordinate) {}
public boolean isCleared() {}
public boolean isMineOpened() {}
Config을 필드로 들고있지 않고 값만 필드에 대입하도록 변경
public class Board {
public static final int[][] DELTAS = new int[][]{{-1, -1}, {-1, 0}, {-1, +1}, {0, -1}, {0, +1},
{1, -1}, {1, 0}, {1, +1}};
private final Cell[][] cells;
private final int rowCount;
private final int columnCount;
private final int mineCount;
private final BoardStringGenerator stringGenerator = new BoardStringGenerator();
}
String 생성관련 로직 BoardStringGenerater에 역할 분리 및 추상화 레벨 균일화
class BoardStringGenerator {
public String generate() {
StringBuilder sb = new StringBuilder();
appendColumnDefinition(sb);
for (int row = 0; row < rowCount; row++) {
appendRowNumber(sb, row);
for (int column = 0; column < columnCount; column++) {
Coordinate coordinate = new Coordinate(row, column);
appendCell(sb, coordinate);
}
sb.append("\n");
}
return sb.toString();
}
// private 생략
}
BoardStringGenerator 테스트 작성
@Test
@DisplayName("toString 테스트")
void generateTest() {
//given
int rowCount = 4;
int columnCount = 4;
int mineCount = 3;
Board board = spy(new Board(rowCount, columnCount, mineCount));
for (int row = 0; row < rowCount; row++) {
for (int col = 0; col < columnCount; col++) {
Coordinate coordinate = new Coordinate(row, col);
when(board.get(coordinate)).thenReturn(switch (col % 4) {
case 0 -> generateFlagCell();
case 1 -> generateOpenedCell(row);
case 2 -> Cell.normalCell();
case 3 -> generateOpenedMineCell();
default -> throw new IllegalStateException("Unexpected value: " + col);
});
}
}
//when
String boardString = board.new BoardStringGenerator().generate();
//then CLOSED("□"), FLAGGED("⚑"), NORMAL_OPENED("■"), MINE_OPENED("☼");
assertThatCharSequence(boardString).isEqualToIgnoringWhitespace("""
A B C D
1 ⚑ ☼ □ ☼
2 ⚑ 1 □ ☼
3 ⚑ ■ □ ☼
4 ⚑ ■ □ ☼
""");
}
//private 생략
복잡한 초기화 로직 추상화레벨 균일화
class A {
@Test
@DisplayName("지뢰가 아니고 주변에 지뢰가 없다면, 깃발을 제외하고 함께 연다.")
void openNormalCell() {
//given
Coordinate targetCoordinate = new Coordinate(1, 1);
Cell[] cells = createCellsForAround();
Coordinate[] coordinates = createCoordinatesForAround(targetCoordinate);
for (int i = 0; i < coordinates.length; i++) {
when(board.get(coordinates[i])).thenReturn(cells[i]);
}
//when
board.open(targetCoordinate);
//then
for (Cell cell : cells) {
verify(cell, atLeastOnce()).open();
}
}
private Coordinate[] createCoordinatesForAround(Coordinate targetCoordinate) {
int[][] deltas = {{0, -1}, {0, +1}, {1, -1}, {1, 0},
{1, +1}, {-1, -1}, {-1, 0}, {-1, 1}};
return Arrays.stream(deltas)
.map(delta -> new Coordinate(targetCoordinate.row() + delta[0],
targetCoordinate.column() + delta[1]))
.toArray(Coordinate[]::new);
}
private Cell[] createCellsForAround() {
return IntStream.range(0, 8).mapToObj((i) -> spy(Cell.normalCell()))
.toArray(Cell[]::new);
}
}
강의에서 언급된 ParameterizedTest 적용
static Stream<Arguments> serveIncorrectCoordinates() {
int rowCount = 4;
int columnCount = 4;
return Stream.of(
Arguments.of(rowCount + 1,
columnCount),
Arguments.of(rowCount,
columnCount + 1),
Arguments.of(-1, columnCount),
Arguments.of(rowCount, -1)
);
}
@DisplayName("좌표가 범위밖이면 예외를 던진다.")
@ParameterizedTest()
@MethodSource("serveIncorrectCoordinates")
void configCoordinateBalidate2(int row, int column) {
//given
Coordinate targetCoordinate = new Coordinate(row, column);
//when
//then
assertThatThrownBy(() -> board.validateCoordinates(targetCoordinate))
.isInstanceOf(IllegalArgumentException.class);
}
가독성을 해치는 경우 ParameterizedTest를 활용해 해결했다.
이외 일반과제 피드백도 지뢰찾기에 반영
private Board board;
private int rowCount;
private int columnCount;
private int mineCount;
@BeforeEach
void setUp() {
rowCount = 4;
columnCount = 4;
mineCount = 5;
board = new Board(4, 4, 5);
}
댓글을 작성해보세요.