-
[Django][TDD] 클린 코드를 위한 테스트 주도 개발 - 1부 요약 정리Book 2025. 3. 15. 23:45
현재 '항해 플러스' 에서 진행하는 개발 블로그 동기 부여 모임인 '오늘은 써야지' 1기 멤버로 참여 중이다. (이에 대한 소개는 그 맨 마지막에!)
'오늘은 써야지' 의 이번 주 주제는 늘 공부해보고 싶었던 TDD 이다.
덕분에 그동안 미루기만 했던 Djgango + TDD 를 공부하게 되었다.(동기부여 지대로)
그렇게 사놓고 표지 구경만 했던 책 '클린 코드를 위한 테스트 주도 개발' 을 드디어 펼쳐 보았다.클린 코드를 위한 테스트 주도 개발 - 예스24
이 책은 웹 애플리케이션의 개발 과정 전반을 다룬다. 또한 애플리케이션을 구축하기에 앞서 어떻게 테스트 코드를 작성하고 실행해야 할지를 알려주고, 테스트 코드를 통과하기 위한 최소 기
www.yes24.com
하지만 TDD 라는 것이 딸랑 일주일 공부하고 완벽히 습득할 수 있는 개념은 아니기에...
이번 포스팅에선 우선 'TDD 의 개념 + Django 에서 TDD 를 시작하는 방법' 에 대해 작성해 보았다.1부 TDD와 Django 개요
3장. 단위 테스트를 이용한 간단한 홈페이지 테스트
- 기능 테스트(Feature Test, FT): 사용자 관점에서 기능을 사용하는 흐름(=시나리오)이 실패 없이 잘 수행되는지 확인하는 테스트. end-to-end. E2E. 앱 외부를 테스트
- 단위 테스트: 개발자 관점에서 각 Code가 의도한 대로 동작하는지 확인하는 테스트. 기능 테스트로부터 파생.(=기능 테스트의 각 Code line에 해당하는 단위 테스트를 작성한다. → 기능 테스트가 모두 통과될 때까지) 앱 내부를 테스트. 단위 테스트가 성공한 다음엔 반드시 기능 테스트를 수행해야 한다.
단위 테스트-코드 주기 를 따른 예시
- Django의 처리 흐름: 특정 URL에 대한 HTTP Request 받기 → Django의 URL 해석(요청 받은 URL에 어떤 view 함수를 실행할지 미리 정해놓은 규칙을 이용해 결정) >>단위 테스트를 해봐야 하는 부분 → 실행된 view 함수에서 요청을 처리하고 HTTP Response 반환
- 단위 테스트 작성:
from django.test import TestCase from django.urls import resolve from lists.views import home_page class HomePageTest(TestCase): def test_root_url_resolves_to_home_page_view(self): found = resolve('/') # Django의 'URL 해석' 결과를 받아서 self.assertEqual(found.func, home_page) # 원하는 view 함수와 연결되었는지 확인.
위 Test 결과 list.view의 home_page를 작성하기 전이므로 해당 요소를 import할 수 없다는 error가 발생함.
3. 발생한 error만을 처리하기 위한 최소한의 Code(lists.views.py 에 home_page=None 추가) 작성 다시 2 → 3 과정 반복
요약: ‘테스트 → 실패가 발생한 assertion 라인을 통과할 수 있는 최소한의 Code 작성 → 테스트’ 반복
4장. 왜 테스트를 하는 것인가?
TDD = 훈련
‘단위 테스트-코드’ 주기가 너무 짧다고 느껴져도 반복된 훈련으로 그 과정에 익숙해져야 한다. 왜냐하면 점진적으로 Code 가 쌓여 나도 모르는 새에 손댈 수 없을 정도로 복잡해졌을 때에는 뼈대도 없이 Test Code를 작성하기가 힘들다. 아주 작은 코드일 때부터 Test Code를 작성하고 한 줄 씩 수정 → 추가를 반복하는 것이 Test Code의 틀(=뼈대)를 만들어 가는 것이고 나아가 신뢰도 높고 안정적인 Code 작성의 지름길이다.
상수, 문자열은 테스트 하지 않기.
단위 테스트는 로직의 흐름, 제어 ,설정 등을 테스트하는 것이 목적이므로 변하지 않는 값들을 테스트하는 것은 무의미하다. 차라리 상수 값과 그 값을 반환해야 하는 개체를 비교하는 등, 다른 방식으로 테스트하는 방향이 더 바람직하다.
ex) response.content 에 예상되는 html code 가 있는지 확인하는 테스트를 할 것이 아니라, template 파일로부터 html을 받아와(render_to_string()
사용) 변수에 담고 이를 response.content와 비교. → 구현 결과물을 테스트하기!Refactoring = 기능(=결과)은 변경하지 않고 Code 만 개선하는 것.
Test 없이 Refactoring 할 수 없다. Refactoring과 Test Code 수정은 동시 진행이 아니라 한 번에 하나씩 진행해야 함. → 복잡도가 최소일 때부터 차근차근 진행하기 위해.
ex) Refactoring 진행 후 기존 테스트 실행 → 테스트 결과가 실패라면 Refactoring 된 Code를 제대로 활용하여 테스트 하도록 Test Code 를 수정 → Test Code 수정이 잘 되었고 그래도 테스트 결과가 실패라면 → 테스트를 통과하도록 App Code를 수정 (→ 두 번째부터 절차부터 반복)
위 내용을 바탕으로 TDD 프로세스의 전체적인 흐름도를 직접 그려보았다.
(그림)Django 에서 TDD 시작하기
TDD 를 위한 Django 프로젝트 구조를 다음과 같다.
기능 테스트를 위한 패키지는 별도로 두었고
단위 테스트는 각 앱 별로 진행되므로 앱 폴더 하위에tests.py
를 생성한다.기능 테스트는 실제 사용자의 행동을 모방할 수 있는 Selenium 모듈을 사용한다.
import os from selenium import webdriver from selenium.webdriver.common.keys import Keys from selenium.common.exceptions import WebDriverException from django.test import LiveServerTestCase from django.contrib.staticfiles.testing import StaticLiveServerTestCase import time MAX_WAIT = 5 class NewVisitorTest(StaticLiveServerTestCase): def setUp(self): ''' setUP 함수: 각 테스트 메서드가 실행되기 전에 호출되어 해당 테스트에 필요한 환경과 데이터를 초기화하는 역할 ''' self.browser = webdriver.Chrome() self.browser.implicitly_wait(3) staging_server = os.environ.get("STAGING_SERVER") if staging_server: self.live_server_url = f"http://{staging_server}" def tearDown(self): ''' tearDown 함수: 각 테스트 메서드 종료 후, 테스트 중에 생성된 데이터나 임시 자원을 정리하는 역할 ''' self.browser.quit() def test_can_start_a_todo_list(self): # 사용자는 To-Do 웹 사이트에 방문한다. self.browser.get(self.live_server_url) # To-Do 웹 사이트의 타이틀 헤더엔 "To-Do" 가 포함 되어 있다. self.assertIn("To-Do", self.browser.title) header_text = self.browser.find_element("tag name", "h1").text self.assertIn("To-Do Lists", header_text) # 사용자는 작업을 추가하기로 한다. input_box = self.browser.find_element(value="id_new_item") self.assertEqual(input_box.get_attribute("placeholder"), "작업 아이템 입력") # "공작 깃털 사기" 라는 텍스트 상자에 추가한다. input_box.send_keys("공작 깃털 사기") # 엔터키를 치면 페이지가 갱신되고 작업 목록에 '1: 공작 깃털 사기' 아이템이 추가된다. input_box.send_keys(Keys.ENTER) self.wait_for_row_in_list_table("1: 공작 깃털 사기") # 추가 아이템을 입력할 수 있는 여분의 텍스트 상자가 존재한다. input_box = self.browser.find_element(value="id_new_item") # 이번엔 "공작 깃털을 이용해 그물 만들기"라고 텍스트 상자에 입력하고 엔터키를 친다. input_box.send_keys("공작 깃털을 이용해 그물 만들기") input_box.send_keys(Keys.ENTER) # 페이지는 다시 갱신되고 두 개의 작업 목록이 존재한다. self.wait_for_row_in_list_table("1: 공작 깃털 사기") self.wait_for_row_in_list_table("2: 공작 깃털을 이용해 그물 만들기")
test_can_start_a_todo_list()
의 주석을 읽어보면 실제 사용자가 웹 페이지를 사용할 때 하는 행동을 자연스러운 흐름 순으로 작성되었음을 확인할 수 있다. 기능테스트는 어디까지나 사용자 관점에서 테스트를 수행하는 것이기 때문이다.다음은 단위 테스트 코드이다.
from django.test import TestCase from django.urls import resolve from lists.views import home_page from lists.models import Item, List class ListItemModelTest(TestCase): def test_saving_and_retrieving_items(self): mylist = List() mylist.save() first_item = Item() first_item.text = "첫 번째 아이템" first_item.list = mylist first_item.save() second_item = Item() second_item.text = "두 번째 아이템" second_item.list = mylist second_item.save() saved_items = Item.objects.all() self.assertEqual(saved_items.count(), 2) saved_list = List.objects.get() self.assertEqual(saved_list, mylist) first_saved_item = saved_items[0] self.assertEqual(first_saved_item.text, "첫 번째 아이템") self.assertEqual(first_saved_item.list, mylist) second_saved_item = saved_items[1] self.assertEqual(second_saved_item.text, "두 번째 아이템") self.assertEqual(second_saved_item.list, mylist)
위 코드는 List 란 모델을 이용하여 저장 기능을 테스트하기 위해 작성되었다.
데이터 저장을 위해 DB가 필요한 상황인데, 이럴 경우 Django 가 제공하는
TestCase
클래스를 사용하면 된다. TestCase 는 임시 DB를 생성하여 테스트 중 데이터 변경이 실제 환경에 영향을 미치지 않도록 해주기 때문이다. 생성된 임시 DB 는 테스트 종료 후tearDown()
이 호출되는 시점에 삭제가 된다. 따라서 테스트를 여러 번 수행하여도 각 테스트는 서로 영향을 미치지 않는 독립된 상태를 가지게 된다.작성된 테스트 코드를 실행하기 위한 명령어는 다음과 같다.
- 전체 테스트 실행:
python manage.py test
- 특정 앱만 테스트:
python manage.py test 앱이름
- 기능 테스트 실행:
python 기능테스트모듈명.py
(위 설명일 기준으로 기능테스트 코드는 앱 디렉터리 외부에 있으므로 단위테스트 실행 명령어와는 상이한 명령어 구조를 갖는다.)
PyCharm 에서는 Run/Debug Configuration 에서 Django tests 를 추가하면 된다. 추가하는 방법은 Django server 를 추가하는 것과 동일하다.
소감
TDD 를 공부하고 직접 실습을 진행해 보니 테스트 코드의 중요성을 이해하는 한편, 많은 기업에서 테스트 코드를 생략하는지도 이해할 수 있었다.
짧게 여러 번 순환하는 TDD 프로세스를 처음 따라할 때 매우 귀찮고, 혼란스럽고, 자꾸 까먹는 일을 반복해야 했다. 이미 TDD에 익숙한 상태가 아니라면 실무에서는 업무 효율성을 위해 TDD 적응 기간 대신 당장의 기능 구현을 택하리라.반대로 말한다면, TDD 에 능숙한 상태인 경우 빠르게 테스트 코드를 작성하고 업무에 적용하여 코드의 완성도를 높일 수 있다는 의미이기도 할 것이다.
만나는 개발자들의 태반이 '저희는 테스트 코드를 안써요.' 이지만, 그럼에도 간혹 '테스트 코드 쓰고 있죠.' 라는 대답을 들을 때마다 부러운 마음이 든다는 것은 결국 내가 하고 싶은 일이 무엇인지 알려주는 것과 같다. 아직 남아있는 TDD 를 공부하고 테스트에 능숙한 개발자가 되자.'항해 플러스' 간단 소개
https://hanghae99.spartacodingclub.kr/
개발자 커리어 개척 캠프 항해99, 첫 취업부터 현직자 역량 강화까지
10년이 지나도 남는 커리큘럼을 바탕으로 커리어를 개척하세요. 진정성있는 멘토링과 2천 명이 넘는 끈끈한 커뮤니티가 여러분과 함께 합니다.
hanghae99.spartacodingclub.kr
간단히 말하면 '항해99'에서 진행하는 주니어 개발자들을 위한 부트캠프이다.
- 현업의 멘토들과 기대 이상으로 더 가까이 대화하고 배움을 얻을 수 있다.
- 수료생들은 여러 세미나와 모임에 대한 정보를 우선적으로 제공받을 수 있다.
'오늘은 써야지' 모임 역시 항해 플러스 수료 이후에 꾸준히 제공된 정보를 통해 지원할 수 있었다.
끝으로 내가 항해 플러스를 지원하기로 마음먹은 결정타 멘트를 소개한다.
팀에 사수가 없어서 조언이나 코드 리뷰를 받기 어려워요.
소개 글을 쓰는 지금 항해플러스의 새로운 기수가 모집 중이다. (~3/18 11:00까지)
지원 시 전 수료생의 추천인 코드를 입력하면 20만원 할인 혜택을 받을 수 있다.
항해플러스 추천 할인코드: 6g4pYi'Book' 카테고리의 다른 글
[클론코딩] 예제로 배우는 Django 4: 블로그 (1) #책과_현실_다른_점_찾기 (0) 2025.01.31 읽기 좋은 코드가 좋은 코드다. - 10 상관없는 하위문제 추출하기 (0) 2023.09.23 읽기 좋은 코드가 좋은 코드다. - 09 변수와 가독성 (0) 2023.09.17 [CS][읽는 중]혼자 공부하는 컴퓨터구조 + 운영체제 (0) 2023.08.23 [Django] 배프의 오지랖 파이썬 웹프로그래밍 (0) 2023.08.14