[DjangoTDDStudy] #02: UnitTest 이용해 기능 테스트 하기

최소기능 앱

TDD를 하는데 있어 가장 기본은 당연히 테스트코드를 짜는 것이다. 하지만 그 전에 먼저, 테스트 시나리오를 작성해야 한다. 예를들어, “인터넷 쇼핑몰에 들어간 후, 상품을 검색하고, 상품을 선택하고, 장바구니에 담고, 카트에 간 후, 결제를 한다.”라는 시나리오도 가능하다.

그리고, 이 시나리오를 충족하면서 가장 간단한 기능으로만 구성되지만 실제로 ‘동작’하는 앱을 만드는 것이다.

일단 이 최소기능 앱을 만들기 전에 우리 코드를 약간 바꿔보자.

Python 기본 라이브러리: unittest

지금까지는 selenium의 webdriver만을 직접 이용해왔지만, 만약 테스트 할 내용이 단순히 인터넷 창 하나를 띄우는것이 아니라 여러가지로 테스트를 늘려야 한다면 이전시간에 짠 코드는 딱히 도움이 되지는 않을 듯 하다.

그리고, 열려진 브라우저를 일일히 닫아주는 것도 상당히 귀찮다. 그러니까 unittest를 이용해 보자.

방금까지 사용한 functional_tests.py파일을 아래와 같이 바꿔보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# functional_tests.py

from selenium import webdriver
import unittest

class NewVisitorTest(unittest.TestCase):
def setUp(self):
self.browser = webdriver.Chrome('chromedriver')

def tearDown(self):
self.browser.quit()

def test_can_start_a_list_and_retrieve_it_later(self):
self.browser.get('http://localhost:8000')

self.assertIn('To-Do', self.browser.title)
self.fail('Finished the Test')

if __name__=='__main__':
unittest.main(warning='ignore')

위 코드중 마지막의 if __name__=='__main__'은, 이 파이썬 코드가 다른 파이썬 파일에서 import 되어 사용되지 않고 python functional_tests.py라고 직접 실행한 경우에만 아래 코드를 실행한다.

만약 위 파일이 import functional_tests의 방식으로 import되었다면 위 코드의 __name__은 functional_tests가 된다.

1
2
3
4
5
6
7
8
from abc import some_thing
# __name__ 은 some_thing

import abc
# __name__ 은 abc

from abc import some_thing as st
# __name__ 은 st

이와 같은 __name__을 가진다.

다시한번 위 코드를 살펴보면, NewVisitorTest 클래스는 unittest라이브러리의 TestCase클래스를 상속받고 내장함수는 setUp, teatDown, test_can_start_a_list_and_retrieve_it_later가 있다.

unittest.main()을 통해 실행되면 ~~Test라는 class들이 모두 테스트 클래스로 지정되고 실행되는데, 이 클래스 안에 있는 test_로 시작하는 함수 하나하나가 테스트 함수로 인식된다. (만약 test_로 시작하지 않으면 테스트 코드라고 인식하지 않는다.)

또한, 테스트 함수 하나하나가 실행되기 전에 setUp이 실행되고 테스트 함수가 끝날때 마다 tearDown이 실행된다.

암묵적 대기 / 명시적 대기

셀레늄이 URL에 들어가 페이지 로딩이 끝날때까지 기다렸다가 동작하기는 하지만, 완벽하지는 않기때문에 (ajax call로 DOM을 재구성하는 경우 등) 얼마정도 우리가 지정한 element가 나올때까지 기다리게 할 수 있다.

바로 implicit_wait()이라는 암시적 대기를 주는 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# functional_tests.py

from selenium import webdriver
import unittest

class NewVisitorTest(unittest.TestCase):
def setUp(self):
self.browser = webdriver.Chrome('chromedriver')
self.browser.implicit_wait(3)

def tearDown(self):
self.browser.quit()

def test_can_start_a_list_and_retrieve_it_later(self):
self.browser.get('http://localhost:8000')

self.assertIn('To-Do', self.browser.title)
self.fail('Finished the Test')

if __name__=='__main__':
unittest.main(warning='ignore')

browser에 implicit_wait을 3초로 두었다.
그런데, 이 implicit_wait은 이 테스트 코드 전체 행동에 영향을 준다. 즉, 어떤 동작을 하기 전에 일단 3초는 허용하고 본다는 것이다.

그래서 좀 더 복잡한 코드의 경우에는 명시적 대기를 줘야만 한다.

아래는 selenium의 공식 문서 webdriver_advanced의 코드를 일부 변형한 것이다.(Driver/URL)

1
2
3
4
5
6
7
8
9
10
11
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait # v2.4.0 이상
from selenium.webdriver.support import expected_conditions as EC # v2.26.0 이상

browser = webdriver.Chrome('chromedriver')
browser.get("http://localhost:8000")
try:
element = WebDriverWait(browser, 10).until(EC.presence_of_element_located((By.ID, "myDynamicElement")))
finally:
browser.quit()

위 코드에서 브라우저는 0.5초마다 ‘myDynamicElement’라는 ID를 가진 요소가 존재하는지를 체크한다. 만약 10초 내로 나타난다면 정상적으로 진행되고, 나타나지 않으면 TimeoutException을 내뱉는다.

여담

하지만 selenium문서에서는 이 코드는 implicit_wait와 크게 다르지 않다고 한다. 좀더 공부가 필요할 듯 하다.

[DjangoTDDStudy] #01: 개발환경 세팅하기(Selenium / ChromeDriver)

Web을 직접 테스트한다고?

웹 서비스를 개발하는 과정에서 꼭 필요한 것이 있다. 바로 실제로 기능이 동작하는지 테스트 하는 것.
이 테스트를 개발자가 직접 할 수도 있고, 혹은 전문적으로 테스트만 진행하는 QA팀에서 진행할 수도 있다.
하지만 위의 두 방법은 ‘사람이 직접 해야한다’는 공통점이 있다. 이걸 자동화 할 수 있다면 어떨까?

Selenium 설치하기

Selenium은 위의 질문에 대한 답변을 준다. 사람이 하기 귀찮은 부분을 자동화!

우선 Selenium을 설치해주자.
(단, Python3가 설치되어있다는 상황을 가정하며, Virtualenv / Pyvenv등의 가상환경 사용을 권장한다. 이 게시글에서는 tdd_study라는 이름의 가상환경을 이용한다.)

1
$ pip install selenium

PIP가 설치되어있다면 위 명령어 한줄만으로 Selenium이 설치된다.
Selenium의 설치가 완료되었다면, 우선 ChromeDriver를 받아준다.

ChromeDriver 설치하기

Selenium은 기본적으로 Firefox 드라이버를 내장하고있다. 이 ‘Driver’들은 시스템에 설치된 브라우저들을 자동으로 동작하게 하는 API를 내장하고 있고, 우리는 각 브라우저별 드라이버를 다운받아 쉽게 이용할 수 있다.

현재 Selenium은 대다수의 모던 웹브라우저들(Chrome, Firefox, IE, Edge, Phantomjs, etc.)을 지원하고 있기 때문에, 일상적으로 사용하는 크롬드라이버를 사용하기로 했다.
(만약 Headless Browser를 이용해야 한다면 Phantomjs를 이용해보자.)

크롬드라이버는 크로미움의 ChromeDriver에서 최신 버전으로 받을 수 있고, 이번 스터디에서는 Chrome v54에서 v56까지를 지원하는 ChromeDriver 2.27버전을 이용하려 한다.

크롬드라이버는 어떤 파일을 설치하는 것이 아니라, Binary가 내장되어있는 하나의 실행파일이라고 보면 된다.

만약 MAC OS나 Linux계열을 사용한다면, 크롬드라이버를 받은 후 그 파일을 PATH에 등록해 주자.

예시)
위 사이트에서 받은 파일의 이름이 chromedriver 이고 받은 경로가
/Users/beomi/bin 이라면,
사용하는 쉘(bash / zsh등)의 RC파일(유저 홈 디렉토리의 .bashrc / .zshrc)의 제일 아래에

1
export PATH=${PATH}:~/bin

위의 코드를 적고 저장한 후, 쉘을 재실행해준다.(터미널을 껐다가 켜주자.)

이렇게 하고나면, 아래 실습시 크롬드라이버의 위치를 지정하지 않고 파일 이름만으로 이용 할 수 있다는 장점이 있다.

Django 설치하기

Django는 앞으로 우리가 스터디에 사용할 WebFramework다.

1
$ pip install django

위 명령어로 역시 쉽게 설치 가능하다.
(2016.12.27기준 1.10.4가 최신버전이며, 1.10.x버전으로 스터디를 진행할 예정이다.)

pip로 Django가 설치되고 나면 django-admin 이라는 명령어를 쉘에서 사용할 수 있다.

실습

0. 설치 잘 되었는지 확인해 보기

쉘에서

1
$ pip list --format=columns

라는 명령어를 쳤을 때 아래 스샷과 같이 Django와 selenium이 보인다면 정상적으로 설치가 진행 된 것이다.

설치가 잘 되었다면 다음으로 진행해 보자.

1. Selenium 이용해보기

1
2
3
4
5
6
7
8
from selenium import webdriver

browser = webdriver.Chrome('chromedriver')
# chromedriver가 Python파일과 같은 위치에 있거나, 혹은 OS의 PATH에 등록되어 쉘에서 실행 가능한 경우 위와같이 한다.
# 혹은 browser = webdriver.Chrome('/path/to/chromedriver')의 절대경로로 해도 된다.
browser.get('http://localhost:8000')

assert 'Django' in browser.title

위 코드는 Chrome 브라우저를 작동시키는 WebDriver를 이용해 새 크롬 창을 띄우고 http://localhost:8000이라는 url로 들어간 후 브라우저의 title에 ‘Django’라는 글자가 들어가 있는지를 확인(Assert)해준다.

현재 상황에서는 django웹서버를 실행하지 않았기 때문에 당연하게도 AssertionError가 난다.

2. Django 서버 띄우기

이제 Django서버를 띄워보자.

Django는 django-admin이라는 명령어를 통해 기본적인 뼈대가 구성된 프로젝트 폴더 하나를 만들어 준다.

1
$ django-admin startproject tdd_study_proj

위 명령어를 치면 다음과 같은 폴더 구조를 가진 프로젝트 폴더가 생긴다.

1
2
3
4
5
6
7
8
9
10
(tdd_study) ➜  tdd_study_proj tree
.
├── manage.py
└── tdd_study_proj
├── __init__.py
├── settings.py
├── urls.py
└── wsgi.py

1 directory, 5 files

(유의: tree명령어는 Mac OS에서 HomeBrew를 통해 설치한 패키지다. 자신의 쉘에서 동작하지 않는다고 문제가 있는건 아니다.)

위 파일 구조를 보면 tdd_study_proj라는 큰 폴더(현재위치) 안에 manage.py파일과 현재위치 폴더이름과 같은 tdd_study_proj라는 프로젝트 폴더가 생겨있다.

이 상태에서 장고에 내장된 테스트 웹서버를 구동해 보자. 테스트용 웹서버는 runserver 라는 명령어로 실행할 수 있고, CTRL-C로 작동을 멈추게 할 수 있다.
manage.py파일이 있는 곳에서 아래의 명령어를 쳐주자.

1
$ python manage.py runserver

위 명령어를 치면 아래와 같이 테스트 서버가 http://127.0.0.1:8000 에서 실행되고 있다.
(참고: 127.0.0.1 주소는 localhost와 동일합니다. 즉, 127.0.0.1:8000은 localhost:8000입니다.)

위 URL로 들어갔을 때 아래와 같은 화면이 나온다면 Django가 정상적으로 설치되었고, 테스트 웹서버도 정상적으로 구동중인 것이다.

3. 다시한번 테스트!

Django서버가 켜져있는 상태로 둔 후, 새 쉘(혹은 cmd)창을 켜서 실습1. Selenium 이용해보기에서 만든 파일을 manage.py파일이 있는 폴더에 selenium_test.py라는 이름으로 만들어 주자.

# selenium_test.py

from selenium import webdriver

browser = webdriver.Chrome('chromedriver')
browser.get('http://localhost:8000')

assert 'Django' in browser.title

이제는 에러가 나지 않고 테스트가 아무말(아무 에러)없이 끝나는걸 볼 수 있다 :)

[DjangoTDDStudy] #00: 스터디를 시작하며

스터디를 시작하며

파이썬을 이용한 클린 코드를 위한 테스트 주도 개발이라는 도서를 처음 보았을 때는 Django와 관련이 있는 책이라고는 생각조차 하지 못했다. 단지, 파이썬을 좀 더 잘 하려면 어떤 것을 알아야 할까 하고U 생각하던 중 TEST를 사용하는 때가 생산성이 올라간다는 말을 듣고서 “테스트 주도 개발”을 하려고 시도해 보려던 참, 이 책을 보게 된 것이다.

Python/Django 공부에 관심을 가지고 있던 지환님과 이야기 하던 중, 마침 스터디에 대한 이야기가 나왔고 이 염소책과 TDD에 대한 이야기도 나왔다. 사실상 즉석에서 ‘아 그래요? 그러면 해보죠!’라는 느낌으로 시작했다. 그자리에서 명서님에게 물어보니 바로 동참하신다고 해서 9XD에 글을 바로 글을 올리고 스터디원을 모집했다.
사실 이정도로 인기가 많을거라고 생각하지는 못했다

생각보다 빠르게 열명이 찼고, 저번주 화요일에 첫 모임을 가졌다.

첫 모임에서

첫 모임에서는 많은 내용을 다루지는 않았다. 하지만 진행에 있어 미흡한 준비가 아쉬웠다.
모임 전 계획은 서로 인사하기 / 왜 우리는 여기에 왔는가 / 우리의 목표 등을 이야기하고, Django를 사용하지 않은 분들을 위해 개발환경을 설치하고, 1장 정도(Selenium을 이용해 크롬드라이버로 테스트 한번 돌려보기)를 진행하려 했다.
하지만 위 내용이 생각보다 빨리 진행되어 1시간내로 모든게 끝나버렸고, 남은 1시간동안은 뭘 해야할지 고민이 들어 2장을 조금 진행해 보려고 했는데 사실 이게 욕심이었다고 생각한다. 물론, 2~3단원까지 끝낸다면 진도상으로는 좋다고 말할 수 있지만 구성원들이 스터디를 따라오지 못할 수 있다고 겁을 먹을 수 있었기 때문이다.
다음에 스터디를 처음 시작한다면, 좀더 여유로운 시간을 가지고 ‘스터디’가 아닌 ‘모임’으로 첫 만남을 카페 등에서 가지면 더 좋지 싶다.

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×