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

[2단계 - 웹 자동차 경주] 도기(김동호) 미션 제출합니다. #104

Merged
merged 61 commits into from
Apr 19, 2023

Conversation

kdkdhoho
Copy link

@kdkdhoho kdkdhoho commented Apr 15, 2023

안녕하세요 조앤! 좋은 하루 보내고 계신가요?
생각보다 2단계가 빠르게 마무리되었네요.
그럼 이번에도 잘 부탁드립니다 🙇‍♂️

궁금한 점

인텔리제이 설정에서 build tools를 gradle에서 intellj로 변경하고 WebApplication을 실행하면 에러가 발생합니다.
에러 메시지로는, PlayRequest에 기본 생성자가 존재하지 않아 발생하는 에러로 판단됩니다.
다시 build tools를 gradle로 변경하면 문제 없이 잘 실행됩니다.

PlayRequest 내의 필드들을 모두 불변으로 하고 싶은데, 방법이 없나요??
무엇이 문제인지 관련된 키워드를 알려주시면 찾아보겠습니다 !

kdkdhoho added 30 commits April 14, 2023 13:35
- 자동차가 최소 2개 이상 생성되었는가?
- 자동차 이름에 중복이 존재하지 않는가?
Copy link

@seovalue seovalue left a comment

Choose a reason for hiding this comment

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

안녕하세요, 도기! 조앤이에요ㅎㅎ
리팩터링까지 깔끔하게 해주셔서 이정도면 머지해도 될 것 같아요ㅎㅎ
하지만~ 이전 코멘트에 대해 답변도 몇가지 달아보았고, 추가로 몇가지 질문도 드려봤는데 답변이 궁금해서 마지막 Request Changes 남겨요! 👍

@@ -18,6 +19,6 @@ public void run() {

racingGame.playRacing(cars, trialCount);

outputView.printResult(cars.getCars(), cars.winnerNames());
outputView.printResult(PlayResponse.from(cars));

Choose a reason for hiding this comment

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

리뷰 반영 👍

public static final String TABLE_NAME = "game";
public static final String KEY_COLUMN_NAME = "id";

private final JdbcTemplate jdbcTemplate;
private final SimpleJdbcInsert insertActor;

@Autowired

Choose a reason for hiding this comment

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

@Autowired를 붙여주신 이유가 있나요? 붙였을 때와 그렇지 않을 때의 동작이 달라지나요?

Copy link
Author

Choose a reason for hiding this comment

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

개인적인 취향으로써, 암시적으로 되어있더라도 명시적으로 적어주는 것을 선호하다 보니 붙이게 되었습니다.

현재는 생성자가 1개이므로 붙였을 때와 붙이지 않았을 때 동작은 같습니다.
하지만 생성자가 2개 이상인 경우에는 하나의 메서드에만 @Autowired를 붙여주어 의존성 주입 할 방식을 선택해야 합니다.

혹시 실무에서는 생성자가 1개일 때 @Autowired를 붙여주나요? 혹은 그렇지 않나요??

Choose a reason for hiding this comment

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

오홍!ㅎㅎ 그건 팀의 컨벤션에 따라 다를 것 같아요!

@@ -31,9 +35,9 @@ public long insert(final int trial_count) {
return insertActor.executeAndReturnKey(params).longValue();
}

public int countAll() {
public Optional<Integer> countAll() {

Choose a reason for hiding this comment

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

count는 Int를 반환하는 값인데 optional을 반환하기로 결정하신 이유가 궁금해요. 실제 쿼리에서도 count() 함수를 실행했을 때 조건에 해당하는 값이 없다면 null이 아닌 0을 뱉는데, 동일하게 동작하도록 하는건 어떨까요?

Copy link
Author

Choose a reason for hiding this comment

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

인텔리제이에서 Integer에서 언박싱할 때 NPE가 발생할 수 있다고 경고하기에 Optional로 반환했습니다.
하지만 실제로 null이 아닌 0을 반환하는 것을 확인했고, 쿼리 실행 결과를 바로 return 하도록 수정하였습니다!

public PlayResponse(final List<String> winners, final List<CarDto> racingCars) {
this.winners = convertToString(winners);
private PlayResponse(final List<String> winnerNames, final List<CarDto> racingCars) {
this.winners = String.join(",", winnerNames);

Choose a reason for hiding this comment

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

개인적으로 다시 메서드를 풀어서 적어주신 게 좀 더 직관적인 것 같네요 👍

return new PlayResponse(winners, carDtos);
}

private static void decideWinner(final List<String> winners, final RecordDto recordDto) {

Choose a reason for hiding this comment

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

내부에서 파라미터로 들어온 리스트의 값을 변경시켜주는 것보다 recordDto.getPlayerName()을 반환해 외부에서 그 값을 컬렉션에 넣어주는 건 어떨까요? (의견이 궁금한 거예요!)

Copy link
Author

Choose a reason for hiding this comment

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

죄송하지만 이해가 잘 되지 않습니다 😭

Choose a reason for hiding this comment

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

decideWinner 메서드의 역할이 애매해 보여서 남겼는데 제가 헷갈리게 코멘트를 남겼군요!!

    public static PlayResponse from(final List<RecordDto> recordDtos) {
        List<String> winnerNames = recordDtos.stream()
            .filter(RecordDto::isWinner)
            .map(RecordDto::getPlayerName)
            .collect(Collectors.toList());
        
        List<CarDto> carDtos = recordDtos.stream()
            .map(CarDto::from)
            .collect(Collectors.toList());

        return new PlayResponse(winnerNames, carDtos);
    }

Copy link
Author

Choose a reason for hiding this comment

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

헉.. 이렇게 좋은 방법이 있었네요 😅

import java.util.Arrays;
import java.util.List;

@TestPropertySource(locations = "/application.properties")
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
@Transactional
@TestInstance(TestInstance.Lifecycle.PER_CLASS)

Choose a reason for hiding this comment

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

스크린샷 2023-04-17 오후 8 32 43
ㅠㅠ

@PostMapping("/plays")
public PlayResponse plays(@RequestBody final PlayRequest playRequest) {
return gameService.playRacing(playRequest.getNames(), playRequest.getCount());
@PostMapping(value = "/plays", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
Copy link

@seovalue seovalue Apr 17, 2023

Choose a reason for hiding this comment

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

도기! 너무너무 잘 구현해주셨는데 이번에 딱 POST /playsGET /plays에 대해 RestAssured 혹은 mockMvc를 활용해 E2E 테스트를 도전해보시는 것은 어떨까요?

@kdkdhoho
Copy link
Author

안녕하세요 조앤! 2단계 첫 리뷰요청 보내고 꽤 많은 날이 지났네요.
사실 2단계 빠르게 끝날 줄 알았는데, E2E 테스트라는 난관에 봉착과 동시에 여러 일정이 몰려서 이제야 재리뷰 요청 보냅니다.
E2E 테스트.... 너.......무............ 어렵네요 😭

이번에 리뷰 반영하면서 궁금했던 점들 적어보겠습니다.

1. E2E 테스트의 목적과 범위?

이번에 처음으로 E2E 테스트를 접했고 도전해봤습니다.
처음엔 String requestJsonString responseJson을 만들어, MockMvc를 통해 Input과 Output을 검증하려고 했습니다. 하지만 자동차 경주를 하는 로직에 랜덤성이 존재하기에 이를 제어하고자 테스트용 Service와 RacingGame을 추가하려고 했습니다.
하지만 마음처럼 잘 되지 않아 주변 크루들에게 물어보니, 대부분 응답되는 json의 key값이 2개인지? key에 'winners', 'racingCars'가 포함되는지만 테스트하더라구요. 여기서 많은 고민이 생겼습니다.

E2E 테스트의 목적은 무엇인가요? 그리고 범위가 어떻게 되나요?

2. 테스트 시, 자동으로 테이블 초기화해주는 방법은 없나요?

이번에 Dao 테스트와 Controller 테스트를 하면서 테스트를 하나씩 실행하면 통과되지만, 전체 테스트를 실행하면 많이 깨지더라구요.
주로 깨지는 이유가 Game 테이블의 id가 AUTO_INCREMENT로 되어 있어서 @Transactional로 데이터를 돌려놓아도, id 값은 다시 낮은 값부터 들어가는 것이 아닌, 이전 id의 +1 값이 들어가서 제가 기대하던 경우와 달라지기 때문이었습니다.
그래서 beforeEach에서 항상 모든 테이블을 동일한 환경으로 세팅해주어야겠다고 생각했습니다.

그런데 현재는 코드에서 쿼리문을 실행시키는 방식으로 하였는데 코드가 굉장히 길어질뿐더러 유지보수 측면에서도 매우 안좋다고 생각합니다. 혹시 스프링에게 schema.sql에 있는 쿼리문을 실행시켜달라고 하는 방법이 없을까요??

Copy link

@seovalue seovalue left a comment

Choose a reason for hiding this comment

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

안녕하세요, 도기! 조앤이에요ㅎㅎ
controller 테스트까지 작성해주시느라 고생 많으셨어요~ 앞서 말씀드렸듯이 요구사항은 모두 만족하기에, 이제 머지해도 될 것 같아요!! 👏
다만 남겨주신 질문에 대해 제 의견 남겨두었으니, 꼭 확인하시고 도기도 의견 남겨주세요! (댓글 남기시면 저(@seovalue) 멘션해주세요.. 그냥 댓글달면 알림이 안와서 확인이 안되더라구요 😢)
고생 많으셨어요!!

@@ -22,6 +22,7 @@ dependencies {

// test
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'io.rest-assured:rest-assured:4.4.0'

Choose a reason for hiding this comment

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

버전을 선택하는 기준이 무엇이었나요?

Copy link
Author

@kdkdhoho kdkdhoho Apr 20, 2023

Choose a reason for hiding this comment

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

어.. 솔직히 말씀드리면 기억이 나지 않습니다 😭 아마 RestAssured 사용법 관련 포스팅을 보다가 무의식적으로 설정한 것 같아요.

특별한 이유가 있는 게 아니면 따로 버전을 명시하지 않는 것이 좋은가요?

Choose a reason for hiding this comment

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

아뇨! 버전 명시는 필요한데, 특정 버전을 선택하는 기준이 궁금했어요! 버전마다 제공되는 기준도 다르고 하니깐요~

@@ -17,16 +16,48 @@
@TestPropertySource(locations = "/application.properties")
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
@Transactional
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class GameDaoTest {

Choose a reason for hiding this comment

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

도기~ 2번째 질문에 대한 답 여기서 할게요!
@Sql 애노테이션에 대해 알아보시면 좋을 것 같아요ㅎㅎ

@TestPropertySource(locations = "/application.properties")
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
@Transactional
@Sql("/schema.sql")
class GameDaoTest {

    @Autowired private GameDao gameDao;
    @Autowired private JdbcTemplate jdbcTemplate;

    @BeforeEach
    void beforeEach() {
        initGameTable();
        initGameTableData();
    }
// 기존 코드
}

class WebControllerTest {

@Autowired
private MockMvc mockMvc;

Choose a reason for hiding this comment

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

우선 mockMvc를 사용하기로 선택하신 이유가 궁금해요. mockMvc는 RestAssured와 어떤 차이가 있나요?

  • 하지만 열심히 적용해주셨으니 참고 자료도 남겨요ㅎㅎ

[도기 질문] e2e 테스트의 목적은 무엇인가요? 범위가 어떻게 되나요?

앞으로 다양한 테스트를 할테니, 우선 참고 자료를 먼저 링크해요.
단위 테스트 vs 통합 테스트 vs 인수 테스트
인수 테스트와 E2E 테스트 차이

E2E 테스트의 목적은 테스트하고자 하는 기능이 필요로 하는 모든 구성 요소를 거쳐 최종적으로 우리가 원하는 결과를 반환하는지를 확인하는 것이에요. [목적]
즉, 지금 구조로 보자면 Controller - Service - Dao 까지를 거쳐 최종적으로 Controller 에서 원하는 json 응답을 뱉어내는지를 검증하는거죠. [범위]

지금 테스트해주신 것을 보자면, 저는 얼추 E2E 테스트에 근접하다고 생각해요. 하지만 이런 고민을 해주신 이유가 결과적으로 winners에 누가 들어있는지, racingCars에는 어떤 값이 담겨있는지까지 검증해야 완벽한 검증이라고 생각하셔서 그런 것 같아요.

우리가 컨트롤할 수 없는 부분이 비즈니스 로직에 담겼을 때 이러한 것은 테스트 하기 어려운 코드 라고 생각해요.

관련된 글

[아래는 좀 더 나아간 내용이라.. 남길까 고민되었지만 그래도 남겨보아요!]

이럴 때엔 어쩔 수 없지만 위에 jojoldu 님이 작업해주신 것처럼 리팩터링해볼 수도 있고, 아니면 랜덤에 대해서만 mocking을 사용하는 수도 있어요.

  • mocking이라 함은 실제 객체가 아닌 가짜 객체를 만들어 우리가 원하는 응답을 뱉도록 하는 객체예요. 테스트가 특정 케이스에 의존하거나 우리가 값을 컨트롤하지 못하는 경우에 사용할 수 있어요. 즉 지금과 같은 케이스에는 GameService를 모킹해서 playRacing을 호출했을 때 이런 값을 반환해달라고 모킹을 해야할거예요.

하지만 모킹을 사용하게 된다면 실제 동작을 테스트하는 것이 아니기 때문에 신뢰도가 떨어질 수도 있겠죠. 이런 부분을 보완하기 위해서 단위 테스트가 있다는 생각도 들구요ㅎㅎ 테스트에 대해서 어떻게 해야 신뢰도를 높일 수 있을지 많이 고민해보시면 좋을 것 같아요~

Copy link
Author

@kdkdhoho kdkdhoho Apr 20, 2023

Choose a reason for hiding this comment

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

처음엔 스프링 학습 테스트에 예제 코드가 존재하여 그나마 익숙한 RestAssured를 사용했습니다.
하지만 E2E 테스트를 위해 결과적으로 winners에 누가 들어있는지, racingCars에는 어떤 값이 담겨있는지까지 검증하고 싶었고,
따라서 GameService와 RacingGame 객체를 mocking하기 위해 결과적으로 MockMvc를 사용했습니다.

하지만 이렇게 Service를 mocking하면 WebController와 GameService와의 의존성이 끊어지기에, 결과적으로 E2E 테스트가 아닌, controller의 단위 테스트가 된다는 의견을 들었습니다. 따라서 기존 생각을 내려놓고 단순히 json의 key값으로만 테스트했습니다.
이 과정에서 익숙해진 MockMvc를 사용했습니다.

추가로 RestAssured도 사용해보고 싶었지만, 현재 웹 자동차 경주미션에서 스프링 테스트에 집중하기보단 Spring MVC, Spring JDBC, Spring Core에 집중하고 싶었습니다.


MockMvc와 RestAssured의 차이로는 원하는 Controller와 Mocking한 객체를 이용할 것이냐 혹은, 실제 프로덕션 코드를 전부 사용하냐인 것 같아요.

Spring 테스트 정말 공부할 것도 많고 사용하기 어렵군요 ,,
상세하고 친절히 답변달아주셔서 너무 감사합니다 🥲
열심히 공부해볼게요..!


@SpringBootTest
@AutoConfigureMockMvc
class WebControllerTest {

Choose a reason for hiding this comment

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

👍

Copy link
Author

Choose a reason for hiding this comment

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

@seovalue 안녕하세요 조앤!
남겨주신 코멘트 모두 확인 후 답변 남겨두었습니다. 특히, 테스트 관련 답변에서 너무 감동이었어요 🥲

이전에 남겨주신 코멘트에 대한 추가적인 질문이 있었는데, 따로 남겨주시지 않은 것들이 있습니다!
아래에 링크 남겨드리겠습니다 :)
#104 (comment)
#104 (comment)
#27 (comment) // 요거는 답변에 대한 피드백을 듣고 싶어서 남깁니다!
#27 (comment)

항상 감사합니다.

@seovalue seovalue merged commit 69c194f into woowacourse:kdkdhoho Apr 19, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants