리셋 되지 말자

[TDD with selenium] Django Selenium test 학습 본문

TDD

[TDD with selenium] Django Selenium test 학습

kyeongjun-dev 2021. 6. 16. 16:44

진행 순서

프로젝트 생성부터 처음부터 작성, selenium과 web driver(Firefox 사용) 설치 후, 테스트 진행
webserver가 실행되어야 하는 selenium 특성상, 'python manage.py test' 수행 시, 자동으로 'python manage.py runserver'가 수행되고, 테스트가 끝나면 자동으로 runserver 프로세스를 끝내는 과정 포함

 

참고 사이트

 

프로젝트 및 앱 생성

- 가상환경 venv 생성 및 설정

ubuntu@ubuntu-All-Series:~/바탕화면/re-test$ python3 -m venv venv
ubuntu@ubuntu-All-Series:~/바탕화면/re-test$ source venv/bin/activate
(venv) ubuntu@ubuntu-All-Series:~/바탕화면/re-test$

 

- pip 업그레이드

(venv) ubuntu@ubuntu-All-Series:~/바탕화면/re-test$ pip install --upgrade pip
Cache entry deserialization failed, entry ignored
Collecting pip
  Using cached https://files.pythonhosted.org/packages/cd/82/04e9aaf603fdbaecb4323b9e723f13c92c245f6ab2902195c53987848c78/pip-21.1.2-py3-none-any.whl
Installing collected packages: pip
  Found existing installation: pip 9.0.1
    Uninstalling pip-9.0.1:
      Successfully uninstalled pip-9.0.1
Successfully installed pip-21.1.2
(venv) ubuntu@ubuntu-All-Series:~/바탕화면/re-test$ pip --version
pip 21.1.2 from /home/ubuntu/바탕화면/re-test/venv/lib/python3.6/site-packages/pip (python 3.6)

 

- django 설치

(venv) ubuntu@ubuntu-All-Series:~/바탕화면/re-test$ pip install django
Collecting django
  Using cached Django-3.2.4-py3-none-any.whl (7.9 MB)
Collecting pytz
  Using cached pytz-2021.1-py2.py3-none-any.whl (510 kB)
Collecting sqlparse>=0.2.2
  Using cached sqlparse-0.4.1-py3-none-any.whl (42 kB)
Collecting asgiref<4,>=3.3.2
  Using cached asgiref-3.3.4-py3-none-any.whl (22 kB)
Collecting typing-extensions
  Using cached typing_extensions-3.10.0.0-py3-none-any.whl (26 kB)
Installing collected packages: typing-extensions, sqlparse, pytz, asgiref, django
Successfully installed asgiref-3.3.4 django-3.2.4 pytz-2021.1 sqlparse-0.4.1 typing-extensions-3.10.0.0
(venv) ubuntu@ubuntu-All-Series:~/바탕화면/re-test$

 

- 프로젝트 생성할 디렉토리 생성 및 프로젝트 생성

(venv) ubuntu@ubuntu-All-Series:~/바탕화면/re-test$ mkdir myproject
(venv) ubuntu@ubuntu-All-Series:~/바탕화면/re-test$ cd myproject
(venv) ubuntu@ubuntu-All-Series:~/바탕화면/re-test/myproject$ django-admin startproject config .
(venv) ubuntu@ubuntu-All-Series:~/바탕화면/re-test/myproject$ ls
config  manage.py
(venv) ubuntu@ubuntu-All-Series:~/바탕화면/re-test/myproject$

 

- app 생성

(venv) ubuntu@ubuntu-All-Series:~/바탕화면/re-test/myproject$ django-admin startapp myapp
(venv) ubuntu@ubuntu-All-Series:~/바탕화면/re-test/myproject$ ls
config  manage.py  myapp

 

 

runserver로 웹 동작 테스트

- runserver 실행 (runserver 종료는 Ctrl + C 눌러서 종료)

(venv) ubuntu@ubuntu-All-Series:~/바탕화면/re-test/myproject$ python manage.py runserver
Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).

You have 18 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.
Run 'python manage.py migrate' to apply them.
June 16, 2021 - 06:49:26
Django version 3.2.4, using settings 'config.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

 

- localhost:8000 브라우저 접속

 

 

app 등록, url 설정 및 접속

- myproject -> config -> settings.py 수정 : 'myapp' 추가

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'myapp',
]

 

 

- myapp -> views.py 에 home view 작성

from django.shortcuts import render
from django.http import HttpResponse
# Create your views here.
def home(request):
    return HttpResponse('hello myapp!')

 

- myapp -> urls.py 추가 및 작성

from django.urls import path
from . import views
urlpatterns = [
    path('', views.home, name='home'),
]

 

- config -> urls.py 작성

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

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

 

- runserver실행 후, localhost:8000 접속

 

 

Test 동작 확인

- myapp -> tests.py 작성

from django.test import TestCase

# Create your tests here.
class MyTest(TestCase):
    def test_1(self):
        self.assertEqual(1+1, 3)

원인은 모르겠지만, 'plus_test'를 test_1 대신 썻을 때, 테스트가 제대로 진행되지 않음. 네이밍에 유의해야 할듯

 

- python manage.py test 실행

(venv) ubuntu@ubuntu-All-Series:~/바탕화면/re-test/myproject$ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
F
======================================================================
FAIL: test_1 (myapp.tests.MyTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/ubuntu/바탕화면/re-test/myproject/myapp/tests.py", line 6, in test_1
    self.assertEqual(1+1, 3)
AssertionError: 2 != 3

----------------------------------------------------------------------
Ran 1 test in 0.001s

FAILED (failures=1)
Destroying test database for alias 'default'...

1+1은 2인데, 3과 비교하니 FAILED 출력 확인

 

 

브라우저 드라이버 설치 및 PATH 추가

- 다운로드 : 링크(gecko driver) - 각 운영체제의 버젼에 맞게 다운로드

- PATH 추가 (다운로드/geckodriver 디렉토리 밑에 실제 geckodriver가 있는 상태이므로, 위치까지만 추가)

export PATH=$PATH:/home/ubuntu/다운로드/geckodriver

 

 

selenium 설치

- pip install selenium

(venv) ubuntu@ubuntu-All-Series:~/바탕화면/re-test/myproject$ pip install selenium
Collecting selenium
  Using cached selenium-3.141.0-py2.py3-none-any.whl (904 kB)
Collecting urllib3
  Using cached urllib3-1.26.5-py2.py3-none-any.whl (138 kB)
Installing collected packages: urllib3, selenium
Successfully installed selenium-3.141.0 urllib3-1.26.5
(venv) ubuntu@ubuntu-All-Series:~/바탕화면/re-test/myproject$

 

 

test.py 수정

- myapp -> tests.py 수정

from django.test import TestCase
from selenium import webdriver

# Create your tests here.
class MyTest(TestCase):
    def setUp(self):
        self.browser = webdriver.Firefox()

    def tearDown(self) -> None:
        self.browser.quit()

    # def test_1(self):
    #     self.assertEqual(1+1, 2)

    def test_2(self):
        self.browser.get('http://localhost:8000')
        self.assertIn('myapp', self.browser.title)

터미널 창을 하나 더 열어서, 'python manage.py runserver'로 웹서버를 시작한 뒤에 'python manage.py test' 실행해야함.

 

- python manage.py test 실행

(venv) ubuntu@ubuntu-All-Series:~/바탕화면/re-test/myproject$ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
F
======================================================================
FAIL: test_2 (myapp.tests.MyTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/ubuntu/바탕화면/re-test/myproject/myapp/tests.py", line 17, in test_2
    self.assertIn('myapp', self.browser.title)
AssertionError: 'myapp' not found in ''

----------------------------------------------------------------------
Ran 1 test in 2.736s

FAILED (failures=1)
Destroying test database for alias 'default'...

현재 localhost:8000 접속 시, title이 설정되어 있지 않으므로 FAILED 발생

 

html 템플릿 작성 및 views.py 작성 후 test 수행

- myapp -> templates 디렉토리 생성

(venv) ubuntu@ubuntu-All-Series:~/바탕화면/re-test/myproject$ mkdir myapp/templates

 

- myapp -> templates -> home.html 작성

<html>
    <head>
        <title>Django - myapp</title>
    </head>
    <body>
        <p>Hello myapp!</p>
    </body>
</html>

 

- myapp -> views.py 수정

from django.shortcuts import render
from django.http import HttpResponse
# Create your views here.
def home(request):
    return render(request, 'home.html')

 

- test  수행

(venv) ubuntu@ubuntu-All-Series:~/바탕화면/re-test/myproject$ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 2.736s

OK
Destroying test database for alias 'default'...

title이 'Django - myapp' 이므로, 'myapp'이라는 문구가 존재하여 테스트 성공

 

 

Model 사용

- myapp -> models.py 모델 작성

from django.db import models
from django.core.validators import MaxValueValidator, MinValueValidator
# Create your models here.
class Profile(models.Model):
    name = models.CharField(max_length=100)
    height = models.IntegerField(validators=[MinValueValidator(1), MaxValueValidator(300)])

 

- myapp -> forms.py 추가 및 model form작성

from django import forms
from .models import Profile

class ProfileForm(forms.ModelForm):
    class Meta:
        model = Profile
        fields = ('name', 'height')

 

- myapp -> views.py 수정

from django.shortcuts import render
from django.http import HttpResponse
from .forms import ProfileForm
from .models import Profile
# Create your views here.
def home(request):
    # return render(request, 'home.html')
    if request.method == 'POST':
        form = ProfileForm(request.POST)
        if form.is_valid():
            form.save()
    
    form = ProfileForm
    return render(request, 'home.html', {'form': form})

POST 요청이 오고, form이 유효하면 정보 저장

 

- home.html 수정

<html>
    <head>
        <title>Django - myapp</title>
    </head>
    <body>
        <p>Hello myapp!</p>

        <br>
        <form method="post">
            {% csrf_token %}
            {{ form }}
            <button type="submit" id="submit_buttion">Submit</button>
        </form>
    </body>
</html>

 

- runserver 종료 후, migrate 실행

(venv) ubuntu@ubuntu-All-Series:~/바탕화면/re-test/myproject$ python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying auth.0012_alter_user_first_name_max_length... OK
  Applying sessions.0001_initial... OK

 

- runserver 재실행 후, localhost:8000 접속 확인

 

- Name, Height 입력 후 'Submit' 클릭시 에러 발생

 

- runserver 종료 후, makemigrations, migrate 실행

(venv) ubuntu@ubuntu-All-Series:~/바탕화면/re-test/myproject$ python manage.py makemigrations
Migrations for 'myapp':
  myapp/migrations/0001_initial.py
    - Create model Profile
    
(venv) ubuntu@ubuntu-All-Series:~/바탕화면/re-test/myproject$ python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, myapp, sessions
Running migrations:
  Applying myapp.0001_initial... OK

 

- runserver 실행 후, 다시 Name, Height Submit 시도

(venv) ubuntu@ubuntu-All-Series:~/바탕화면/re-test/myproject$ python manage.py runserver
Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).
June 16, 2021 - 09:16:14
Django version 3.2.4, using settings 'config.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
[16/Jun/2021 09:16:20] "GET / HTTP/1.1" 200 657
[16/Jun/2021 09:16:25] "POST / HTTP/1.1" 200 657

터미널 창에서 오류 없이 post 요청 확인

 

 

Profile들이 보이도록 수정

- myapp -> views.py 수정

from django.shortcuts import render
from django.http import HttpResponse
from .forms import ProfileForm
from .models import Profile
# Create your views here.
def home(request):
    # return render(request, 'home.html')
    if request.method == 'POST':
        form = ProfileForm(request.POST)
        if form.is_valid():
            form.save()
    
    form = ProfileForm
    profiles = Profile.objects.all()
    return render(request, 'home.html', {'form': form, 'profiles': profiles})

 

- myapp -> templates -> home.html 수정

<html>
    <head>
        <title>Django - myapp</title>
    </head>
    <body>
        <p>Hello myapp!</p>

        <br>
        <form method="post">
            {% csrf_token %}
            {{ form }}
            <button type="submit" id="submit_buttion">Submit</button>
        </form>
        <div>
            {% for profile in profiles %}
            <div>
                <p>{{ profile.name }}</p>
                <p>{{ profile.height }}</p>
            </div>
            <br>
            {% endfor %}
        </div>
    </body>
</html>

 

- localhost:8000 확인

1개 확인

 

2개 확인

Name과 Height을 입력하고, Submit을 누르면, 추가되어 보이는 것을 확인할  수 있다.

 

 

POST 요청 selenium으로 처리하기

- myapp -> tests.py 수정

from django.test import TestCase
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
# Create your tests here.
class MyTest(TestCase):
    def setUp(self):
        self.browser = webdriver.Firefox()

    def tearDown(self) -> None:
        self.browser.quit()

    # def test_1(self):
    #     self.assertEqual(1+1, 2)

    def test_2(self):
        self.browser.get('http://localhost:8000')
        self.assertIn('myapp', self.browser.title)

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

        profile_name = self.browser.find_element_by_id('id_name')
        profile_height = self.browser.find_element_by_id('id_height')

        submit = self.browser.find_element_by_id('submit_button')

        profile_name.send_keys('test-name')
        profile_height.send_keys('170')

        submit.send_keys(Keys.RETURN)

        self.browser.get("http://localhost:8000")
        self.browser.find_element_by_xpath("//*[contains(text(), 'test-name')]")

        self.assertIn('test-name', self.browser.page_source)

POST 요청으로 name과 height을 보내고, 다시 페이지를 로드했을 때, 해당 페이지 소스에 'test-name'이 존재하는지 확인한다

 

- test 실행

(venv) ubuntu@ubuntu-All-Series:~/바탕화면/re-test/myproject$ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
..
----------------------------------------------------------------------
Ran 2 tests in 7.101s

OK
Destroying test database for alias 'default'...

 

 

- 브라우저에 localhost:8000 접속하여 test-name 추가된것 확인

 

 

Test 실행 시, 자동으로 runserver 실행

지금까지는 runserver가 다른 터미널에서 실행 중일때, selenium을 이용한 테스트가 가능했다. 이렇게 하지말고, test 시작 시에 runserver가 시작되고, test가 끝나면 자동으로 runserver 가 종료되도록 수정해본다.

- myapp -> tests.py 수정

from unittest import signals
from django.test import TestCase
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
# from selenium.webdriver.support.ui import WebDriverWait
# from selenium.common.exceptions import TimeoutException
# from selenium.webdriver.common.by import By
# from selenium.webdriver.support import expected_conditions as EC

import subprocess
import os
import signal

# Create your tests here.
class MyTest(TestCase):
    def setUp(self):
        sp = subprocess.Popen(
        ['python', 'manage.py', 'runserver'], stdout=subprocess.PIPE)
        self.sp_pid = sp.pid
        self.browser = webdriver.Firefox()

    def tearDown(self) -> None:
        self.browser.quit()
        os.kill(self.sp_pid, signal.SIGTERM)

    # def test_1(self):
    #     self.assertEqual(1+1, 2)

    def test_2(self):
        self.browser.get('http://localhost:8000')
        self.assertIn('myapp', self.browser.title)

        # try:
        #     myElement = WebDriverWait(self.browser, 10).until(EC.presence_of_element_located((By.ID, 'submit_button')))
        #     print('page is ready')
        # except TimeoutException:
        #     print('Loading took too much time')

    def test_3(self):
        self.browser.get('http://localhost:8000')
        # try:
        #     myElement = WebDriverWait(self.browser, 10).until(EC.presence_of_element_located((By.ID, 'submit_button')))
        #     print('page is ready')
        # except TimeoutException:
        #     print('Loading took too much time')

        profile_name = self.browser.find_element_by_id('id_name')
        profile_height = self.browser.find_element_by_id('id_height')

        submit = self.browser.find_element_by_id('submit_button')

        profile_name.send_keys('test-name2')
        profile_height.send_keys('170')

        submit.send_keys(Keys.RETURN)

        self.browser.get("http://localhost:8000")
        self.browser.find_element_by_xpath("//*[contains(text(), 'test-name2')]")

        self.assertIn('test-name2', self.browser.page_source)

subprocess를 이용하여 test 수행 시, runserver가 실행되고, test 종료 시 runserver에 해당하는 process를 종료 시키는 방식으로 구성했다.
매번 테스트케이스 마다 브라우저를 열고, 닫고 하는게 조금 문제가 될 것 같기도...?

 

정말 다양하다. 공식문서에서 selenium을 추천하는 이유를 알겠다.

'TDD' 카테고리의 다른 글

[Selenium] webdriver.dispose() .close() .quit() 차이  (0) 2021.06.22
docker django selenium 테스트  (0) 2021.06.18
TDD 테스트 종류  (0) 2021.05.17
[TDD with Python] - 2  (0) 2021.05.17
[TDD with Python] - 1  (0) 2021.05.17
Comments