Deprecation 에러 처리

기존 예제에는 firefox로 처리하라고 되어있으나 따로 설치하기도 그렇고 크롬으로 테스트하는게 마음이 편하니 크롬으로 설치하게끔 처리하였다.

크롬드라이버 버전 다운로드는 사용하고 있는 크롬 버전에 다음 URL에서 받자.

https://chromedriver.chromium.org/downloads

물론 크롬드라이버를 다운받아 테스트하는 방법도 있으나 또 설치되어있는 크롬버전과 맞춰서 크롬드라이버를 다운로드 받아야하는 삽질이 있으니, 아래의 방법을 이용하도록 하자.

$ pip install webdriver-manager
  1. webdriver-manager 패키지를 설치
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager

chrome_options = webdriver.ChromeOptions()
driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()), options=chrome_options)
  1. Service 객체에 webdriver-manager의 ChromeDriverManager를 사용하여 크롬드라이버를 다운받은 것이 아닌 현재 설치된 크롬브라우저를 사용하도록 한다.

selenium의 Service는 버전 4부터 사용되는 듯 하다.

django 변경점

TDD 책의 예제코드를 4.0버전 기반으로 새로 쓰는 개발을 진행하면서 생긴 일들의 기록이다. Django 2.0 버전부터 4.0버전까지의 변경점을 정리하는 글을 쓰려다가 당장은 의미 없을 것 같아, 생겼던 일을 중심으로 적는다.

간소화된 URL 라우팅 문법

기존의 urls.py에서는

django.conf.url import pattern, include, url

urlpattern = pattern('',
  url('r'admin/', include(admin.site.urls)),
)

로 urlpattern에 정규식 문법과 include()를 활용하여 url 패턴을 작성하였다. 2.0버전부터는 이러한 방식에서 다음과 같이 바뀌었다.

from django.contrib import admin
from django.urls import path

urlpatterns = [
    path('admin/', admin.site.urls),
]

path 방법으로 간단하게 url을 설정할 수 있도록 변경되었다. 앞의 방법으로 예제코드를 현재 버전에 맞게 수정하였다. 물론 기존 방법도 사용할 수도 있다.

path('', views.home_page, name='home'),

urlresolvers 모듈 삭제

기존 예제코드를 그대로 진행하다보면 다음과 같은 에러가 나온다.

ModuleNotFoundError: No module named 'django.core.urlresolvers'

django 2.0 부터 urlresolvers 모듈이 삭제되었다. 해당 모듈을 django.urls import resolve로 바꿔진행하면 된다.

seleninum

셀레니움도 최신 버전으로 올려 진행했다.

selenium 문법 변화

browser = webdriver.Chrome 와 같이 크롬 드라이버를 설정하여 예전 예제코드를 그대로 따라 진행을 하면 진행이 안되는 경우가 나온다.

AttributeError: 'WebDriver' object has no attribute 'find_element_by_tag_name'

문법이 바뀌었기 때문인데, 모듈을 추가하고, 코드를 다음과 같이 수정하자.

from selenium import webdriver # 기존 webdriver 
from selenium.webdriver.common.by import By # 변경된 문법을 위해 추가

# 기존 코드
header_text = self.browser.find_element_by_tag_name('h1').text
inputbox = self.browser.find_element_by_id('id_new_item')
table = self.browser.find_element_by_id('id_list_table')
rows = table.find_elements_by_tag_name('tr')

# 변경된 코드
header_text = self.browser.find_element(By.TAG_NAME, 'h1').text
inputbox = self.browser.find_element(By.ID, 'id_new_item')
table = self.browser.find_element(By.ID, 'id_list_table')
rows = table.find_elements(By.TAG_NAME, 'tr')

find_element(s)까지는 동일하나 by 메소드를 By.로 처리하도록 변경되었다.

신경쓰이는 에러 처리

파이썬에서 셀레니움으로 크롬드라이버를 실행해 진행하다보면 이상한 에러가 뜨기 시작하는데

USB: usb_device_handle_win.cc:1048 Failed to read descriptor from node connection: 시스템에 부착된 장치가 작동하지 않습니다. (0x1F)

USB를 사용하고 있는게 없는데 이상한 에러가 뜨기 시작하니 황당했다. 큰 오류는 아닌것 같지만 신경쓰이기에 코드를 바꿔 처리했다.

chrome_options = webdriver.ChromeOptions()
chrome_options.add_experimental_option("excludeSwitches", ["enable-logging"])

이 옵션을 넣으니 신경쓰이는 에러는 해결되었다.

CSRF token 문제 해결

입력 폼을 만드는 부분을 만들고 테스트를 해보는데 자꾸 실패가 뜨길래 에러 상황을 보니 csrf token 때문이었다.

CSRF(Cross-Site Request Forgery)란

CSRF는 웹사이트 취약점 공격의 하나로, 사용자가 자신의 의지와는 무관하게 공격자가 의도한 행위(수정, 삭제, 등록 등)를 특정 웹사이트에 요청하게 하는 공격을 말한다. 사이트 간 스크립팅(XSS)을 이용한 공격이 사용자가 특정 웹사이트를 신용하는 점을 노린 것이라면, CSRF는 특정 웹사이트가 사용자의 웹 브라우저를 신용하는 상태를 노린 것이다. 일단 사용자가 웹사이트에 로그인한 상태에서 사이트간 요청 위조 공격 코드가 삽입된 페이지를 열면, 공격 대상이 되는 웹사이트는 위조된 공격 명령이 믿을 수 있는 사용자로부터 발송된 것으로 판단하게 되어 공격에 노출된다.

CSRF 공격을 막기위해 token을 사용하여 사용자 인증을 하는 방식이 있는데, Django의 경우는 form 태그 안에 추가하여 간편하게 사용할 수 있다.

다만 이걸 추가하면서 테스트하기가 귀찮아 졌는데, 응답을 받은 html의 hidden type에는 input으로 token이 생성되었지만 template html에는 생성되지 않았기 때문이다.

======================================================================
FAIL: test_home_page_returns_correct_html (lists.tests.HomePage.test_home_page_returns_correct_html)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\Users\kkh65\git\Django_TDD\superlists\lists\tests.py", line 30, in test_home_page_returns_correct_html
    self.assertEqual(response.content.decode(), expected_html)  # type: ignore
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError: '<htm[229 chars]     <input type="hidden" name="csrfmiddleware[187 chars]\n\n' != '<htm[229 chars]     \n        </form>\n        <table id="id_[66 chars]\n\n'

테스트하는 값에 대해서는 알고 있었기 때문에 어렵지는 않았다. 테스트용 html을 파일을 만들자고 하기엔 유지보수를 2배로 해야하고, 테스트에서 제외하면 문제가 생기는 부분이 있으나, 해당 문제는 제외를 해도 되는 문제이므로 제외하는 방향으로 진행한다.

찾아보니 스택오버플로우에 같은 이슈를 겪고 있는 질문이 있어 해당 내용을 적용했다.

list/test.pyHomepageTest 클래스의 전체 코드이다.

import re

class HomePageTest(TestCase):

    @staticmethod
    def remove_csrf(html_code):
        csrf_regex = r'<input[^>]+csrfmiddlewaretoken[^>]+>'
        return re.sub(csrf_regex, '', html_code)

    def assertEqualExceptCSRF(self, html_code1, html_code2):
        return self.assertEqual(
            self.remove_csrf(html_code1),
            self.remove_csrf(html_code2)
        )

    def test_root_url_resolves_to_home_page_view(self):
        found = resolve('/')
        self.assertEqual(found.func, home_page)

    def test_home_page_returns_correct_html(self):
        request = HttpRequest()
        response = home_page(request)
        self.assertEqualExceptCSRF(
            render_to_string('home.html', request=request),
            response.content.decode()
        )

    def test_home_page_can_save_a_POST_request(self):
        request = HttpRequest()
        request.method = 'POST'
        request.POST['item_text'] = '신규 작업 아이템'

        response = home_page(request)

        self.assertIn('신규 작업 아이템', response.content.decode())
        self.assertEqualExceptCSRF(
            render_to_string('home.html', {'new_item_text' : '신규 작업 아이템'}),
            response.content.decode()
        )

앞의 코드로 실행하면 문제없이 코드가 실행된다. :)