테스트하기 좋은 코드로 리팩토링
🎬 Intro
사이드플젝인 OOTC에서 사용하는 코드를 테스트 하기 좋은 코드로 리팩토링 해보겠습니다.
✅ Before
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
@Component
@Slf4j
public class NationalForecastRegionReader {
public List<Region> read() {
try {
ClassPathResource resource = new ClassPathResource("national_forecast_regions.csv");
CSVReader csvReader = new CSVReader(new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8));
List<Region> regions = new ArrayList<>();
String[] fields;
// 첫 번째 줄(헤더)을 읽고 무시
csvReader.readNext();
while ((fields = csvReader.readNext()) != null) {
if (fields.length < 6) {
continue; // 잘못된 형식의 라인은 무시합니다.
}
Region region = convertToRegion(fields);
regions.add(region);
}
return regions;
} catch (Exception e) {
log.error(e.getMessage());
throw new RuntimeException("national_forecast_regions.csv을 읽어오는데 실패하였습니다.");
}
}
private Region convertToRegion(String[] fields) {
String code = fields[0];
String city = fields[1];
String district = fields[2];
String neighborhood = fields[3];
String nx = fields[4];
String ny = fields[5];
Address address = new Address(code, city, district, neighborhood);
return Region.builder()
.address(address)
.nx(nx)
.ny(ny)
.build();
}
}
- 위 코드는 해당 클래스의 메서드에서
ClassPathResource
,CSVReader
를 정의하고 있어서 강결합 상태입니다. - 이 경우에는 테스트 코드 작성 시 Mocking을 하는것이 불가능 하므로 외부에서 주입받는 형태로 리팩토링해야합니다.
✅ After
1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class FileReader {
@Value("${national-forecast-regions}")
private String fileName;
@Bean
@Qualifier("national_forecast_regions")
public CSVReader csvReader() throws Exception {
ClassPathResource resource = new ClassPathResource(fileName);
return new CSVReader(new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8));
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@Component
@Slf4j
public class NationalForecastRegionReader {
private final CSVReader csvReader;
public NationalForecastRegionReader(@Qualifier("national_forecast_regions") CSVReader csvReader) {
this.csvReader = csvReader;
}
public List<Region> read() {
try {
List<Region> regions = new ArrayList<>();
String[] fields;
// 첫 번째 줄(헤더)을 읽고 무시
csvReader.readNext();
while ((fields = csvReader.readNext()) != null) {
if (fields.length < 6) {
continue; // 잘못된 형식의 라인은 무시합니다.
}
Region region = convertToRegion(fields);
regions.add(region);
}
return regions;
} catch (Exception e) {
log.error(e.getMessage());
throw new RuntimeException("national_forecast_regions.csv을 읽어오는데 실패하였습니다.");
}
}
private Region convertToRegion(String[] fields) {
String code = fields[0];
String city = fields[1];
String district = fields[2];
String neighborhood = fields[3];
String nx = fields[4];
String ny = fields[5];
Address address = new Address(code, city, district, neighborhood);
return Region.builder()
.address(address)
.nx(nx)
.ny(ny)
.build();
}
}
- csvReader를 빈으로 등록하고, 이를 생성자 주입을 통해 외부에서 주입받는 형태로 리팩토링하였습니다.
- 참고로 어떠한 빈에 생성자가 오직 하나만 있고, 생성자의 파라미터 타입이 빈으로 등록가능한 존재라면 이 빈은
@Autowired
애노티에션 없이도 자동으로 의존성 주입이 됩니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// @SpringBootTest 를 붙여도 되지만 그러면 모든 스프링 컨텍스트가 로드 되기 때문에 느려진다.
@ExtendWith(MockitoExtension.class)
class NationalForecastRegionReaderTest {
@InjectMocks // @Mock으로 선언된 얘들을 @InjectMocks에 붙은곳에 자동으로 선언해준다.
private NationalForecastRegionReader nationalForecastRegionReader;
@Mock
private CSVReader csvReader;
@BeforeEach
public void setUp() throws Exception {
when(csvReader.readNext()).thenReturn(
new String[]{"code", "city", "district", "neighborhood", "nx", "ny"},
new String[]{"001", "서울특별시", "강남구", "삼성동", "60", "127"},
new String[]{"002", "부산광역시", "해운대구", "정동", "98", "76"},
null
);
}
@Test
public void testRead_정상_케이스() {
List<Region> regions = nationalForecastRegionReader.read();
assertEquals(2, regions.size());
assertEquals("001", regions.get(0).getAddress().getCode());
assertEquals("서울특별시", regions.get(0).getAddress().getCity());
assertEquals("강남구", regions.get(0).getAddress().getDistrict());
assertEquals("삼성동", regions.get(0).getAddress().getNeighborhood());
assertEquals("60", regions.get(0).getNx());
assertEquals("127", regions.get(0).getNy());
}
@Test
public void testRead_잘못된_형식의_라인_무시() throws Exception {
when(csvReader.readNext()).thenReturn(
new String[]{"code", "city", "district", "neighborhood", "nx", "ny"},
new String[]{"001", "서울특별시", "강남구", "삼성동", "60", "127"},
new String[]{"002", "부산광역시"},
null
);
List<Region> regions = nationalForecastRegionReader.read();
assertEquals(1, regions.size());
assertEquals("001", regions.get(0).getAddress().getCode());
assertEquals("서울특별시", regions.get(0).getAddress().getCity());
assertEquals("강남구", regions.get(0).getAddress().getDistrict());
assertEquals("삼성동", regions.get(0).getAddress().getNeighborhood());
assertEquals("60", regions.get(0).getNx());
assertEquals("127", regions.get(0).getNy());
}
@Test
public void testRead_예외_발생() throws Exception {
when(csvReader.readNext()).thenThrow(new RuntimeException("파일 읽기 실패"));
RuntimeException exception = assertThrows(RuntimeException.class, () -> {
nationalForecastRegionReader.read();
});
assertThat(exception.getMessage()).isEqualTo("national_forecast_regions.csv을 읽어오는데 실패하였습니다.");
}
}
csvReader가 외부에서 주입받는 형태로 리팩토링 되었기 때문에 Mocking이 가능해졌습니다.
@ExtendWith(MockitoExtension.class)
- 목적: Mockito를 사용한 단위 테스트(Unit Test) 설정.
- 사용 용도: 주로 단위 테스트에서 Mockito를 활용해 모의(Mock) 객체를 주입하고 테스트할 때 사용됩니다.
- 범위: 스프링 컨텍스트를 로드하지 않으며, 필요한 객체만 모의하여 테스트하는 데 사용됩니다.
- 속도: 스프링 컨텍스트를 로드하지 않으므로 테스트가 매우 빠르게 실행됩니다.
@SpringBootTest
- 목적: 스프링 부트 애플리케이션 컨텍스트를 로드하고 통합 테스트(Integration Test)를 수행.
- 사용 용도: 애플리케이션의 전체 컨텍스트를 로드하여 실제 빈(bean)들을 테스트할 때 사용됩니다.
- 범위: 스프링 컨텍스트를 포함한 애플리케이션 전체를 로드합니다.
- 속도: 전체 애플리케이션 컨텍스트를 로드하므로 테스트 실행 속도가 느릴 수 있습니다.
This post is licensed under CC BY 4.0 by the author.