단위테스트에 @MockBean 활용하고 의존성 개선하기

주싱·2022년 9월 30일
2

더 나은 테스트

목록 보기
6/16

테스트 도메인

지상국 안테나의 위성 추적 기능을 직접 구현해야하는 상황이 생겼습니다. 위성이 지평선을 떠오르는 시간부터 매 초 안테나가 바라볼 고도각과 방위각 목록(이하 위성 추적 목록)을 생성해서 안테나를 연속적으로 구동해 주어야 합니다. 위성이 지상국을 한 번 지나가는 경로를 하나의 독립된 ‘패스’라고 하고 지상국은 한 패스 동안 위성과 데이터를 주고 받는 ‘미션’을 수행합니다. 저는 그 중에서 안테나를 제어하는 AntennaController라는 모듈을 개발하고 있습니다.

테스트할 메시지 흐름

AntennaController는 미션 시작 요청을 받으면 TrackingTableService 모듈에 위성 추적 목록 생성을 요청하고 응답된 목록을 기반으로 1초마다 안테나를 구동합니다. 사무실에서 안테나 장비에 직접 연결하여 테스트를 수행하기에는 많은 제약이 있어 Antenna 하드웨어 대신 시뮬레이터를 서비스에 내장(Embedded)하고 TCP 루프백 주소(127.0.0.1)를 사용해 테스트를 진행하고 있습니다.

테스트를 어렵게 하는 요소

위 메시지 흐름 가운데 테스트를 어렵게 하는 요소는 테스트의 입력인 위성 추적 목록을 의도한 초기값으로 설정해 주는 부분입니다. TrackingTableService는 위성 추적 목록 생성에 필요한 파라미터를 DB에서 읽어와 알고리즘의 입력으로 사용하고 계산된 결과를 반환해 줍니다. 따라서 위성 추적 목록을 특정 값으로 초기화 해주기 위해서는 DB에 저장된 파라미터 값을 의도적으로 설정해 주어야 했습니다. 제가 개발하는 모듈에서는 직접적인 DB 참조가 발생하지 않는데 의존하는 모듈을 실행시키기 위해 테스트 초기에 DB에 접근해야 했습니다.

Mocking

최근에 배운 Mocking이라는 개념이 생각났습니다. TrackingTableService는 제가 테스트하려는 대상이 아니며 단지 테스트 입력 값을 생성해 주는 역할을 하고 있습니다. 저는 서비스 코드는 그대로 유지하면서 TrackingTableService가 반환하는 위성 추적 목록을 특정 값으로 초기화 해주고 싶었는데 MockBean을 사용하면 딱인 것 같습니다. 그래서 TrackingTableService 빈에 @MockBean 어노테이션을 붙여주고 generate(id) 메서드가 특정 목록을 반환하도록 Mocking 했습니다. 이를 통해 서비스 코드의 변경 없이 내가 원하는 입력을 간단하게 고정해서 테스트를 진행할 수 있었습니다.

@ActiveProfiles("test")
@SpringBootTest(classes = {
        ApplicationContextProvider.class,
        AntennaController.class,
        AntennaSimulator.class,
})
class AntennaControllerTest {
		@MockBean
		TrackingTableService tracktingTableService;
		@Autowired
		AntennaController controller;
		@Autowired
		AntennaSimulator simulator;
		int passId = 1;
		LocalDateTime endTime;

		@BeforeEach
		void setUp() {
				// 위성 추적 목록 생성
				List<Position> trackingTable = new ArrayList();
				LocalDateTime startTime = LocalDateTime.now().plusSeconds(1);
				int second=0;
				trackingTable.add(new Position(startTime.plusSeconds(second++), 10, 100));
				trackingTable.add(new Position(startTime.plusSeconds(second++), 11, 101));
				....
				endTime = trackingTable.get(trackingTable.size()-1).getTime();

				// Mocking 처리
				Mockito.when(trackingTableService.generate(passId)).thenReturn(trackingTable);
		}

		@Test
		@Display("요청한 안테나 추적 목록과 시뮬레이터가 수신한 목록이 일치합니다")
		void trackingTest() {
				// 위청 추적 시작
				controller.startMission(passId);
				
				// 미션 종료 시간 대기 
				TryUntilSuccess.job(() -> {
					 	return LocalDateTime.now().isAfter(endTime);
				});

				// 시뮬레이터가 구동 요청 받은 목록 획득
				List<Position> actualTrackedList = simulator.getTrackedList();

				// 계산된 목록과 시뮬레이터가 수신한 목록이 동일한지 비교
				AssertEquals(trackingTable, actualTrackedList);
		}
}

내게도 잘못된 의존성이 없나?

최근에 이동욱님의 ‘@SpyBean @MockBean 의도적으로 사용하지 않기’라는 글을 접했습니다. 저도 혹 테스트의 효율성만을 생각해서 잘못된 의존관계를 방치하고 있는 것은 아닌지 다시 살펴보게 됩니다. 고민하고 있던 찰나에 TrackingTableService 모듈을 포함하는 서비스 파트 개발자가 제안을 해옵니다. “HwController에서 TrackingTableService에 접근하는 의존성을 가지는 것 자체가 이상한 것 같습니다. HwController 모듈에 위성 추적 목록을 직접 전달해 주겠습니다” 아, 그러고보니 그렇게되면 Mocking이 필요없습니다. HwController는 외부에서 입력 받은(누가 입력해 주는지는 알 필요가 없이) 위성 추적 목록으로 위성을 추적하는 일에 집중하면 됩니다. 그래서 아래와 같이 의존관계가 정리된 테스트 코드가 만들어졌습니다.

@ActiveProfiles("test")
@SpringBootTest(classes = {
        ApplicationContextProvider.class,
        AntennaController.class,
        AntennaSimulator.class,
})
class AntennaControllerTest {
		// @MockBean
		// TrackingTableService tracktingTableService; ← 의존관계 제거
		@Autowired
		AntennaController controller;
		@Autowired
		AntennaSimulator simulator;
		// int passId = 1; ← 불필요한 입력
		List<Position> trackingTable;
		LocalDateTime endTime;

		@BeforeEach
		void setUp() {
				// 위성 추적 목록 생성
				trackingTable = new ArrayList();
				LocalDateTime startTime = LocalDateTime.now().plusSeconds(1);
				int second=0;
				trackingTable.add(new Position(startTime.plusSeconds(second++), 10, 100));
				trackingTable.add(new Position(startTime.plusSeconds(second++), 11, 101));
				....
				endTime = trackingTable.get(trackingTable.size()-1).getTime();
		}

		@Test
		@Display("요청한 안테나 추적 목록과 시뮬레이터가 수신한 목록이 일치합니다")
		void trackingTest() {
				// 위청 추적 시작
				controller.startMission(trackingTable);

 				// 미션 종료 시간 대기 
				TryUntilSuccess.job(() -> {
					 	return LocalDateTime.now().isAfter(endTime);
				});

				// 시뮬레이터가 구동 요청 받은 목록
				List<Position> actualTrackedList = simulator.getTrackedList();

				// 계산된 목록과 시뮬레이터의 수신 목록이 동일한지 비교
				AssertEquals(trackingTable, actualTrackedList);
		}
}

정리하며

현업에서 Mocking이라는 걸 처음 써 보게 되었습니다. 잘 사용한다면 효과적으로 코드를 테스트할 수 있겠단 생각이 듭니다. 그러나 저 역시 잘못된 의존관계를 방치하고 테스트에만 집중할 뻔 했는데 동료의 조언 덕에 인지할 수 있었습니다. 동료가 시간이 좀 걸린다고 공유해 오네요. 그래서 일단 앞서 구현한 Mocking 방식의 테스트로 기능을 검증했습니다. 테스트 코드를 짜두었으니 추후 구조가 변경되어도 자신있게 코드를 개선할 수 있을거라 기대합니다. Mocking, 의존관계 개선, 테스트 코드를 먼저 작성함으로 자신있게 리팩토링 하기, 세 가지를 차례차례 현업에 적용해 볼 수 있어서 좋았습니다.

profile
소프트웨어 엔지니어, 일상

0개의 댓글