• Home
  • About
    • Che1's Blog photo

      Che1's Blog

      Che1's Dev Blog

    • Learn More
    • Facebook
    • Instagram
    • Github
    • Steam
    • Youtube
  • Posts
    • All Posts
    • Django
    • Python
    • Front-end
    • Algorithm
    • etc
    • All Tags
  • Projects

[TDD Tutorial] 1-2. 기능 테스트의 확장

21 Oct 2017

Reading time ~5 minutes

기능 테스트는 사용자의 시점에서 사람이 이해하기 쉬운 유저 스토리 (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 는 좀 더 자세한 에러 내용을 표시해준다. 또 몇 개의 테스트가 얼마 동안 실행되었으며, 실패한 테스트가 몇 개인지도 알려준다.


Tutorial 목차
  • [TDD Tutorial] 1-3. 유닛 테스트
  • [TDD Tutorial] 1-2. 기능 테스트의 확장
  • [TDD Tutorial] 1-1. 기능 테스트를 통해 장고 시작하기
  • [TDD Tutorial] 사전 준비

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



TDDTutorial Share Tweet +1