토비의 스프링 부트 - DI와 테스트, 디자인 패턴

2023. 4. 22. 15:46·공부/Spring
반응형

출처 : 인프런

테스트 코드를 이용한 테스트

여태 코드를 작성하면 서버를 재실행하고 인텔리제이 터미널에서 HTTPie로 테스트를 진행했다. 이렇게 사람이 수동으로 테스트를 하는 것보다 테스트 코드를 작성해서 사람보다는 기계?가 테스트하는 게 훨씬 시간도 절약되고 정확성이 높다. 

HelloApiTest.java

public class HelloApiTest {

    @Test
    void helloApi() {

    }
}

내용이 빈 메서드를 만들고 실행해보면 테스트가 통과되었다는 표시가 뜬다. 검증할 내용이 없어 테스트가 통과되지 않는 상황이 없기에 몇 번을 돌려도 테스트는 통과된다.

 

 

TestRestTemplate.java

@Test
void helloApi() {
    // http localhost:8080/hello?name=Spring
    TestRestTemplate rest = new TestRestTemplate();

    ResponseEntity<String> res = rest.getForEntity("http://localhost:8080/hello?name={name}", String.class, "Spring");

    Assertions.assertThat(res.getStatusCode()).isEqualTo(HttpStatus.OK);
    Assertions.assertThat(res.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE)).startsWith(MediaType.TEXT_PLAIN_VALUE);
    Assertions.assertThat(res.getBody()).isEqualTo("Hello Spring");
}

TestRestTemplate을 사용하여 API 테스트를 쉽게 작성할 수 있다. TestRestTemplate은 RestTemplate의 테스트를 위한 클래스이다.

.getForEntity는 HTTP GET Method이며 그 외 HTTP Method를 제공하는 메서드가 있다. Assertions는 검증을 위한 클래스이다. 검증을 보다 쉽게 할 수 있도록 도와주며 여러 메서드를 제공한다. Assertions를 static import를 사용하면 아래와 같이 간단하게 사용할 수 있다.

import static org.assertj.core.api.Assertions.assertThat;

public class HelloApiTest {
    @Test
    void helloApi() {
    	//...생략
        assertThat(res.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(res.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE)).startsWith(MediaType.TEXT_PLAIN_VALUE);
        assertThat(res.getBody()).isEqualTo("Hello Spring");
    }
}

 

위 코드를 애플리케이션을 실행 시키고 나서 테스트를 돌려보면 통과된다. 꼭 애플리케이션을 실행 후! 테스트를 돌려야 한다. 나는 바보같이 그것도 모르고 해당 에러를 134224시간 동안 잡고있었다;

 

 

 

DI와 단위 테스트

HelloServiceTest.java

@Test
void simpleHelloService() {
    SimpleHelloService helloService = new SimpleHelloService();
    String ret = helloService.sayHello("Test");
    Assertions.assertThat(ret).isEqualTo("Hello Test");
}

api를 테스트 했었던 코드는 네트워크를 지나다니고 요청과 응답을 받는 과정에서 추가적인 작업들이 필요하기 때문에 속도가 느리다. 위 테스트는 실행해보면 굉장히 빠른 속도로 테스트가 완료된다.

 

 

HelloControllerTest.java

@Test
void helloController() {
    HelloController helloController = new HelloController(name -> name);

    String ret = helloController.hello("Test");

    Assertions.assertThat(ret).isEqualTo("Test");
}

 

검증은 성공하는 테스트 뿐만 아니라 실패일 경우에도 예측하여 검증해야한다. 예를 들면 Controller hello() 메서드에는 objects.requireNonNull() 메서드가 존재하는데, 이 메서드는 입력 값이 null이면 error를 발생시켜주고, null이 아니면 정상적인 실행을 하는 메서드이다. 즉, 입력 값을 null로 넣었을 때 예외가 터지면 검증 성공, 예외가 터지지 않으면 검증 실패이다.

@Test
void failsHelloController() {
    HelloController helloController = new HelloController(name -> name);

    Assertions.assertThatThrownBy(() -> {
        String ret = helloController.hello(null);
    }).isInstanceOf(NullPointerException.class);
}

 

입력 값이 null이 아니지만 문자열이라 빈 문자열("")이 입력 값으로 들어올 수 있다. 이 부분을 검증하면 테스트는 현재로써는 통과되지 못한다. objects.requireNonNull()은 빈 문자열을 체크해주지 않기 때문이다.

Assertions.assertThatThrownBy(() -> {
    String ret = helloController.hello("");
}).isInstanceOf(NullPointerException.class);

 

HelloController.java의 로직을 수정해보자. name이 null이거나 빈문자열일 때 IllegalArgumentException()을 터트린다.

public String hello(String name) {
    if(name == null || name.trim().length() == 0) throw new IllegalArgumentException();
    return helloService.sayHello(name);
}

 

그럼 이제 다시 HelloControllerTest.java로 돌아가 로직 수정을 한다. null의 경우도 NullPointerException()을 터트리는게 아닌 IllegalArgumentException()을 터트리니 수정해준다.

@Test
void failsHelloController() {
    HelloController helloController = new HelloController(name -> name);

    Assertions.assertThatThrownBy(() -> {
        String ret = helloController.hello(null);
    }).isInstanceOf(IllegalArgumentException.class);

    Assertions.assertThatThrownBy(() -> {
        String ret = helloController.hello("");
    }).isInstanceOf(IllegalArgumentException.class);
}

 

 

 

DI를 이용한 Decorator 패턴 (Proxy 생략)

Decorator

@Service
public class HelloDecorator implements HelloService{

    private final HelloService helloService;

    public HelloDecorator(HelloService helloService) {
        this.helloService = helloService;
    }

    @Override
    public String sayHello(String name) {
        return "*" + helloService.sayHello(name) + "*";
    }
}

 

위와 같이 코드를 작성했을 때 생각해야할 것이 하나 있다. 그럼 HelloController에서는 HelloDecorator와 HelloService 중에 무얼 생성자로 넘겨받아야할까. 일단 현재는 HelloService를 생성자로 넘겨받고있다.

public HelloController(HelloService helloService) {
    this.helloService = helloService;
}

 

그럼 또 하나 생각할게 생긴다. 그럼 SimpleHelloService는 어디에서 주입할 수 있는 걸까. 방법은 

  • 명시적인 설정 파일 - HelloController는 HelloDecorator를 주입받고, HelloDecorator는 SimpleHelloService를 주입받으라고 작성하는 방법
  • FactoryMethod를 이용해 자바 코드로 Object의 의존 관계의 순서 정해주기
  • 우선순위 지정(어노테이션 지정)

 

제일 쉬운 방법은 우선순위를 지정해주는 것이다. 우선순위를 지정해주는 방법은 애노테이션을 클래스 레벨에 사용하면 된다.

@Service
@Primary
public class HelloDecorator implements HelloService{ ... }

@Primary가 붙은 후보는 우선순위가 제일 높으므로  HelloController에서 HelloDecorator가 선택되어 주입된다. HelloDecorator는 자기 자신을 제외한 나머지 후보는 SimpleHelloService 밖에 없기 때문에 자동적으로 SimpleHelloService가 선택되어 주입되게 된다.

 

HelloDecorator가 잘 적용되었는지 테스트해보자. HelloApiTest에서 코드를 수정한다.

@Test
void helloApi() {
    // http localhost:8080/hello?name=Spring
    TestRestTemplate rest = new TestRestTemplate();

    ResponseEntity<String> res = rest.getForEntity("http://localhost:8080/hello?name={name}", String.class, "Spring");

    assertThat(res.getStatusCode()).isEqualTo(HttpStatus.OK);
    assertThat(res.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE)).startsWith(MediaType.TEXT_PLAIN_VALUE);
    assertThat(res.getBody()).isEqualTo("*Hello Spring*");
}

 

전체적으로 테스트를 한 번에 돌려보아도 모든 테스트가 성공한다.

 

데코레이터가 잘 적용되었는지를 검증하는 단위 테스트도 하나 작성한다.

@Test
void helloDecorator() {
    HelloDecorator decorator = new HelloDecorator(name -> name);
    String ret = decorator.sayHello("Test");
    Assertions.assertThat(ret).isEqualTo("*Test*");
}

데코레이터의 우선순위 방법에는 후보가 두 개일 경우엔 괜찮지만 여러 개일 경우에 복잡하게 우선순위를 매겨야하는 상황이 올 수 있다. 그러므로 데코레이터는 애노테이션을 이용한 우선순위 방식보다는 명시적인 설정 파일로 작성하는 것이 더 좋을 수 있다.

 


- 출처 : 인프런 토비의 스프링부트 - 이해와 원리 강의

반응형
저작자표시 비영리 변경금지 (새창열림)
'공부/Spring' 카테고리의 다른 글
  • 스프링부트 3.X에 Swagger 적용하기
  • 토비의 스프링 부트 - 자동 구성 기반 애플리케이션_1
  • 토비의 스프링 부트 - 독립 실행형 스프링 애플리케이션-2
  • 토비의 스프링 부트 - 독립 실행형 스프링 애플리케이션-1
데부한
데부한
어차피 할 거면 긍정적으로 하고 싶은 개발자
    반응형
  • 데부한
    동동이개발바닥
    데부한
  • 전체
    오늘
    어제
    • 분류 전체보기 (307)
      • 방통대 컴퓨터과학과 (27)
        • 잡담 (9)
        • 3학년1학기 (17)
      • 프로젝트 및 컨퍼런스 회고 (1)
        • 프로젝트 (4)
        • 한이음 프로젝트 (0)
        • 회고 (3)
      • 공부 (165)
        • Spring (37)
        • JPA (71)
        • 인프런 워밍업 클럽_BE (10)
        • Java (6)
        • React.js (27)
        • 넥사크로 (11)
        • 기타 (3)
      • 알고리즘 (85)
        • 알고리즘 유형 (10)
        • 알고리즘 풀이 (57)
        • SQL 풀이 (18)
      • 에러 해결 (13)
      • 잡담 (7)
        • 국비교육 (2)
        • 구매후기 (5)
        • 진짜 잡담 (0)
  • 블로그 메뉴

    • Github
    • Linkedin
    • 홈
    • 방명록
    • 글쓰기
    • 관리
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    react
    백준
    에러해결
    SpringBoot를 이용한 RESTful Web Service 개발
    Spring
    IT
    스프링부트
    코딩테스트
    운영체제
    springboot
    토이프로젝트
    SQL
    토비의스프링부트
    egov
    자바스크립트
    Java
    방통대
    개발자
    QueryDSL
    프로그래머스
    넥사크로
    RESTful
    전자정부프레임워크
    oracle
    인프런
    JPA
    프론트엔드
    MSA
    알고리즘
    기출문제
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
데부한
토비의 스프링 부트 - DI와 테스트, 디자인 패턴
상단으로

티스토리툴바