클린 코드를 위한 테스트 주도 개발 1.5

전호종·2021년 3월 10일
0

TDD

목록 보기
2/6

POST 요청을 전송하기 위한 폼(Form) 연동

앞 장의 마지막 테스트 결과에서 사용자 입력을 저장할 수 없다는 메시지가 출력됐다.

브라우저가 데이터를 서버에 전송하려면 < input > 태그에 name 속성을 지정하고 < form > 태그로 감싸야 한다. 이때 method = 'POST'속성으로 전송 방식을 설정한다.

템플릿 파일 수정

테스트 및 오류 확인
저자 : 예상하지 못한 오류가 확인 되면 다음과 같은 사항을 디버깅한다.

  • print문을 사용해서 현재 페이지 텍스트 등을 확인
  • 에러 메세지를 개선해서 더 자세한 정보를 출력
  • 수동으로 사이트를 열어본다.
  • time.sleep을 이용해서 실행 중에 있는 테스트를 잠시 정지시킨다.

수정
에러가 발생하는 위치 앞에 time.sleep을 추가
(사실, 짧은 시간이지만 django에서 제공하는 디버깅 정보가 브라우저에 보이긴 한다.)

다시 테스트
django에서 제공하는 디버깅 정보 확인

CSRF(Cross-Site Request Forgery)
django의 CSRF 보호는 사이트의 각 폼이 생성하는 POST 요청을 확인할 수 있는 토큰을 자동 생성한다. 지금까지는 순수 HTML로 이루어진 템플릿을 사용했지만, 이제부터는 django의 템플릿 마술을 적용하도록 한다. CSRF 토큰을 추가하기 위해서 템플릿 태그를 이용한다.

수정
django는 CSRF 토큰을 포함하는 < input type='hidden'> 요소로 변경해서 렌더링한다.

예상된 오류
폼을 이용해 서버에 데이터를 제출했지만 아직 완벽하지 못하다. 제출 되었기 때문에 작성한 신규 아이템 텍스트가 사라지는 것을 확인할 수 있다. 동작을 확인 했으면 time.sleep은 제거한다.

서버에서 POST 요청 처리

action 속성을 지정하지 않았기 때문에 자동으로 현재 URL을 전달해서 기본 설정된 페이지를 다시 표시한다. 현재 URL과 맵핑된 함수는 home_page다.

단위 테스트 코드 작성

예상에 없는 오류
책과 버전이 달라 render_to_string(home.html)의 결과가 {% csrf_token %}을 공백처리해서 오류가 난다. stackoverflow를 참고 했다.

render_to_string의 마지막 인자에 request = request 를 추가해주면 된다는데 이 방법은 csrf_token 자체는 동작하지만 결과적으로 value 값이 다르게 나온다.

코드 수정

임시방편으로 csrf를 제거하는 함수를 만들어준다.

예상된 오류 확인

파이썬 변수를 전달해서 템플릿에 출력하기

템플릿 구문을 이용하면 파이썬 뷰 코드에 있는 변수를 HTML 템플릿에 전달할 수 있다.
{{...}} 형태로 사용하며 객체를 문자열로 출력한다.

코드 수정
주석 처리한 부분이 왜 필요한지 이해가 가지 않는다. 책과는 다르게 코드를 작성했다.
그리고 테스트가 중복된다고 느껴진다.

오류 확인
response_decode에는 아직 뷰를 통해 데이터를 넘겨주지 않았기 때문에 값이 공백이고
expected_html에는 테스트 코드에서 값을 넘겨 주었기 때문에 '신규 작업 아이템'이라는 값이 있어 오류가 발생한다.

코드 수정
views.py - 사용자에게 POST 방식으로 받은 데이터를 new_item_text라는 변수를 사용해 템플릿에 넘겨준다.

오류 확인
사용자가 입력한 값이 없어서 오류가 발생했다.

값이 없으면 비어있는 문자열을 반환할 수 있도록 get 함수를 사용한다.

그럼 단위테스트 통과를 확인할 수 있다.

기능테스트 오류 확인

오류 메세지를 구체적으로 개선해보자.

하지만 필자는 더 간단한 방법을 소개한다.

"1: "로 시작하는 첫 번쨰 아이템을 원한다는 것이다. 이 문제를 통과하도록 하는 가장 빠른 방법은 템플릿에 약간의 편법을 사용하는 것이다.

그럼 기능테스트가 통과할 것이다. 하지만 필자가 편법이라는 표현을 사용한 이유가 있다.
두 번째 아이템을 작성하는 테스트를 통과하려면 "2: "를 또 작성해야한다. 즉 "2: "로 시작하는 두 번째 아이템이 추가되더라도 이것을 확인할 수 있도록 FT를 확장하는 것으로 해야한다.

기능테스트 수정
두 번째 작업을 입력하고 확인하는 테스트 코드 작성

스트라이크 세 개면 리팩터

무언가 나쁜 "코드 냄새"를 방금 FT를 통해 맡을 수 있다. 현재 목록 테이블에서 신규 아이템을 확인하는 세 개의 코드가 존재하는데 모두 거의 동일한 코드다. DRT(Don't Repeat Yourself)라는 원리가 있는데, 스트라이크 세 개면 리팩터 이론과 일맥상통하는 것으로 지금 필요한 원리다. 즉 한 번 정도는 복사-붙여 넣기를 해줄 수 있지만, 같은 코드가 세 번 등장하게 되면 중복을 제거해야 한다는 이론이다.

현재 하나의 아이템만 저장할 수 있는 상태지만 커밋을 한다.
리팩터링 전에는 반드시 커밋을 해야 한다는 것이다.


오류 확인

Django ORM과 첫 모델

객체 관계형 맵핑(Object-Relational Mapper, ORM)은 데이터베이스의 테이블, 레코드, 컬럼 형태로 저장돼 있는 데이터를 추상화한 것이다. 이것을 이용하면 익숙한 객체 지향 코딩 방식을 이용해서 데이터베이스를 처리할 수 있다.데이터 베이스는 클래스로 표현하고 , 컬럼은 속성, 레코드는 각 클래스의 인스턴스로 표현한다.

코드작성
lists/test.py에 새로운 클래스를 추가한다. 데이터베이스에 새로운 레코드를 비교적 쉽게 생성할 수 있는 것을 볼 수 있다. 객체를 만들어서 속성을 부여하고 .save() 함수만 호출하면 된다. Django에서 API를 제공해서 클래스 속성인 .objects를 통해 데이터베이스를 쿼리할 수 있다.
가장 간단한 쿼리인 .all()을 이용해서 테이블에 있는 모든 레코드를 추출하고 있다. 결과는 QuerySet이라 불리는 리스트 형태의 객체로 반환된다.

단위 테스트 / 코드 주기
오류
단위테스트를 실행하면 정의된 Item이 없기 때문에 에러가 발생한다.

수정
lists/models.py에 모델 클래스를 정의한다.

오류

수정

오류
다음에 발생한 오류는 데이터베이스 에러다.
django.db.utils.OperationalError: no such table: lists_item

Django에선 ORM의 역활이 데이터베이스 모델을 만드는 것이다. 하지만 실제로 데이터베이스 구축을 담당하는 두 번째 시스템이 있다. 마이그레이션(Migration)이라는 것이다. 이것은 models.py 파일에 적용된 내용을 기반으로 사용자가 테이블과 칼럼을 삭제 및 추가할 수 있도록 해준다.

데이터베이스를 위한 버전 관리 시스템이라고 생각하면 이해하기 쉽다.

makemigrations 명령을 이용해 데이터베이스 마이그레이션을 구축하면 된다.

오류

수정

오류
컬럼을 확인할 수 없다. 새로운 컬럼을 정의했으면 마이그레이션을 생성해야 한다.

마이그레이션 명령어를 실행하면 초깃값을 설정해야한다.

deafult 값을 설정하고 다시 마이그레이션을 한다.

테스트 통과
두 개의 데이터베이스 마이그레이션을 생성했다. 결과적으로 모델 객체에 있는 .text 속성이 특수한 속성으로 인식돼서 데이터베이스에 저장된다.

커밋

$ git commit -m '아이템용 모델과 마이그레이션 연계'

POST를 데이터베이스에 저장하기

POST 요청을 위한 테스트를 조정하고 뷰가 단순히 응답을 반환하는 것이 아니라 신규 아이템을 데이터베이스에 저장하도록 수정한다. 이것은 test_home_page_can_save_a_POST_request라는 기존 테스트에 세 줄의 코드를 추가한다.

이 테스트는 양이 많아서 많은 것을 테스트하고 있는 듯이 보인다. 어디선가 코드 냄새가 난다. 긴 단위 테스트는 테스트 자체가 복잡하다는 것으로, 테스트를 몇 개로 나눌 수 있다는 것을 의미한다. 테스트 분할 작업이 필요하다는 것을 지금 만들고 있는 작업 목록 앱에 기록해두자.

오류
사용자가 보낸 데이터를 뷰에서 저장하지 못한 상태이기 때문에 test에서 Item.objects.count()의 값이 0이 된다.

수정
lists/views.py
Item의 객체를 만들고 text속성에 사용자로부터 받은 데이터를 입력한 후에 저장한다.
그럼 테스트를 통과할 수 있다.

작업 메모장

  • 모든 요청에 대한 비어 있는 요청은 저장하지 않는다.
  • 코드 냄새 : POST 테스트가 너무 긴가?
  • 테이블에 아이템 여러 개 표시하기
  • 하나 이상의 목록 지원하기

코드 작성
첫 번째 것부터 작업해보자. 기존 테스트를 수정할 수도 있지만, 단위 테스트는 한 번에 하나만 테스트하는 것이 좋다. 새로운 테스트를 추가한다. POST요청이 아닌 경우 Item을 저장하지 않는지 확인하는 테스트 코드.
lists/test.py

오류
위의 코드에선 사용자의 입력값을 정하지 않아서 빈 문자열이 저장되었을 것이다.
입력값이 없으면 저장하지 않도록 수정해보자.

수정
POST 요청일 경우에 새로운 Item을 만들어 저장하고(create) 아닌 경우에는 빈문자열을 반환한다. 그럼 테스트를 통과할 수 있다.

POST 후에 리디렉션

new_item_text='' 처리가 언짢게 만든다. 감사하게도 작업 메모장에 기록한 두 번째 아이템이 이것을 수정할 기회를 준다. "POST 후에는 항상 리디렉션하라"는 말이 있으니 이를 따르도록 한다. 이번 단위 테스트는 POST 요청을 저장해서 돌아온 응답을 렌더링하는 것이 아니라, 홈페이지로 리디렉션하기 위한 것이다.

수정

?더 이상 response.content가 템플릿에 의해 렌더링되지 않기 때문에, 이것을 확인하는 어설션을 제거했다. 대신에 응답이 HTTP 리디렉션을 하기 때문에 상태 코드가 302가 되며 브라우저는 새로운 위치를 가리킨다.

오류

수정

더 나은 단위 테스트 구현: 각 테스트는 하나의 기능만 테스트 해야 한다
이제 뷰가 POST 후에 리디렉션을 하고 단위 테스트의 길이도 짧아졌다. 하지만 아직 더 테스트를 개선할 여지가 있다. 좋은 단위 테스트는 각 테스트가 한 가지만 테스트하는 것을 의미한다. 이를 통해 버그 추적이 더 용이해진다. 테스트에 많은 어설션이 있는 경우, 앞에 있는 어셜션이 실패하면 뒤에 있는 어설션 상태를 파악할 수 없다.

하나의 테스트를 두 개로 나누었다. 이제 5개가 아닌 6개의 테스트가 통가된다.

템플릿에 있는 아이템 렌더링

작업 메모장

  • 모든 요청에 대한 비어 있는 요청은 저장하지 않는다.
  • 코드 냄새 : POST 테스트가 너무 긴가?
  • 테이블에 아이템 여러 개 표시하기
  • 하나 이상의 목록 지원하기

수정
세 번째 작업은 비교적 쉽다. 템플릿이 여러 아이템을 출력할 수 있는지 확인하는 단위 테스트를 새롭게 추가하도록 한다.

오류
현재 뷰에서는 데이터를 넘겨주는 변수도 설정이 되어있지 않고 넘겨 주더라도 하나의 아이템만 표시할 수 있는 상황이다.

수정

아이템 리스트 객체를 생성하고 템플릿에 넘겨준다.

아이템 리스트를 보여주기 위해서 django 템프릿 구문은 리스트 반복 처리를 위한 태그를 제공한다.

테스트 통과

기능테스트 오류

브라우저로 http : //localhost:8000에 접속해 디버그 메세지를 확인

마이그레이션을 이용한 운영 데이터베이스 생성하기

이 메세지는 데이터베이스가 아직 설치되지 않았다는 불평을 하고 있는 것이다. "단위 테스트는 통과했는데, 왜 문제가 발생하는가?" 하고 의문이 있을 수 있다. 이것은 django가 단위 테스트를 위해 특수한 데이터베이스를 생성하기 때문이다. django의 TestCase 코드가 부리는 마법 중 하나라고 할 수 있다.

"진짜" 데이터베이스를 구축할 필요가 있다. SQLite 데이터베이스는 디스크상에 있는 파일 형태의 데이터베이스로 기본 프로젝트 디렉토리에 db.sqlite3라는 파일로 저장하도록 설정돼있다.

django에선 모든 처리에 데이터베이스를 이용한다. 데이터베이스는 models.py를 이용하며 마이그레이션 파일을 생성하기도 한다. 진짜 데이터베이스를 만들기 위해서는 migrate라는 명령어를 이용한다.

그럼 다시 localhost에 있는 페이지가 로드된다.

오류
아이템 작업 목록의 번호만 수정하면 해결이 된다.

수정
forloop.counter 는 반복문이 반복되는 횟수만큼 정수를 작성한다.

FT통과

커밋
약간의 버그가 있지만 어느 정도 코드가 동작하고 있으니 커밋해주자.

$ git commit -m 'POST 후에 리디렉션과 템플릿에 모든 작업 아이템 표시'

0개의 댓글