Spring/Spring Test

[Spring][Test] 1. Unit Testing

noahkim_ 2025. 4. 19. 22:34

1. 단위 테스트

 

  • 테스트 시에는 코드를 독립적으로 테스트할 수 있어야 해요. (Spring 없이도 테스트가 가능해야 함)
  • Spring의 Dependency Injection 덕분에, J2EE 전통 방식보다 컨테이너에 덜 의존적인 코드 작성이 가능
  • POJO(Plain Old Java Object) 중심의 구조를 만들 수 있음
    • 순수한 자바 코드로 작성된 객체로 설계 가능 (특별한 인터페이스나 어노테이션 등에 의존 ❌)
    • 객체를 직접 생성하여 테스트할 수 있음을 의미함

 

장점

항목 설명
Spring IoC와의 연관성
⚡ 빠른 실행 속도 DB나 서버 등 외부 시스템 없이 실행 가능
IoC로 객체를 외부에서 주입하므로, 테스트 시 Mock/Stubs로 대체 가능
🛠️ 테스트 주도 개발(TDD)에 유리 빠르고 반복적인 테스트 작성 가능
POJO 중심 구조로 인해 객체 생성과 테스트가 간편함
🚀 생산성 향상 빠른 테스트 주기로 리팩토링과 개발 속도 향상
의존성 주입(DI)으로 컴포넌트 간 결합도 낮음
→ 리팩토링 용이
🧱 레이어 분리 & 컴포넌트화 Controller → Service → Repository 계층 분리 용이
Spring의 IoC 설계 원칙을 따르면 자연스럽게 분리 구조 유도
🔁 Mock / Stub 활용 가능 테스트 시 진짜 객체 대신 가짜(mock/stub) 객체로 동작 검증 가능
Service에서 DB 접근하는 Repository를 Mock으로 대체 가능
🎯 비즈니스 로직 집중 테스트 가능 외부 시스템 영향 없이 핵심 로직만 검증 가능
순수 POJO로 로직을 구성하면 DI만으로도 테스트가 충분함

 

2. Spring에서 제공하는 도구들

Mock

분류 주요 클래스/객체 사용 목적 특징 및 확장 도구
Environment 관련 Mock MockEnvironment
MockPropertySource
환경 속성에 의존하는 코드 테스트
@Value, Environment.getProperty() 등을 사용하는 코드 테스트에 적합
Servlet API 관련 Mock MockHttpServletRequest
MockHttpServletResponse
MockFilterChain
MockHttpSession 등
Spring MVC 테스트
실제 서버 없이 서블릿 동작을 모방한 객체 제공
📌 확장 도구: MockMvc
WebFlux 관련 Mock MockServerHttpRequest
MockServerHttpResponse
MockServerWebExchange
Spring WebFlux 기반 테스트
비동기 환경 테스트 지원
Mono<Void>, Flux 흐름 검증
📌 확장 도구: WebTestClient

 

예제) Environment 관련 Mock

더보기
public class AppProperties {

    @Value("${app.mode}")
    private String mode;

    @Value("${app.name}")
    private String name;

    public String getMode() {
        return mode;
    }

    public String getName() {
        return name;
    }
}
public class MockEnvironmentTest {
    private static MockEnvironment mockEnvironment;
    private static AppProperties appProperties;

    @BeforeAll
    static void setUp() {
        mockEnvironment = new MockEnvironment();

        mockEnvironment.setProperty("app.mode", "test");
        mockEnvironment.setProperty("app.name", "testApp");

        appProperties = new AppProperties();

        PropertySourcesPlaceholderConfigurer configurer = new PropertySourcesPlaceholderConfigurer();
        configurer.setEnvironment(mockEnvironment);

        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        context.registerBean(AppProperties.class, () -> appProperties);
        context.addBeanFactoryPostProcessor(configurer);
        context.refresh();
    }

    @Test
    void testMockEnvironment() {
        assertEquals("test", mockEnvironment.getProperty("app.mode"));
        assertEquals("testApp", mockEnvironment.getProperty("app.name"));
    }

    @Test
    void testAppProperties() {
        assertEquals("test", appProperties.getMode());
        assertEquals("testApp", appProperties.getName());
    }
}

 

예제) Servlet API 관련 Mock

더보기
@RestController
public class MyController {

    @GetMapping("/test/hello")
    public String hello(@RequestParam String name) {
        return "Hello, " + name;
    }
}
public class MyControllerTest {
    private MockMvc mockMvc;

    @BeforeEach
    void setUp() {
        mockMvc = MockMvcBuilders.standaloneSetup(new MyController()).build();
    }

    @Test
    void helloTest() throws Exception {
        mockMvc.perform(get("/test/hello?name=Spring"))
                .andExpect(status().isOk())
                .andExpect(content().string("Hello, Spring"));
    }
}

 

Utils

클래스 설명 사용 예시
AopTestUtils 프록시로 감싸진 객체의 실제 대상(Target) 객체에 접근
Mockito 등으로 mock 객체를 감싸놓았을 때 내부 접근
ReflectionTestUtils private/protected 필드나 메서드에 접근 (값 설정/호출)
JPA 엔티티나 @Autowired된 필드 수동 설정 등
TestSocketUtils 사용 가능한 랜덤 포트 조회 (테스트용)
- 포트의 지속적인 사용 가능성은 보장하지 않음
  (운영 환경에서는 주의 필요)
외부 서버와 통신하는 통합 테스트 등에서 유용

 

예제) AopTestUtils

더보기
@Aspect
@Component
public class LoggingAspect {
    @Before("execution(* MyService.hello(..))")
    public void logBefore() {
        System.out.println("Before method");
    }
}
@Service
public class MyService {
    public String hello(String name) {
        return "Hello, " + name;
    }
}
public class AopTest {

    @Test
    void testGetTargetObject() {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
        ctx.register(AopConfig.class);
        ctx.refresh();

        MyService proxy = ctx.getBean(MyService.class);
        MyService target = AopTestUtils.getTargetObject(proxy);

        assertEquals("Hello, spring", target.hello("spring"));
    }
}

 

예제) ReflectionTestUtils

더보기
public class SecretService {
    private String secret = "initial";

    private String getSecretMessage(String name) {
        return secret + " message for " + name;
    }
}
public class SecretServiceTest {

    @Test
    void testReflectionAccess() {
        SecretService secretService = new SecretService();
        ReflectionTestUtils.setField(secretService, "secret", "updated");

        String result = ReflectionTestUtils.invokeMethod(secretService, "getSecretMessage", "Spring");

        assertEquals("updated message for Spring", result);
    }
}

 

Spring MVC

도구 설명 사용 방법
ModelAndViewAssert Spring MVC 컨트롤러가 반환한 ModelAndView 객체를 검증
단위 테스트에서 모델, 뷰 검증 시 사용
MockHttpServletRequest
MockHttpServletResponse
MockHttpSession
컨트롤러 단위 테스트에서 Spring Context 없이
동작을 검증
DispatcherServlet, Filter, ViewResolver 등 생략 가능
POJO 방식 컨트롤러 테스트에 사용

 

예제) ModelAndViewAssert

더보기
@Controller
public class SampleController {

    @RequestMapping("/test/greet")
    public ModelAndView greet() {
        ModelAndView mav = new ModelAndView("greetingView");
        mav.addObject("message", "Hello, Spring!");
        return mav;
    }
}
public class ModelAndViewTest {

    @Test
    void testMav() {
        SampleController controller = new SampleController();

        ModelAndView mav = controller.greet();

        assertViewName(mav, "greetingView");
        assertModelAttributeAvailable(mav, "message");
        assertModelAttributeValue(mav, "message", "Hello, Spring!");
    }
}

 

예제) MockHttp*

더보기
@Controller
public class LoginController {
    public ModelAndView login(@RequestParam String userId, HttpServletRequest request, HttpServletResponse response) {
        HttpSession session = request.getSession();
        session.setAttribute("user", userId);

        response.addHeader("Set-Custom-Header", "logged-in");

        ModelAndView mav = new ModelAndView("home");
        mav.addObject("user", userId);

        return mav;
    }
}
public class MockServletTest {

    @Test
    void testLoginController() {
        LoginController controller = new LoginController();
        MockHttpServletRequest request = new MockHttpServletRequest();
        MockHttpServletResponse response = new MockHttpServletResponse();
        MockHttpSession session = new MockHttpSession();

        request.setSession(session);
        request.setParameter("userId", "Spring");

        ModelAndView mav = controller.login("Spring", request, response);

        assertEquals("home", mav.getViewName());
        assertEquals("Spring", mav.getModel().get("user"));
        assertEquals("Spring", session.getAttribute("user"));
        assertEquals("logged-in", response.getHeader("Set-Custom-Header"));
    }
}

 

 

출처