TIL - 0529
스프링부트 - 테스트하면서 만들기
인수테스트(Acceptance Test) - TestRestTemplate
- 단위테스트가 단위별로 태스트를 한다면 인수테스트는 end to end 테스트라고 해서 전체적으로 합쳤을 때 동작을 제대로 하는가를 테스트하는 것
- 고객 테스트, 시스템 테스트라고도 함 : 인수테스트보단 이름에서 뜻을 유추할 수 있을 것 같음
- 단순 도메인 로직을 테스트하는 것이 아니라 고객이 사용할 서비스의 일부분 테스트 : 특정 URL 맵핑된 서비스 정상 동작 테스트
- 테스트 코드 작성해보기
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class HomeControllerTest {
@AutoWired
private TestRestTemplate template
@Test
public void home() {
ResponseEntity<String> response = template.getForEntity("/", String.class);
assertThat(response.getStatusCode(), is(HttpStatus.OK));
}
}
@RunWith(SpringRunner.class)
: SpringRunner.class - JUnit4 기반 스프링 테스트 클래스@SpringBootTest
: 테스트 환경에 맞는 어플리케이션 컨텍스트 관리(특정 빈 생성, 등록, 설정, 대체)webEnvironment = WebEnvironment.RANDOM_PORT
: 서블릿 컨테이너 환경을 구성함, 랜덤 포트 listenTestRestTemplate
: 브라우저 대신 클라이언트 역할, 쿠키와 redirect는 무시되도록 기본 설정ResponseEntity
: 리스폰스 메세지
통합 테스트
- 변경할 수 없는 코드를 대상으로 테스트를 진행하는 것 : DB나 외부 API를 사용한 기능 테스트
단위 테스트 - Mocktio
- 작은 단위마다 테스트를 하는 것 : 각각 메소드가 제대로된 기능을 하는지
- 웹 환경을 가상으로 구성해주는 Mockito를 사용함
- Mock 객체를 DI해줘서 있는 것처럼 세팅한 뒤 결과가 제대로 나오는지 테스트함
- Mockito : 서버의 코드 흐름에 대한 테스트(TestRestTemplate가 클라이언트 입장에서 테스트라면, Mockito는 서버 입장의 테스트)
로그인 관련 인수테스트하기
- UserController에 맵핑된 로그인 서비스에 대한 테스트
-
로그인 기능을 먼저 구현해둠 : UserService에 구현 - Repository(Dao)와 도메인객체(User)를 연결하는 역할(이전에는 컨트롤러에서 중재하는 역할까지 구현했지만 역할을 따로 나눔)
- UserService
@Service("userService")
public class UserService {
@Resource(name = "userRepository")
private UserRepository userRepository;
public User login(String userId, String password) throws UnAuthenticationException {
Optional<User> maybeUser = userRepository.findByUserId(userId);
if (!maybeUser.isPresent()) {
throw new UnAuthenticationException();
}
maybeUser.map(user -> user.matchPassword(password)).orElseThrow(UnAuthenticationException::new);
return maybeUser.get();
}
}
- Login 기능 테스트
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class LoginAcceptanceTest {
private static final Logger log = LoggerFactory.getLogger(LoginAcceptanceTest.class);
@Autowired
private UserRepository userRepo;
@Autowired
private TestRestTemplate template;
private HtmlFormDataBuilder builder;
@Before
public void setUp() throws Exception {
builder = HtmlFormDataBuilder.getEncodedform();
}
@Test
public void login() {
String userId = "javajigi";
builder.addParameter("userId", userId);
builder.addParameter("password", "test");
HttpEntity<MultiValueMap<String, Object>> request = builder.build();
ResponseEntity<String> response = template.postForEntity("/users/login", request, String.class);
assertThat(response.getStatusCode(), is(HttpStatus.FOUND));
assertThat(response.getHeaders().getLocation().getPath(), is("/users"));
assertNotNull(userRepo.findByUserId(userId));
log.debug(response.getBody());
}
}
HtmlFormDataBuilder
: 클라이언트 요청 메세지(HttpEntity)를 만드는 유틸리티 클래스TestRestTemplate
: 클라이언트(웹브라우저) 역할- 인수 테스트 : end to end 테스트로 실제 동작 이후 응답이 왔을 때 설계한 서비스대로 응답이 오는지 테스트
- 설정 해줘야 하는 부분
- server.servlet.session.tracking-modes : TestRestTemplate는 쿠키 허용을 하지않는 것이 기본값이어서, 설정해줘야함(application.properties)
reference
- 코드스쿼드 자바 백엔드 레벨 3 과정
- TOAST Meetup
- hyper-cube.io
스프링 동작 원리 알아보자
어플리케이션 컨텍스트와 설정사항
- 설정사항 만들기
@Configuration
public class DaoFactory {
@Bean
public static UserDao userDao() {
return UserDao.getInstance(getConnectionMaker());
}
@Bean
private static ConnectionMaker getConnectionMaker() {
return new DConnectionMaker();
}
}
@Configuration
: 어플리케이션 컨텍스트가 오브젝트 생성할 때 참고할 설정이라는 표시-
@Bean
: 오브젝트 생성을 담당하는 메소드라는 설정 - 대상을 관리하는 어플리케이션 컨텍스트 만들기
public class UserDaoTest {
private UserDao userDao1;
private UserDao userDao2;
private static final Logger log = LoggerFactory.getLogger(UserDaoTest.class);
@Before
public void setup() {
ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);
userDao1 = context.getBean("userDao", UserDao.class);
userDao2 = context.getBean("userDao", UserDao.class);
}
@Test
public void identity() {
assertEquals(userDao1, userDao2);
log.debug(userDao1.toString(), ", " + userDao2.toString());
}
}
- 어플리케이션 컨텍스트는 ApplicationContext 타입 인스턴스
- Configuration으로 설정한 메소드를 통해서 스프링빈을 생성해서 컨테이너에서 관리함
- getBin()의 리턴 타입은 Object인데, 번거롭게 우리가 캐스팅을 해주지않고, 생성 메소드를 컨테이너 내부에서 호출하면서 결과를 캐스팅해서 리턴함
- 일관된 방법으로 빈을 생성할 수 있음
- getBean 이외에도 특정한 어노테이션이 설정된 빈이나 타입만으로 검색도 가능
- 여기서 빈은 스프링 컨테이너에서 생성되고 관리되는 오브젝트를 말함
어플리케이션 컨텍스트와 싱글톤
- 스프링 빈은 싱글톤(하나의 객체)
- 컨텍스트는 오브젝트를 생성하는 빈 팩토리면서 관리하는 IOC 컨테이너
- 컨테이너면서 싱글톤 레지스트리 : 등록시켜두고 가져다 씀, 싱글톤 패턴은 아니지만(public 생성자를 가짐) - 싱글톤을 만들고 관리함(싱글 레지스트리), 스프링빈으로 선언하면 싱글톤으로 관리
- 자바 엔터프라이즈 환경에서 주로 사용되기때문에 : 멀티쓰레드 환경 - 각 쓰레드마다 객체를 만들어서 제공하면 힙 메모리 오버
- 그렇다면 thread safe 하지않을텐데? 그래서 상태값을 가지는 객체는 스프링빈으로 관리해서는 안됨 : User는 상태값을 각각 가지기떄문에 관리를 설정하지않음, 반면에 Dao나 Controller는 모두 같이 사용해도 되므로 관리 설정
reference
- 토비의 스프링
- 스프링 docs