230912
다이나믹 팩토리 패턴이라는 것이 있다.
리플렉션을 활용하여 런타임에 서브타입 클래스 정보를 얻고, 클래스 이름을 키 정보로, 클래스 객체를 값으로 가지는 맵을 런타임에 초기화한 뒤 가져가 쓰는 방법이다.
이전에 썼던 글에서 전략 패턴이라고 생각했는데 생각해보니 다이나믹 팩토리 패턴이 맞는 것 같다... 수정해야 할듯
블랙박스 테스트와 화이트박스 테스트가 있다. 블랙박스 테스트는 내부 구현을 모르는 상태에서 input과 output을 검증하는 것이고, 화이트박스 테스트는 내부 구현을 검증하는 방식이다.
그렇다면 왜 향로님의 글에서는 '내부 구현을 검증하면 안된다'고 했을까?
이는 depth level이 끝없이 깊어질 수 있기 때문이다. 그리고, 내부 구현을 검증하다보면 결국 내부 구현을 구성하는 다른 클래스를 mocking하고 stubbing하게 된다. 그렇게 되면 의존하고 있는 다른 클래스에 대하여 세부 구현을 상위 모듈에서 기술하게 되고 이는 즉 DIP 위반이다.
그렇다면 결론이 뭔데?
내부 구현을 검증하면 안된다 == 내부 구현의 검증 책임은 해당 구현 클래스에게 있다 라고 생각하면 된다.
그렇게 되면 예전에 언급했던 내용들이 모두 정리된다.
"리팩토링은 deepest branch에서 shortest branch로, 테스트는 shortest branch에서 deepest branch로" 라는 말이 있다.
말이 좀 어렵긴 한테 "테스트는 밖에서부터 돌려깎고 리팩토링은 안쪽에서부터 뜯어고쳐라" 라는 뜻이다.
즉 밖에서부터 돌려깎으란 뜻은 레거시에 테스트를 붙일 때는 통합 테스트부터 시작해서 단위 테스트로 deep-dive 하는 방식을 채택하라는 것이다.
이미 존재하는 코드베이스에 통합 테스트 -> 단위 테스트 순으로 테스트를 작성하게 되면 얻게 되는 이점이 바로 이것이다. 통합 테스트를 작성하게 되면 mocking과 stubbing이 최소화되게 된다. 하지만, 이 경우 내부 구현에서 의존하고 있는 다른 클래스가 black box 영역이 되므로, 해당 영역을 다시 white box로 만들기 위해 각 모듈에 대한 테스팅을 수행한다. 이렇게 하면 각 테스트가 하위 모듈을 검증하는 경우를 최소화시키면서, 전체 테스트를 white box로 만들 수 있다.
말이 좀 횡설수설한데 전체 black box로 이루어진 큰 테스트 상자를 바깥쪽부터 white box로 만들어나간다는 의미다.
나중에 다시 정리해서 글로 써봐야겠다...
https://docs.spring.io/spring-framework/reference/testing/integration.html
이 챕터에서는 통합 테스트에 대하여 스프링이 지원하는 기능과, 단위 테스트에 대한 best practice에 대해 다룹니다. 스프링 팀은 TDD를 지지하고 있습니다. 스프링 팀은 IoC를 올바르게 사용하는 것이 단위 테스트와 통합 테스트 모두를 편하게 만든다는 것을 발견했습니다. 즉 클래스에 대한 setter 메서드와 적절한 생성자가 있으면, 서비스 로케이터 레지스트리에 대한 설정 없이도 테스팅 중에 이러한 테스트를 더 쉽게 연결할 수 있습니다.
테스트는 기업의 소프트웨어 개발에서 필수적인 부분입니다. 이 챕터에서는 IoC 원칙으로 얻어지는 단위 테스트 상 이점과, 통합 테스트를 위한 스프링 프레임워크의 지원 기능에 대해서 다룰 것입니다. 단, 이 문서에서는 실무 환경에서의 테스팅에 대해서는 깊게 다루지는 않습니다.
의존성 주입을 사용하면 기존 J2EE나 Java EE 개발 방식보다 컨테이너에 대한 코드 의존성을 낮출 수 있습니다. 애플리케이션을 구성하는 POJO는 JUnit 혹은 TestNG를 사용하여 테스트할 수 있어야 하며, 이는 스프링이나 다른 컨테이너 없이 `new`` 연산자를 사용하여 초기화된 객체들이어야 합니다. 우리는 (다른 중요한 테스트 기술과 결합하여) 목 객체를 사용하는 식으로 우리의 코드를 독립적으로 테스트할 수 있습니다. Spring에 의해 권장되는 아키텍처를 따랐다면, 코드베이스를 깔끔하게 레이어별 / 컴포넌트별로 구분시켜서 단위 테스트를 더 쉽게 수행할 수 있습니다. 가령 서비스 레이어 객체를 테스트할 때, DAO나 레포지터리 인터페이스를 stubbing하고 mocking하여, 단위 테스트를 수행하는 동안 영속성 계층의 데이터에 접근하지 않아도 됩니다.
스프링은 모킹 전용의 다양한 패키지를 포함하고 있습니다.
- Environment
- JDNI
- Servlet API
- Spring Web Reactive
org.springframework.mock.env
패키지는 Environment
와 PropertySource
추상화에 대한 목 구현체를 포함하고 있습니다.. (Bean Definition Profiles와 PropertySource
추상화를 참고하자) MockEnvironment
와 MockPropertySource
는 특정 환경의 프로퍼티에 의존하는, 컨테이너 외부를 테스트할 때 유용합니다.
org.springframework.mock.jndi
패키지는 JNDI SPI에 대한 부분적인 구현을 포함합니다. 그래서 우리는 test suite 혹은 stand-alone 애플리케이션을 위한 간단한 JNDI 환경을 설정할 수 있습니다. 가령 만약 JDBC DataSource
인스턴스가 Jakarta EE 컨테이너처럼 테스트 코드에서도 같은 JNDI 이름에 바인딩되면, 애플리케이션 코드와 설정 파일을 테스트 시나리오에서도 재사용할 수 있습니다.
(참고)
JNDI(Jav Naming and Directory Interface)는 자바 애플리케이션에서 네이밍과 디렉토리 서비스에 접근하기 위한 API로, datasource가 WAS에 존재하는 경우 (보통은 application.yml
에 존재할 것이다) 여기에 접근하여 커넥션을 가져오기 위해 사용합니다. 장점으로는 다양한 연결 환경을 추상화하여 관리할 수 있다는 것이 있지만... 스프링 부트에는 profile이라는 아주 훌륭한 대안이 존재하므로 굳이 사용할 필요는 없습니다. 애초에 mock 지원도 deprecated 되어서 쓰지도 못해요...
org.springframework.mock.web
패키지는 web contexts, 컨트롤러, 필터를 테스트하는 데 유용한 목 객체를 가지고 있습니다. 이러한 목 객체들은 Spring Web MVC 프레임워크와 함께 사용하기 위해 만들어졌으며, 동적 목 객체(ex: EasyMock
)나 Servlet API에 대한 대안 목 객체(ex: MockObjects
)보다 사용하기에 더 편리합니다.
(팁)
스프링 프레임워크 6.0부터, org.springframework.mock.web
에 존재하는 목 객체들은 서블릿 6.0 API에 기반하고 있습니다.
Spring MVC 테스트 프레임워크는 통합 테스트 환경을 제공하기 위해, Servlet API 목 객체를 기반으로 구축되어 있습니다. MockMvc를 참고해보세요.
스프링 리액티브에 대해서는 배운 적이 없어 스킵하도록 하겠습니다.
스프링은 단위 테스트에 도움이 되는 다양한 클래스를 포함하고 있습니다. 두 가지 카테고리가 존재합니다.
- 일반적인 테스트 유틸리티
- Spring MVC 테스트 유틸리티
org.springframework.test.util
패키지는 단위 / 통합 테스트에 사용할 수 있는 일반적인 목적의 유틸리티를 여럿 포함하고 있습니다.
AopTestUtils
는 AOP 관련 유틸리티 메서드의 콜렉션입니다. 이 메서드를 사용하면, 하나 이상의 스프링 프록시 객체 뒤에 숨겨진 '진짜' 대상 객체에 대한 참조를 얻을 수 있습니다. 예를 들어, 만약 EasyMock
이나 Mockito
같은 라이브러리를 사용하여 동적인 mock으로서 빈을 만들었다면, 해당 빈의 동작을 단언하는 식으로 검증해야 할 수도 있습니다. 스프링의 핵심 AOP 유틸리티에 대해서는 AopUtils
, AopProxyUtils
를 참고합시다.
ReflectionTestUtils
는 리플렉션 기반 유틸리티 메서드입니다. 애플리케이션 코드를 테스트할 때, 상수의 값을 변경해야 하거나, public
이 아닌 필드의 값을 설정하거나, public
이 아닌 setter, configuration, 라이프사이클 콜백 메서드를 호출해야 하는 테스트 시나리오에서 이러한 메서드를 사용할 수 있습니다. 이에 대한 사례는 다음과 같습니다.
private
혹은protected
필드에 대한 접근 ORM 프레임워크 (JPA와 Hibernate)- 스프링이 지원하는,
private
혹은protected
필드, setter, 생성 메서드에 대한 DI를 제공하는 어노테이션(e.g.@Autowired
,@Inject
,@Resource
) - 생명주기 콜백 메서드에 사용되는 어노테이션 (e.g.
@PostContruct
,@PreDestroy
)
(참고)
제 경우에는 ReflectionTestUtils
를 엔티티 인스턴스에 대한 픽스처를 만들 때 자주 사용합니다. JPA 엔티티의 경우 영속화되는 시점까지 pk가 존재하지 않을 수 있는데, 이 경우 TestEntityManager
를 DI받아서 persist
를 호출하는 것은 꽤 번거로운 일입니다. 특히 순수한 엔티티 객체만 필요한 경우 영속성 계층과 관련된 빈을 주입받고 싶지 않을 때가 있는데, 해당 유틸리티를 사용하면 public setter가 없더라도 간단하게 id 값을 원하는대로 지정해줄 수 있습니다.
제 경우에는 인증 관련 기능 테스트 상황에서, 현재 로그인한 유저를 반환하는 유틸리티를 stubbing해줄 때 유저 엔티티에 대한 pk를 설정하기 위해 사용했습니다.
TestSocketUtils
는 통합 테스트 시나리오 상황에서 localhost
의 사용 가능한 TCP 포트를 찾아주는 간단한 유틸리티입니다.
(참고)
TestSocketUtils
는 사용 가능한 랜덤 포트에서 외부 서버를 구동하는 통합 테스트 상황에서 사용될 수 있습니다. 하지만, 이 유틸리티는 주어진 포트의 후속 사용 가능 여부를 보장하지 않으므로, 신뢰하기 어렵습니다. 따라서 TestSocketUtils
를 사용하여 서버에 사용 가능한 로컬 포트를 찾는 대신, 서버가 시작할 때 직접 선택하거나 / 운영체제에 의해서 할당되는 임시 랜덤 포트을 사용하는 것을 권장합니다. 해당 서버와 상호작용하기 위해서는, 서버에게 그 포트가 현재 사용중인지 질의해야만 하니까요.
org.springframework.test.web
패키지는 ModelAndViewAssert
를 포함하고 있습니다. 이는 JUnit
, TestNG
, 혹은 Spring MVC에 대한 단위 테스트를 위한 다른 테스트 프레임워크 등과 함께 사용할 수 있는 클래스입니다.
(팁 - Spring MVC 컨트롤러에 대한 단위 테스트)
Spring MVC Controller
클래스를 POJO로서 단위 테스트하려고 한다면, ModelAndViewAssert
를 MockHttpServletRequest
, MockHttpSession
, Spring의 Servlet API 목 객체 등과 결합하여 사용하세요.
Spring MVC의 WebApplicationContext
구성과 함께 Spring MVC 및 REST Controller
를 철저하게 통합 테스트하려면, Spring MVC Test Framework(i.e. MockMvc
)를 대신 사용하세요.