Super Kawaii Cute Cat Kaoani
본문 바로가기
⚙️ Back-end/Spring

[Spring Boot] 1주차 스터디 : 2장 스프링 부트에서 테스트 코드를 작성하자

by wonee1 2025. 1. 14.
728x90

✏️ 스프링 부트와 AWS로 혼자 구현하는 웹 서비스 책을 보면서 스터디 한 내용을 정리하였습니다 

 

 

 

2장 스프링 부트에서 테스트 코드를 작성하자


 

 

💜테스트 코드 작성


💠TDD

  • 테스트가 주도하는 개발
  • 테스트 코드를 먼저 작성하는 것부터 시작

💠레드 그린 사이클

항상 실패하는 테스트를 먼저 작성 (Red)

테스트가 통과하는 프로덕션 코드를 작성 (Green)

테스트가 통과하면 프로덕션 코드를 리팩토링(Refactor)

 

💠단위 테스트

기능 단위의 테스트 코드를 작성하는 것

 

💠빠른 피드백

  1. 코드를 작성하고
  2. 프로그램 실행한 뒤
  3. Postman과 같은 API 테스트 도구로 HTTP 요청
  4. 요청 결과를 System.out.println()으로 눈으로 검증
  5. 결과가 다르면 다시 프로그램(Tomcat)을 중지하고 코드를 수정한다

2~5는 매번 코드를 수정할 때마다 반복

톰캣을 재시작하는 시간은 수십 초에서 1분 이상 소요되곤 하며 수십 번씩 수정해야한다

 

 

 

 

💜Hello Controller 테스트 코드 작성하기


package com.jojoIdu.book.springboot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.
SpringBootApplication;

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
  • Application 클래스는 앞으로 만들 프로젝트의 메인 클래스가 된다
  • @SpringBootApplication 으로 인해 스프링 부트의 자동 설정, 스프링 Bean 읽기와 생성을 모두 자동으로 설정한다.
  • @SpringBootApplication 이 있는 위치부터 설정을 읽어간다 이 클래스는 항상 프로젝트의 최상단
  • SpringApplicaiton.run 으로 인해 내장 WAS를 실행한다 (내장 WAS를 사용하면 언제 어디서나 같은 환경에서 스프링 부트를 배포할 수 있다)

 

 

파일 구조

 

 

HelloController.java

package com.jojoIdu.book.springboot.web;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.
RestController;

public class HelloController {

    @GetMapping("/hello")
    public String hello() {
        return "hello";
    }

}

  • @RestController
    • 컨트롤러를 JSON을 반환하는 컨트롤러로 만들어준다
    • 예전에는 @ResponseBody를 각 메소드마다 선언했던 것을 한번에 사용할 수 있게 해준다고 생각하면된다
  • @GetMapping
    • HTTP Method인 Get의 요청을 받을 수 있는 API를 만들어준다

 

 

파일 구조

 

test 폴더에 테스트 코드를 작성할 클래스를 생성

 

package com.jojoIdu.book.springboot.web;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@RunWith(SpringRunner.class)
@WebMvcTest(controllers = HelloController.class)
public class HelloControllerTest {

    @Autowired
    private MockMvc mvc;

    @Test
    public void hello가_리턴된다() throws Exception{
        String hello = "hello";

        mvc.perform(get("/hello"))
                .andExpect(status().isOk())
                .andExpect(content().string(hello));

    }

}

 

 

  • @RunWith(SpringRunner.class)
    • 테스트를 진행할 때 JUnit에 내장된 실행자 외에 다른 실행자를 실행시킨다
    • 여기서는 SpringRunner라는 스프링 실행자를 사용한다
    • 스프링 부트 테스트와 JUnit 사이에 연결자 역할을 한다
  • @WebMvcTest
    • 여러 스프링 테스트 어노테이션 중 Web에 집중할 수 있는 어노테이션
    • 선언할 경우 @Controller, @ControllerAdvice등을 사용할 수 있다
    • 단 @Service, @Component, @Repository 등은 사용할 수 없다
    • 여기서는 컨트롤러만 사용하기 때문에 선언
  • @Autowired
    • 스프링이 관리하는 빈을 주입 받는다
  • private MockMvc mvc
    • 웹 API를 테스트할 때 사용한다
    • 스프링 MVC 테스트의 시작점이다.
    • 이 클래스를 통해 HTTP GET, POST 등에 대한 API 테스트를 할 수 있다.
  • mvc.perform(get("/hello"))
    • MockMvc를 통해/hello 주소로 HTTP GET 요청을 한다.
    • 체이닝이 지원되어 아래와 같이 여러 검증 기능을 이어서 선언할 수 있다.
  • andExpect(status().isOk())
    • mvc.perform의 결과를 검증한다
    • HTTP Header의 Status를 검증한다.
    • 우리가 흔히 알고 있는 200, 404, 500 등의 상태를 검증한다.
  • andExpect(content().string(hello))
    • mvc.perform의 결과를 검증한다.
    • 응답 본문의 내용을 검증한다.
    • Controller에서 "hello"를 리턴하기 때문에 이 값이 맞는지 검증한다.

 

💡 401 Unauthorized 오류 발생


Status
필요:200
실제   :404
<클릭하여 차이점 확인>

java.lang.AssertionError: Status expected:<200> but was:<404>
	at org.springframework.test.util.AssertionErrors.fail(AssertionErrors.java:59)
	at org.springframework.test.util.AssertionErrors.assertEquals(AssertionErrors.java:122)
	at org.springframework.test.web.servlet.result.StatusResultMatchers.lambda$matcher$9(StatusResultMatchers.java:627)
	at org.springframework.test.web.servlet.MockMvc$1.andExpect(MockMvc.java:214)
	at com.jojoIdu.book.springboot.web.HelloControllerTest.helloTest(HelloControllerTest.java:27)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:568)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
	at org.springframework.test.context.junit4.statements.RunBeforeTestExecutionCallbacks.evaluate(RunBeforeTestExecutionCallbacks.java:74)
	at org.springframework.test.context.junit4.statements.RunAfterTestExecutionCallbacks.evaluate(RunAfterTestExecutionCallbacks.java:84)
	at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75)
	at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)
	at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)
	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:251)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:97)
	at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
	at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
	at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
	at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
	at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:190)
	at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
	at org.junit.runner.JUnitCore.run(JUnitCore.java:115)
	at org.junit.vintage.engine.execution.RunnerExecutor.execute(RunnerExecutor.java:42)
	at org.junit.vintage.engine.VintageTestEngine.executeAllChildren(VintageTestEngine.java:80)
	at org.junit.vintage.engine.VintageTestEngine.execute(VintageTestEngine.java:72)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:107)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:88)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:54)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:67)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:52)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:86)
	at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:86)
	at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.processAllTestClasses(JUnitPlatformTestClassProcessor.java:119)
	at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.access$000(JUnitPlatformTestClassProcessor.java:94)
	at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor.stop(JUnitPlatformTestClassProcessor.java:89)
	at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.stop(SuiteTestClassProcessor.java:62)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:568)
	at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36)
	at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
	at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
	at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:94)
	at jdk.proxy1/jdk.proxy1.$Proxy2.stop(Unknown Source)
	at org.gradle.api.internal.tasks.testing.worker.TestWorker$3.run(TestWorker.java:193)
	at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:129)
	at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:100)
	at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:60)
	at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56)
	at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:113)
	at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:65)
	at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
	at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)

> Task :test
HelloControllerTest > helloTest FAILED
    java.lang.AssertionError at HelloControllerTest.java:27
1 test completed, 1 failed
> Task :test FAILED
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':test'.
> There were failing tests. See the report at: 

 

 

 

build.gradle를 수정 후 테스트 통과

 

 

💜Application 실행 후 8080에서 확인


수동으로 검증하고 테스트 코드를 작성하지 않는다. 테스트 코드로 먼저 검증 할 것!

 

 

 

 

 

 

💜롬복 설치


 

롬복은 자바 개발자들의 필수 라이브러리

 

롬복은 자바 개발을 할 때 자주 사용하는 코드 Getter, Setter, 기본 생성자, toString 등을 어노테이션으로 자동 생성해준다

  1. build.gradle에 롬복을 등록한다 annotationProcessor 'org.projectlombok:lombok:1.18.30'
  2. 플러그인을 설치한다

     

 

      3. Enable anntation processing을 체그한다 

 

 

 

 

 

💜Hello Controller 코드를 롬복으로 전환하기


기존 코드를 롬복으로 변경한다

 

 

 

  1. web 패키지에 dto 패키지 추가 

       모든 응답 dto는 dto 패키지에 추가한다

 

 

   2. HelloResponseDto 코드 작성

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public class HelloResponseDto {
    private final String name;
    private final int amount;
}

 

  • @Getter
    • 선언된 모든 필드의 get 메소드를 생성해준다
  • @RequiredArgsConstructor
    • 선언된 모든 final 필드가 포함된 생성자를 생성해준다
    • final이 없는 필드는 생성자에 포함되지 않는다

 

 

 3.  테스트 코드 작성 

 

 

 

package com.jojoIdu.book.springboot.web.dto;

import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;
public class HelloResponseDtoTest {

    @Test
    public void lombok_test(){
        //given
        String name= "test";
        int amount = 1000;

        //when 
        HelloResponseDto dto = new HelloResponseDto(name, amount);
        
        //then
        assertThat(dto.getName()).isEqualTo(name);
        assertThat(dto.getAmount()).isEqualTo(amount);
        
        

    }

}

 

  • assetThat
    • assetj라는 테스트 검증 라이브러리의 검증 메소드
    • 검증하고 싶은 대상을 메소드 인자로 받는다
    • 메소드 체이닝이 지원되어 isEqualTo와 같은 메소드를 이어서 사용할 수 있다
  • isEqualTo
    • assertj의 동등 비교 메소드
    • asserThat에 있는 값과 isEqualTo의 값을 비교해서 같을때만 성공

 

 Junit과 비교했을 때 assertj의 장점

  • coreMatchers와 달리 추가적으로 라이브러리가 필요하지 않다
  • 자동완성이 더 확실하게 지원된다

 

 4. 테스트 메소드 실행

 

 

 

5. HelloController에 새로 만든 ResponseDto를 사용하도록 코드 추가

import com.jojoIdu.book.springboot.web.dto.HelloResponseDto;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    @GetMapping("/hello")
    public String hello() {
        return "hello";
    }

    @GetMapping("/hello/dto")
    public HelloResponseDto helloDto(@RequestParam("name") String name, @RequestParam("amount") int amount) {
        return new HelloResponseDto(name, amount);
    }

}

 

  • @RequestParam
    • 외부에서 API로 넘긴 파라미터를 가져오는 어노테이션이다
    • 외부에서 **name(@RequestParam("name"))이란 이름으로 넘긴 파라미터를 메소드 파라미터 name(String name)**에 저장한다

name과 amount는 API를 호출하는 곳에서 넘겨준 값들

 

 

    6. helloControllerTest를 다음과 같이 변경

public class HelloControllerTest {

    @Autowired
    private MockMvc mvc;

    @Test
    public void helloTest() throws Exception {
        String hello = "hello";

        mvc.perform(get("/hello"))
                .andExpect(status().isOk())
                .andExpect(content().string(hello));
    }

    @Test
    public void helloDtoTest() throws Exception {
        String name = "hello";
        int amount = 1000;

        mvc.perform(get("/hello/dto") 
                        .param("name", name)
                        .param("amount", String.valueOf(amount)))
                .andExpect(status().isOk())
                .andExpect(j**sonPath("$.name"**, is(name)))
                .andExpect(**jsonPath("$.amount**", is(amount)));
    }
}

Param

  • API 테스트를 할 때 사용될 요청 파라미터를 설정한다.
  • 단 값은 String만 허용된다
  • 숫자, 날짜 등의 데이터를 등록할 때는 문자열로 변경해야 한다.

jsonPath

  • JSON 응답값을 필드별로 검증할 수 있는 메소드.
  • $를 기준으로 필드명을 명시.
  • 여기서는 name과 amount를 검증하니 $name, $ amount로 검증한다.
  1. 추가된 API를 테스트 하는 코드를 HelloControllerTest에 추가한다

 

 

7. dto API 테스트 결과

 

 

 

 

💡간단한 개념 정리


💠빈(Bean)

  • Spring에서 관리하는 객체
  • 예: 프로그램에서 사용할 객체를 만들어주고, 관리(생성, 소멸 등)까지 해주는 역할.

 

💠어노테이션(Annotation)

  • 특정 기능이나 정보를 알려주는 표시.
  • 예: @Override, @Component처럼 코드에 붙여서 특별한 역할을 하게 만드는 도구.
  • 빈 등록 관련 어노테이션:
    • @Component: 일반적인 빈으로 등록.
    • @Service: 서비스 계층에서 사용.
    • @Repository: 데이터 계층에서 사용.
    • @Controller: 컨트롤러 계층에서 사용.
  • 의존성 주입 관련 어노테이션:
    • @Autowired: 의존성 주입을 자동으로 처리.
    • @Qualifier: 주입할 빈을 명시적으로 선택.
    • @Inject (Java 표준, Spring에서 지원).
  • 설정 관련 어노테이션:
    • @Configuration: 설정 파일을 나타냄.
    • @Bean: 메서드의 반환값을 빈으로 등록.
  • 테스트 관련 어노테이션:
    • @Test: 테스트 메서드를 나타냄 (JUnit)

 

💠의존성 주입(Dependency Injection, DI)

객체 간의 의존 관계를 Spring 컨테이너가 설정해주는 방법

객체가 필요한 의존성을 직접 생성하지 않고 컨테이너가 대신 주입해준다

 

DI 구현 방법:

 

1. 필드 주입:

@Autowired
private MyService myService;

2. 생성자 주입 (권장)

private final MyService myService;

@Autowired
public MyController(MyService myService) {
    this.myService = myService;
}

 

 

3. Setter 주입:

 

private MyService myService;

@Autowired
public void setMyService(MyService myService) {
    this.myService = myService;
}

 

 

 

 

💠Getter와 Setter란?

GetterSetter는 객체의 캡슐화(Encapsulation)를 구현하기 위해 사용하는 메서드

  • Getter: 객체의 필드 값(변수 값)을 반환하는 메서드.
  • Setter: 객체의 필드 값을 설정(변경)하는 메서드.
728x90