✏️ 스프링 부트와 AWS로 혼자 구현하는 웹 서비스 책을 보면서 스터디 한 내용을 정리하였습니다
2장 스프링 부트에서 테스트 코드를 작성하자
💜테스트 코드 작성
💠TDD
- 테스트가 주도하는 개발
- 테스트 코드를 먼저 작성하는 것부터 시작
💠레드 그린 사이클
항상 실패하는 테스트를 먼저 작성 (Red)
테스트가 통과하는 프로덕션 코드를 작성 (Green)
테스트가 통과하면 프로덕션 코드를 리팩토링(Refactor)
💠단위 테스트
기능 단위의 테스트 코드를 작성하는 것
💠빠른 피드백
- 코드를 작성하고
- 프로그램 실행한 뒤
- Postman과 같은 API 테스트 도구로 HTTP 요청
- 요청 결과를 System.out.println()으로 눈으로 검증
- 결과가 다르면 다시 프로그램(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 등을 어노테이션으로 자동 생성해준다
- build.gradle에 롬복을 등록한다 annotationProcessor 'org.projectlombok:lombok:1.18.30'
- 플러그인을 설치한다
3. Enable anntation processing을 체그한다
💜Hello Controller 코드를 롬복으로 전환하기
기존 코드를 롬복으로 변경한다
- 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로 검증한다.
- 추가된 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란?
Getter와 Setter는 객체의 캡슐화(Encapsulation)를 구현하기 위해 사용하는 메서드
- Getter: 객체의 필드 값(변수 값)을 반환하는 메서드.
- Setter: 객체의 필드 값을 설정(변경)하는 메서드.
'⚙️ Back-end > Spring' 카테고리의 다른 글
[Spring Boot] 2주차 스터디 : 4장 머스테치로 화면 구성하기 (0) | 2025.01.15 |
---|---|
[Spring Boot] 2주차 스터디 : 3장 스프링 부트에서 JPA로 데이터 베이스를 다뤄보자 (1) | 2025.01.14 |
[Spring Boot] 1주차 스터디 : 1장 인텔리 제이로 스프링 부트 시작하기 (0) | 2025.01.14 |
Springboot [5] : 데이터베이스 개념 (0) | 2024.03.15 |
Springboot [4] : 테스트 (3) | 2024.03.05 |