코딩하는 털보

21.09.25 TIL 본문

Diary/Today I Learned

21.09.25 TIL

이정인 2021. 9. 25. 20:57

sailinglog에 단위 테스트를 만들어야 한다!

사실 이미 늦었다. 클린 코드에서 봤던 TDD 방법론에는 실제 코드를 작성하기 전에 테스트를 먼저 작성해야 한다.
테슽 FIRST 원리에서 T(Timely)에 어긋나기도 한다.
https://rockintuna.tistory.com/150?category=886434
하지만 당연히 없는 것 보다는 낫지 않을까! 일단 두들겨 보자.


JUnit5으로 테스트하기

https://rockintuna.tistory.com/56

JUnit은 자바 프로그래밍 언어용 단위 테스트 프레임워크이며, 자바 TDD(테스트 주도 개발)의 핵심적인 역할을 한다.
일단 테스트를 작성하고 실행하기 전에 해야할 일들이 있었다. (junit을 사용하려면 해야하는 작업은 아니고 그냥 개인적인 요구사항이다.)

  1. 내 코드는 local에서 실행해도 RDS에 바로 붙도록 되어있었다. 일단 이 정보를 노출하고 싶지 않아서 local에 있는 mysql 을 사용하려고 한다.
  2. 테스트는 mysql이 아닌 h2에서 동작하도록 하고 싶다.

사전 작업

새로운 DB 생성

mysql> create database sailinglog;

접속할 User 생성 및 권한 부여

mysql> create user rockintuna@localhost identified by 'password';
Query OK, 0 rows affected (0.00 sec)

mysql> grant all privileges on sailinglog.* to rockintuna@localhost;
Query OK, 0 rows affected (0.00 sec)

mysql> flush privileges;
Query OK, 0 rows affected (0.00 sec)

여기까지 하고 인텔리제이에서 DB연결을 테스트했다.
무사히 성공!

그 다음 application.properties를 변경했다.

spring.jpa.show-sql=true
spring.datasource.url=jdbc:mysql://localhost:3306/sailinglog?serverTimezone=Asia/Seoul
spring.datasource.username=rockintuna
spring.datasource.password=password
spring.jpa.hibernate.ddl-auto=update

여기서 의문이 하나 생겼다.

어차피 AWS에 배포할 애플리케이션은 RDS를 바라봐야 할텐데 이전처럼 application.properties를 고치면 github에 나의 RDS 접속 정보를 노출하게 된다.
현재는 다른 파일로 만들어 놓고 git ignore를 해서 git이 add하려고 알려주지 않도록 해놓았다.
그럼 AWS에 올리기 위해 빌드할때마다 이 파일을 다시 변경해야할까...?

결과적으로 나는 Spring Boot의 profile 기능을 사용해서 profile 별로 서로 다른 properties 파일을 사용하게 하면 될 것이라고 생각했다.

먼저 application-dev.properties와 같이 각 profile 별로 사용할 properties파일을 만들어 주고나서
실행 설정에서 Active profiles 에 'prod'를 넣고 실행하면?
와! application-prod.properties를 자동으로 읽어서 속성을 가져간다.

이제 개발환경에서는 dev로 배포된 환경에서는 prod로 실행하고, github같은 저장소에는 application-prod.properties 파일이 안올라가도록 신경쓴다면 될 듯 하다.

사랑해요 스프링 부트

테스트는 H2에서 수행되도록 변경
테스트 코드는 mysql 말고 h2에서 수행하고 싶다.
src/test/resources 에 실제 코드처럼 application.properties를 작성해두면 테스트할때는 이 파일을 사용하게 된다.

spring.jpa.show-sql=true
spring.datasource.url=jdbc:h2:mem:sailinglogtest
spring.jpa.hibernate.ddl-auto=update

결과 확인을 위해 임시 테스트 클래스에 @SpringBootTest를 붙이고 실행해보자. (또는 기본으로 만들어지는 ApplicationTests를 실행해도 됨.)
로그 중간에 Using dialect: org.hibernate.dialect.H2Dialect와 같은 내용이 보인다면 성공! (DB 방언을 h2용으로 사용한다는 로그이다.)

테스트 코드 작성

오후 5시나 되서야 드디어 테스트 코드 작성 시작... 오늘의 목표는 컨트롤러 테스트 작성 완료하기.

나만의 규칙
BUILD-OPERATE-CHECK 패턴으로 작성하기
하나의 테스트 케이스는 하나의 assert문

@WebMvcTest

  • MVC를 위한 테스트.
  • 웹에서 테스트하기 힘든 컨트롤러를 테스트하는 데 적합하다.
  • @SpringBootTest 통합테스트 보다 가볍게 테스트할 수 있다.

컨트롤러 테스트는 @WebMvcTest를 사용하려고 한다.

@WebMvcTest
class ArticleControllerTest {

    @Test
    void getArticlesOrderByCreatedAtDesc() throws Exception {
    }
}

이렇게 실행하면 바로 에러가 난다.
난 아무것도 입력하지 않았는데 왜...?
No qualifying bean of type 'me.rockintuna.sailinglog.service.ArticleService'
딱보아하니 ArticleController에서 의존하고 있는 ArticleService를 찾을 수 없다는 것 같았다.

@MockBean

단위 테스트에서는 이런 빈들을 실제 빈을 사용하지 않고 가짜 빈을 사용한다.
@MockBean은 가짜 빈을 쉽게 만들어 준다.

@WebMvcTest
class ArticleControllerTest {

    @MockBean
    private ArticleService articleService;

    @Test
    void getArticlesOrderByCreatedAtDesc() throws Exception {
    }
}

가짜 빈을 추가했지만 이번에도 에러가 난다. 제발그만..
JPA metamodel must not be empty!
이 에러의 원인은 @SpringBootApplication 클래스에 @EnableJpaAuditing가 있기 때문이었다.
좀 더 자세히 말하자면 @WebMvcTest는 @SpringBootApplication 클래스는 스캔하지만 JPA 관련 Bean들은 로드하지 않기 때문이다.

기껏 만든 JPA Auditing 기능을 없애긴 좀 그러니 새로운 @Configuration으로 분리해보자.

@Configuration
@EnableJpaAuditing
public class JpaAuditingConfiguration {
}

후.. 드디어 성공 화면을 확인할 수 있었다. 다만 이 테스트에서는 JPA Auditing을 사용할 수 없을 것 같다..

MockMVC

컨트롤러를 테스트한다는 것은 각 핸들러를 테스트한다는 것이다. 즉 Postman 처럼 각 API에 요청을 보내는 테스트를 해보는 것이다.

테스트 코드에서 요청을 어떻게 보낼까. 이때 도와주는 도구가 MockMVC이다. MockMVC는 @WebMvcTest에 들어있기 때문에(@AutoConfigureMockMvc) @Autowired로 주입하면 바로 쓸 수 있다.

    @Autowired
    private MockMvc mvc;

Given-When-then

given은 테스트의 조건을 정의한다.
when은 테스트를 실행한다.
then은 테스트를 검증한다.

MockMvc의 perform 메서드는 HTTP 통신에 대한 when/then을 작성할 수 있게 도와준다.

@Test
void getArticlesOrderByCreatedAtDesc() throws Exception {
    //given
    ArticleRequestDto articleRequestDto1 =
            new ArticleRequestDto("test title 1", "tester", "test content 1");
    ArticleRequestDto articleRequestDto2 =
            new ArticleRequestDto("test title 2", "tester", "test content 2");
    ArticleRequestDto articleRequestDto3 =
            new ArticleRequestDto("test title 3", "tester", "test content 3");
    List<Article> articleList = new ArrayList<>();
    articleList.add(Article.from(articleRequestDto1));
    articleList.add(Article.from(articleRequestDto2));
    articleList.add(Article.from(articleRequestDto3));

    given(articleService.getArticlesOrderByCreatedAtDesc())
            .willReturn(articleList);

    //when
    mvc.perform(MockMvcRequestBuilders.get("/articles"))
            .andDo(print())

            //then
            .andExpect(status().isOk())
            .andExpect(content().contentType(MediaType.APPLICATION_JSON))
            .andExpect(jsonPath("$[0].title").value("test title 1"))
            .andExpect(jsonPath("$[0].writer").value("tester"))
            .andExpect(jsonPath("$[0].content").value("test content 1"))
            .andExpect(jsonPath("$[2].title").value("test title 3"))
            .andExpect(jsonPath("$[2].writer").value("tester"))
            .andExpect(jsonPath("$[2].content").value("test content 3"));

  verify(articleService).getArticlesOrderByCreatedAtDesc();
}

여기에는 없지만 json 입력을 필요로 하는 API에 대한 테스트도 있었는데, 이럴때 사용할 수 있는게 Jackson 라이브러리이다.
스프링부트는 Jackson 의존성을 가지고 있기 때문에 따로 추가하지 않아도 어플리케이션 컨텍스트에서 꺼내서 사용할 수 있다.

    @Autowired
    ObjectMapper objectMapper;

    @Test
    void createArticle() throws Exception {
        ArticleRequestDto requestDto =
                new ArticleRequestDto("test title 1", "tester", "test content 1");
        String jsonString = objectMapper.writeValueAsString(requestDto);

        given(articleService.createArticle(any(ArticleRequestDto.class)))
                .willReturn(articleList.get(0));

        mvc.perform(post("/articles")
                .contentType(MediaType.APPLICATION_JSON)
                .content(jsonString))
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                .andExpect(jsonPath("$.title").value("test title 1"))
                .andExpect(jsonPath("$.writer").value("tester"))
                .andExpect(jsonPath("$.content").value("test content 1"));

        verify(articleService).createArticle(any(ArticleRequestDto.class));
    }

프로파일도 설정했고 테스트도 작성했다. 이참에 AWS에 다시 배포하고 실행해보자.

실행가능 jar를 실행할때 프로파일을 지정하는 방법은 아래와 같다.

java -jar sailinglog-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod

오늘은 별다른 막힘없이 꽤 많은 변경을 한 것 같아서 기분이 너무 좋다.

Comments