기능 테스트는 사용자의 시점에서 사람이 이해하기 쉬운 유저 스토리 (User story)
형태로 작성한다. 테스트 코드가 검사해야할 각 상황들을 주석으로 달아서 실제 앱이 스토리대로 작동하는지를 테스트 할 수 있도록 기능 테스트를 구성한다. 프로그래머가 아닌 사람들이 보아도 이해할 수 있도록 스토리를 작성해야한다.
지금 만들려고 하는 To-Do list
앱을 경험하게되는 어떤 사용자의 시점에서 유저 스토리를 구성해보자.
유저 스토리
-
한 사용자가 해야할 일 목록을 만들어주는
To-Do list
앱에 대해 듣고 직접 확인해보러 홈페이지에 접속한다. -
홈페이지의
타이틀
과헤더
에To-Do list
라고 적혀있는 것을 보고 제대로 찾아온 것을 확인한다. -
메인 화면에 바로 할 일을 입력할 수 있도록 준비된 것을 확인한다.
-
사용자가 해야할 일인
기타줄 갈기
를 입력한다. - 사용자가 할 일을 입력한 다음 엔터를 치면, 페이지가 새로고침되면서 방금 입력한 할 일이 목록으로 뜬다.
1: 기타줄 갈기
-
다른 할 일 목록을 받을 수 있도록 할 일 입력창이 계속 표시되고, 사용자는 다음 할 일인
피크 사기
를 입력한다. - 페이지가 다시 새로고침 되고, 지금까지 입력한 할 일들이 모두 목록으로 표시된다.
1: 기타줄 갈기 2: 피크 사기
-
사용자는 이 목록을 따로 저장할 수 있는지 궁금해한다. 그러던 중 사이트가 사용자의 목록을 위해 고유의 URL 주소를 생성한 것을 확인한다. 이를 알아차릴 수 있도록 도움말도 표시함.
-
사용자가 해당 URL 주소로 접속해본 뒤, 생성했던 할 일 목록이 그대로 나타나는 것을 확인한다.
- 사용자가 만족한 뒤, 사이트를 떠난다.
유저 스토리를 기반으로 기능 테스트 코드 작성하기
지금까지의 기능 테스트 코드는 아래와 같다.
# functional_test.py
from selenium import webdriver
browser = webdriver.Chrome('chromewebdriver 경로')
browser.get('http://localhost:8000')
assert 'Django' in browser.title
이제 위에서 구상한 스토리 중 각 테스트 코드가 구현할 부분에 해당하는 스토리를 주석으로 추가한다.
# functional_test.py
from selenium import webdriver
browser = webdriver.Chrome('chromewebdriver 경로')
# 한 사용자가 해야할 일 목록을 만들어주는 `To-Do list` 앱에 대해 듣고
# 직접 확인해보러 홈페이지에 접속한다.
browser.get('http://localhost:8000')
# 홈페이지의 `타이틀` 과 `헤더` 에 `To-Do list` 라고 적혀있는 것을 보고 제대로 찾아온 것을 확인한다.
assert 'To-Do' in browser.title
사용자가 홈페이지 타이틀에 To-Do list
라고 적혀있는 것을 확인한다고 했으므로, browser.title
에 To-Do
라는 문자열이 있는지 검사하도록 assert
문의 Django
를 To-Do
로 바꿔주었다.
이제 기능 테스트를 실행해보자. 항상 runserver
를 먼저 실행하는 것을 잊지말자.
python functional_test.py
당연히 개발용 서버의 타이틀에 To-Do
라는 문자열을 넣어주지 않았으므로 에러가 날 것이다.
Traceback (most recent call last):
File "functional_tests.py", line 10, in <module>
assert 'To-Do' in browser.title
AssertionError
AssertionError
가 발생했다. 예상한대로 To-Do
라는 문자열이 browser.title
에 없기 때문에 발생한 에러이다.
이러한 에러를 expected fail (예상된 실패)
이라고 한다. 예상한 대로 에러가 발생했기 때문에 현재 상황이 통제하에 있다고 할 수 있다.
이제 예상된 에러가 발생하는 것을 확인했으므로 이를 해결할 수 있는 실제 어플리케이션 코드를 작성하면 된다.
unittest 모듈 사용하기
실제 코드 작성에 앞서 몇 가지 불편한 사항들에 대해 짚고 넘어가야한다. 기능 테스트는 매우 자주 실행되어야 하는 프로세스인데 한 번 실행할 때마다 여러 불편한 사항들이 발생한다.
먼저, 발생하는 에러에 대한 좀 더 자세한 설명이 필요하다. 위의 AssertionError
의 경우에도 10번 줄에서 AssertionError
가 발생했다고만 알려줄 뿐 자세한 내용은 알 수가 없다. 예상할 수 있는 에러이기에 원인을 금방 알아차렸지만 만약 예상치못한 에러가 발생하면 디버깅에 큰 문제가 따를 수 있다.
또, 테스트 코드를 실행할 때 생성된 웹브라우저가 테스트가 끝난 후에도 계속 남아있다. 여러번의 테스트를 반복하는 경우 여러 개의 웹브라우저가 계속 쌓일 것이고, 이것을 일일이 꺼주는 것도 상당히 번거로울 것이다.
이러한 불편 사항은 테스트 환경을 구축할 때 자주 발생하는 사항들이며, 이를 극복하기 위해 기본 라이브러리의 unittest
라는 모듈이 제공된다.
unittest
모듈은 테스트 코드를 클래스의 형태로 입력하여 객체화 할 수 있도록 해주며, 테스트를 위한 여러가지 편의 기능을 제공한다.
functional_test.py
를 다음과 같이 바꿔보자.
# functional_test.py
from selenium import webdriver
import unittest
class NewVisitorTest(unittest.TestCase): #1
def setUp(self): #2
self.browser = webdriver.Chrome('chromewebdriver 경로')
def tearDown(self): #3
self.browser.quit()
def test_can_start_a_list_and_retrieve_it_later(self): #4
# 한 사용자가 해야할 일 목록을 만들어주는 `To-Do list` 앱에 대해 듣고
# 직접 확인해보러 홈페이지에 접속한다.
self.browser.get('http://localhost:8000')
# 홈페이지의 `타이틀` 과 `헤더` 에 `To-Do list` 라고 적혀있는 것을 보고
# 제대로 찾아온 것을 확인한다.
self.assertIn('To-Do', self.browser.title) #5
self.fail('Finish the test!') #6
if __name__ == '__main__': #7
unittest.main() #8
#1: class NewVisitorTest(unittest.TestCase):
테스트 코드들은 이제 unittest.TestCase
를 상속받는 클래스의 메소드로 정의되었다.
#2, #3: setUp, tearDown
setUP
과 tearDown
은 특별한 역할을 수행하는 미리 정의된 메소드들이다.
setUP
은 테스트가 시작될 때 실행되는 메소드이며, tearDown
은 테스트가 끝날 때 실행되는 메소드이다.
이 둘은 try/except
문과 비슷하게 동작한다. 테스트 코드를 실행하던 중 에러가 발생하면 tearDown
메소드가 실행되는 식이다.
setUP
메소드의 self.browser = webdriver.Chrome
명령으로 테스트가 시작될 때 웹브라우저를 시작하고, tearDown
메소드의 self.browser.quit()
명령으로 테스트가 끝나거나 혹은 에러가 발생시, 웹브라우저를 자동으로 닫아준다. 이를 통해 번거로운 작업이 하나 줄어들게 된다.
#4: test_can_start_a_list_and_retrieve_it_later
test
로 시작하는 이름을 가진 메소드는 실행되는 테스트에 포함된다. 여러개의 test
메소드를 정의할 수 있다. 테스트 메소드의 이름을 지을 때는 사용자 시점에서 어떤 동작을 구현하는 지를 자세히 적어주면 좋다.
test_can_start_a_list_and_retrieve_it_later
메소드에는 사용자가 리스트를 만들수 있고, 나중에 다시 그 리스트를 확인할 수 있다는 기능을 테스트하는데에 필요한 테스트 코드들이 포함된다.
#5: self.assertIn
이제 assert
대신, self.assertIn
과 같은 어썰트 메소드 (assert methods)
를 통해 테스트를 수행한다.
다음은 자주 사용하는 기본적인 어썰트 메소드들이다.
메소드 | 기능 |
---|---|
assertEqual(a, b) | a == b |
assertNotEqual(a, b) | a != b |
assertTrue(x) | bool(x) is True |
assertFalse(x) | bool(x) is False |
assertIs(a, b) | a is b |
assertIsNot(a, b) | a is not b |
assertIsNone(x) | x is None |
assertIsNotNone(x) | x is not None |
assertIn(a, b) | a in b |
assertNotIn(a, b) | a not in b |
assertIsInstance(a, b) | isinstance(a, b) |
assertNotIsInstance(a, b) | not isinstance(a, b) |
#6: self.fail
어떤 경우이던 상관없이 self.fail
이 실행되면 에러 메세지와 함께 테스트가 정지된다. 특정 부분까지만 테스트를 실행하거나 할 때 응용할 수 있다.
#7: if name == ‘main’:
functional_test.py
가 터미널에서 바로 실행되었음을 Python에 알려주는 부분이다.
#8: unittest.main()
unittest.main()
은 test runner
를 실행시켜 정의된 모든 테스트를 실행한다. #7
의 조건을 만족하면, 파일안에 정의된 모든 테스트 클래스와 test
메소드들을 실행하도록 설정한 것이다.
이제 다시 functional_test.py
를 실행해보면 아래와 같이 에러 메세지가 뜬다.
F
======================================================================
FAIL: test_can_start_a_list_and_retrieve_it_later (__main__.NewVisitorTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "funcional_test.py", line 21, in test_can_start_a_list_and_retrieve_it_later
self.assertIn('To-Do', self.browser.title) #5
AssertionError: 'To-Do' not found in 'Welcome to Django'
----------------------------------------------------------------------
Ran 1 test in 4.899s
FAILED (failures=1)
이제 어떤 클래스의 어떤 메소드에서 에러가 발생했으며, AssertionError
는 좀 더 자세한 에러 내용을 표시해준다. 또 몇 개의 테스트가 얼마 동안 실행되었으며, 실패한 테스트가 몇 개인지도 알려준다.
Reference
Test-Driven Development with Python, Harry Percival: http://chimera.labs.oreilly.com/books/1234000000754/ch02.html
Python 공식 문서: https://docs.python.org/3/library/unittest.html