개발자들에게 테스트 코드는 계륵과 같은 존재입니다. 작성을 안 하면, 실제 프로덕션을 할 때 안 돌아가는 경우가 생기고, 하나하나 세세한 부분을 나누어서 단위 테스트로써 테스트 코드를 나누자니, 시간이 오래걸립니다. 오늘은 이 테스트 코드를 활용하여, 개발하는 방식중 하나인 Tdd와 Mockito에 대해서 공부해보는 시간을 가지겠습니다.
애자일 개발 프로세스는 기존의 모든 계획을 수립후,
개발에 들어가는 폭포수 방법론과는 달리,
개발 즉시 피드백을 받아서 수정해나가는 능동적인 프로세스입니다.
이 중 익스트림 프로그래밍은 이 프로세스를 원할히 할 수 있는 프로그래밍 방법론입니다.
그렇다면 한 번 실제로 Tdd를 적용해보고, 장단점을 비교해볼까요?
https://github.com/Munhangyeol/Tdd
@SpringBootTest
public class CalculateTest {
@Autowired
public CalculateService calculateService;
private double x1;
private double x2;
@Test
public void plusTest(){
//given
x1=random();
x2=random();
//when,then
Assertions.assertEquals(calculateService.calculate(x1, x2, "+"),x1+x2);
}
}
package tdd.tdd;
import org.springframework.stereotype.Service;
@Service
public class CalculateService {
public double calulate(double x1,double x2,String operater){
return x1+x2;
}
}
public double calulate(double x1,double x2,String operater){
if(operater.equals("+"))
return x1+x2;
return 0;
}
@Test
public void minusTest(){
//given
x1=random();
x2=random();
//when,then
Assertions.assertEquals(calculateService.calculate(x1, x2, "-"),x1-x2);
}
package tdd.tdd;
import org.springframework.stereotype.Service;
@Service
public class CalculateService {
public double calculate(double x1,double x2,String operater){
if(operater.equals("+"))
return x1+x2;
if(operater.equals("-"))
return x1 - x2;
return 0;
}
}
package tdd.tdd;
import org.springframework.stereotype.Service;
@Service
public class CalculateService {
public double calculate(double x1,double x2,String operater){
switch (operater) {
case "+":
return x1+x2;
case "-":
return x1-x2;
default:
return 0;
}
}
}
@Test
public void multipeTest(){
//given
x1=random();
x2=random();
//when,then
Assertions.assertEquals(calculateService.calculate(x1, x2, "x"),x1*x2);
}
@Service
public class CalculateService {
public double calculate(double x1,double x2,String operater){
switch (operater) {
case "+":
return x1+x2;
case "-":
return x1-x2;
case "x":
return x1*x2;
default:
return 0;
}
}
}
public double calculate(double x1,double x2,String operater){
return switch (operater) {
case "+" -> x1 + x2;
case "-" -> x1 - x2;
case "x" -> x1 * x2;
default -> 0;
};
}
@Test
public void divideTest(){
//given
x1=random();
x2=random();
//when,then
Assertions.assertEquals(calculateService.calculate(x1, x2, "/"),x1/x2);
}
public double calculate(double x1,double x2,String operater){
return switch (operater) {
case "+" -> x1 + x2;
case "-" -> x1 - x2;
case "x" -> x1 * x2;
case "/"->x1/x2;
default -> 0;
};
}
package tdd.tdd;
import org.springframework.stereotype.Service;
@Service
public class CalculateService {
public double calculate(double x1,double x2,String operater){
return switch (operater) {
case "+" -> plus(x1, x2);
case "-" -> minus(x1,x2);
case "x" -> multiple(x1,x2);
case "/"->divide(x1,x2);
default -> throw new IllegalStateException("Unexpected value: " + operater);
};
}
public double plus(double x1,double x2){
return x1 + x2;
}
public double minus(double x1,double x2){
return x1 - x2;
}
public double multiple(double x1,double x2){
return x1 * x2;
}
public double divide(double x1,double x2){
return x1 / x2;
}
}
@Slf4j
@Component
public class InfreanVideoCrawling {
public ArrayList<InfreanVideoDTO> crawlingInfreanVideo(String teck_stack){
WebDriver driver = getWebDriver(teck_stack);
ArrayList result = new ArrayList<InfreanVideoDTO>();
try {
//WebElement 추출
List<WebElement> lectures = getLectures(driver);
// 각 강의 요소를 순회하며 데이터 추출
for (WebElement lecture : lectures) {
if (result.size() >= 12) {
break;
}
try {
String thumbnailUrl = getUrl(lecture, "div.mantine-AspectRatio-root img", "src", "No image");
// 강의 URL 추출
String lectureUrl = getUrl(lecture, "a", "href", "No URL");
// 강의 제목 추출
String title = getTitle(lecture.getText());
// 가격 추출
String price=getPrice(lecture.getText());
// 결과 출력
log.info("InfreanCrawling result: " + "thumnail: " + thumbnailUrl + " url: " + lectureUrl +
" title: " + title + " price: " + price);
InfreanVideoDTO infreanVideoDTO = new InfreanVideoDTO(lectureUrl,title,thumbnailUrl,Long.valueOf(price.replaceAll("[^\\d]", "")));
result.add(infreanVideoDTO);
} catch (org.openqa.selenium.NoSuchElementException e) {
log.info("Element not found in this lecture element: " + e.getMessage());
}
}
} catch (Exception e) {
log.error("An unexpected error occurred: " + e.getMessage(), e);
}
driver.quit();
return result;
}
private String getUrl(WebElement lecture, String cssSelector, String src, String x) {
// 썸네일 이미지 URL 추출
WebElement thumbnailElement = lecture.findElement(
By.cssSelector(cssSelector));
String thumbnailUrl = thumbnailElement != null ? thumbnailElement.getAttribute(src) : x;
return thumbnailUrl;
}
private String getPrice(String lectureText) {
// 가격은 "원"이 마지막에 있고, 숫자로 변환했을 때, 숫자 인것만 반환함
String[] lines = lectureText.split("\n");
for (String line : lines) {
if (line.trim().endsWith("원")&&line.replaceAll("[^\\d]", "")!="") {
return line.replaceAll("[^\\d]", "");
}
}
return "0";
}
private String getTitle(String lectureText){
String[] lines = lectureText.split("\n");
for (String line : lines) {
if (line.charAt(0) == '할' && line.charAt(1) == '인') {
return lines[1];
} else {
return line;
}
}
return "0";
}
private List<WebElement> getLectures(WebDriver driver) {
// 페이지가 로드될 때까지 잠시 대기 (필요에 따라 조정)
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(5));
wait.until(ExpectedConditions.presenceOfElementLocated(By.cssSelector("ul.css-y21pja li.mantine-1avyp1d")));
// 강의 목록 요소를 선택
List<WebElement> lectures = driver.findElements(By.cssSelector("ul.css-y21pja li.mantine-1avyp1d"));
return lectures;
}
public WebDriver getWebDriver(String teck_stack) {
WebDriverManager.chromedriver().setup();
// System.setProperty("chrome.driver", "/chromedriver.exe");
ChromeOptions options = new ChromeOptions();
options.addArguments("--headless");
options.addArguments("--no-sandbox");
options.addArguments("--disable-dev-shm-usage");
WebDriver driver = new ChromeDriver(options);
/*
중략......
/*
driver.get(INFREAN_CRAWRLING_URL_SEARCH+teck_stack+"&types=ONLINE");
return driver;
}
}
private final VideoRepository videoRepository;
private final YoutubeVideoFetcher youtubeVideoFetcher;
private final UdemyVideoFetcher udemyVideoFetcher;
private final InfreanVideoFetcher infreanVideoFetcher;
private final TechnologyStackRepository technologyStackRepository;
public VideoService(VideoRepository videoRepository, YoutubeVideoFetcher youtubeVideoFetcher,
UdemyVideoFetcher udemyVideoFetcher, InfreanVideoFetcher infreanVideoFetcher, TechnologyStackRepository technologyStackRepository) {
this.videoRepository = videoRepository;
this.youtubeVideoFetcher = youtubeVideoFetcher;
this.udemyVideoFetcher = udemyVideoFetcher;
this.infreanVideoFetcher = infreanVideoFetcher;
this.technologyStackRepository = technologyStackRepository;
}
@ExtendWith(MockitoExtension.class)
public class VideoServiceTest {
@InjectMocks
private VideoService videoService;
@Mock
private VideoRepository videoRepository;
@Mock
private RestTemplate restTemplate;
@Mock
private YoutubeVideoFetcher youtubeVideoFetcher;
@Mock
private UdemyVideoFetcher udemyVideoFetcher;
@BeforeEach
public void setUp() {
}
@DisplayName("가짜 유튜브 비디오 객체를 생성하여,Fetch하고 Save하는 과정이 13번 진행되나 테스트한다.")
@Test
public void testFetchAndSaveYoutubeVideos() {
// Given
YouTubeApiResponse response = getMockYouTubeApiResponse();
when(youtubeVideoFetcher.fetchYoutubeVideos(anyString())).thenReturn(response);
//When
videoService.fetchAndSaveYoutubeVideos();
// Then
//Tehcnology stack name이 총 13개임으로 13번 호출 되는 것이 맞음.
verify(videoRepository, times(13)).save(any());
}
//가짜 YoububeApiResponse를 가져온다.
private YouTubeApiResponse getMockYouTubeApiResponse() {
Item item = new Item();
setYoutubeItem(item);
YouTubeApiResponse response = new YouTubeApiResponse();
response.setItems(Collections.singletonList(item));
return response;
}
private YouTubeApiResponse getMockYouTubeApiResponseWithVideoIdIsNull() {
// ReflectionTestUtils.setField(videoService, "youtubeApiKey", "fakeApiKey");
Item item = new Item();
setYoutubeItemWithVideoIdIsNull(item);
YouTubeApiResponse response = new YouTubeApiResponse();
response.setItems(Collections.singletonList(item));
return response;
}
//가짜 YoububeItem을 생성한다.
private Item setYoutubeItem(Item item) {
Id id = new Id();
id.setVideoId("video123");
item.setId(id);
Snippet snippet = new Snippet();
snippet.setTitle("Sample Video");
Thumbnails thumbnails = new Thumbnails();
Thumbnail thumbnail = new Thumbnail();
thumbnail.setUrl("Sample Thumnail");
thumbnails.setDefault(thumbnail);
snippet.setThumbnails(thumbnails);
item.setSnippet(snippet);
return item;
}
//videoId가 없는 가짜 YoububeItem을 생성한다.
private Item setYoutubeItemWithVideoIdIsNull(Item item) {
Snippet snippet = new Snippet();
snippet.setTitle("Sample Video");
Thumbnails thumbnails = new Thumbnails();
Thumbnail thumbnail = new Thumbnail();
thumbnail.setUrl("Sample Thumnail");
thumbnails.setDefault(thumbnail);
snippet.setThumbnails(thumbnails);
item.setSnippet(snippet);
return item;
}
}
when(youtubeVideoFetcher.fetchYoutubeVideos(anyString())).thenReturn(response)
→when문을 통해서, 해당 mock객체의 return값을 위와같이 제어할 수 있습니다.(given도 있으나, 비슷한 기능을 합니다.verify(videoRepository, times(13)).save(any())
→verify를 통해서 실제test가 잘 되었는지를 확인(then)TDD는 코드의 품질을 향상시키고 리팩토링에 대한 두려움을 줄여주는 강력한 개발 방식입니다. 반복적인 테스트와 리팩토링 과정을 통해 신뢰성 있는 코드를 유지할 수 있으며, 테스트 코드 자체가 문서 역할을 하여 유지보수에도 도움이 됩니다. 하지만 TDD는 단순하거나 명확한 로직에서는 다소 지루하고 시간이 오래 걸릴 수 있습니다. 따라서 TDD는 불확실하거나 디버깅이 많이 필요한 복잡한 기능 구현에 특히 유용합니다.
Mockito는 이러한 TDD와 결합하여 독립적인 테스트 환경을 제공해, 의존성이나 외부 시스템과 무관하게 특정 로직만을 검증할 수 있도록 해줍니다. 이를 통해 빠르고 효율적인 테스트가 가능하며, 특히 API 호출 등 외부 연동이 포함된 부분에서 큰 이점을 제공합니다.
따라서 TDD와 Mockito를 적절히 활용하면 안정적이고 확장 가능한 코드를 지속적으로 유지할 수 있습니다.
https://www.youtube.com/watch?v=n01foM9tsRo&t=1053s
https://tech.kakaopay.com/post/implementing-tdd-in-practical-applications/
https://www.youtube.com/watch?v=3LMmPXoGI9Q&t=362s