공부/Spring

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

데부한 2023. 4. 22. 15:46
반응형

출처 : 인프런

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

여태 코드를 작성하면 서버를 재실행하고 인텔리제이 터미널에서 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*");
}

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

 


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

반응형