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 : 서블릿 컨테이너 환경을 구성함, 랜덤 포트 listen
  • TestRestTemplate : 브라우저 대신 클라이언트 역할, 쿠키와 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

스프링 동작 원리 알아보자

어플리케이션 컨텍스트와 설정사항

  • 설정사항 만들기
@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