요깨비's LAB

[Spring Boot, Back-End] 번외. Mock(가짜) 객체 본문

웹 개발/스프링 부트 프로젝트(레스토랑 예약)

[Spring Boot, Back-End] 번외. Mock(가짜) 객체

요깨비 2020. 1. 16. 20:09

지난번에 ControllerTest를 만들면서 우리가 테스트하고자 하는 것 외에 여러개의 객체가 동시에 주입되는 것을 확인했습니다. 이 경우에는 우리가 테스트하고자 하는 대상 외에 것들에 대한 의존성이 커서 우리가 테스트에 집중하기가 어려웠습니다. 
그리고 하나의 테스트를 통과시키기 위해 의존하는 부분들을 모두 만들어줘야 했습니다. 이러한 어려움을 해결하기 위해 사용하는 것이 바로 MockObject입니다.

1. Mokito

스프링은 기본적으로 POJO 방식의 자바 오브젝트를 권장합니다. 그래서 Mokito와 같은 라이브러리를 제공합니다.
코드를 통해 살펴보도록 하겠습니다.

@RunWith(SpringRunner.class)
@WebMvcTest(RestaurantController.class) // Restaurant 컨트롤러를 테스트하겠다는 어노테이션
public class RestaurantControllerTest {
	@Autowired
	private MockMvc mvc;
	
	@MockBean
	private RestaurantService restaurantService;
	
//	@SpyBean(RestaurantRepositoryImpl.class)
//	private RestaurantRepository restaurantRepository;
//
//	@SpyBean(MenuItemRepositoryImpl.class)
//	private MenuItemRepository menuItemRepository;
//	
//	@SpyBean(RestaurantService.class)
//	private RestaurantService restaurantService;

	@Test
	public void list() throws Exception {
		List<Restaurant> restaurants = new ArrayList<Restaurant>();
		restaurants.add(new Restaurant(1004L, "Mcdonald", "Seoul"));
		
		given(restaurantService.getRestaurants()).willReturn(restaurants);
		
		mvc.perform(get("/restaurants")).andExpect(status().isOk())
				.andExpect(content().string(containsString("\"id\":1004")))
				.andExpect(content().string(containsString("\"name\":\"Mcdonald\"")))
				.andExpect(content().string(containsString("\"address\":\"Seoul\"")));
	}
    
	@Test
	public void detail() throws Exception {
		Restaurant restaurant1 = new Restaurant(1004L, "Joker House","Seoul");
		restaurant1.addMenuItem(new MenuItem("BigMac"));
		Restaurant restaurant2 = new Restaurant(2020L, "Cyber Food","Seoul");
		
		given(restaurantService.getRestaurant(1004L)).willReturn(restaurant1);
		given(restaurantService.getRestaurant(2020L)).willReturn(restaurant2);
		
		mvc.perform(get("/restaurants/1004"))
			.andExpect(status().isOk())
			.andExpect(content().string(containsString("\"id\":1004")))
			.andExpect(content().string(containsString("\"name\":\"Joker House\"")))
			.andExpect(content().string(containsString("\"address\":\"Seoul\"")))
			.andExpect(content().string(containsString("BigMac")));
	
		mvc.perform(get("/restaurants/2020"))
		.andExpect(status().isOk())
		.andExpect(content().string(containsString("\"id\":2020")))
		.andExpect(content().string(containsString("\"name\":\"Cyber Food\"")))
		.andExpect(content().string(containsString("\"address\":\"Seoul\"")));
	}
}

목 객체를 사용하면 실제로 서비스가 어떻게 되있는냐와는 별개로 컨트롤러가 우리가 만들어준 가짜 처리를 통해서 작동하는 것을 확인할 수 있습니다. 예를 들어서 진짜 서비스에서는 무조건 Mcdonald를 돌려줘야하지만 저 부분을 "Joker House"로 변경하고 andExpect의 Mcdonald도 Joker House로 변경하면 통과가 됩니다.

이런식으로 우리가 테스트하려는 대상(컨트롤러)은 서비스를 활용한다는 것에만 집중하고 서비스가 실제로 어떻게 작동하느냐는 컨트롤러의 관심사가 아님을 알 수 있습니다. 우리가 가짜 객체를 사용함으로써 실제 서비스의 상태와 상관없이 원하는 테스트를 가능하게 해줍니다.

같은 방식으로 RestaurantServiceTest에서도 Repository는 실제로 테스트하려는 RestaurantService와 크게 관련이 없기 때문에 가짜 객체로 만들어 주겠습니다.

class RestaurantServiceTest {
	private RestaurantService restaurantService;
//	private RestaurantRepository restaurantRepository;
//	private MenuItemRepository menuItemRepository;

	@Mock
	private RestaurantRepository restaurantRepository;
	@Mock
	private MenuItemRepository menuItemRepository;

	@BeforeTestExecution
	public void setUp() {
		MockitoAnnotations.initMocks(this); // 현재 @Mock이 붙어있는 것들을 초기화해줌.
		mockRestaurantRepository();
		mockMenuItemRepository();
//		restaurantRepository = new RestaurantRepositoryImpl();
//		menuItemRepository = new MenuItemRepositoryImpl();
		restaurantService = new RestaurantService(restaurantRepository, menuItemRepository);
	}

	private void mockMenuItemRepository() {
		// TODO Auto-generated method stub
		List<MenuItem> menuItems = new ArrayList<>();
		menuItems.add(new MenuItem("BigMac"));
		given(menuItemRepository.findAllByRestaurantId(1004L)).willReturn(menuItems);
	}

	private void mockRestaurantRepository() {
		List<Restaurant> restaurants = new ArrayList<>();
		Restaurant restaurant = new Restaurant(1004L, "Joker House", "Seoul");
		restaurants.add(restaurant);

		given(restaurantRepository.findAll()).willReturn(restaurants);
		given(restaurantRepository.findById(1004L)).willReturn(restaurant);
	}

	@Test
	public void getRestaurant() {
		setUp();
		Restaurant restaurant = restaurantService.getRestaurant(1004L);

		assertThat(restaurant.getId(), is(1004L));
	}

	@Test
	public void getRestaurants() {
    	setUp();
		List<Restaurant> restaurants = restaurantService.getRestaurants();
		System.out.println(menuItemRepository.findAllByRestaurantId(1004L).get(0).getName());

		Restaurant restaurant = restaurants.get(0);
		assertThat(restaurant.getId(), is(1004L));
	}

}

가짜 객체를 통해 실제로 테스트하려는 부분 외의 부분을 통제할 수 있다면 앞으로 더욱 튼튼하고 견고한 프로그램을 만들 수 있습니다. 해당 내용은 완벽하게 이해가 되지 않았기 때문에 토비의 스프링의 내용을 참고하여 추가하도록 하겠습니다.

Comments