Python 테스트 시작하기
이호성
http://slides.com/hosunglee-1/deck/live
에서 함께 보실 수 있습니다
Whitebox testing
(Unit Test)
(Function Test)
# portfolio.py from http://nedbatchelder.com/text/st.html#7
class Portfolio(object):
"""간단한 주식 포트폴리오"""
def __init__(self):
# stocks is a list of lists:
# [[name, shares, price], ...]
self.stocks = []
def buy(self, name, shares, price):
"""name 주식을 shares 만큼 주당 price 에 삽니다"""
self.stocks.append([name, shares, price])
def cost(self):
"""이 포트폴리오의 총액은 얼마일까요?"""
amt = 0.0
for name, shares, price in self.stocks:
amt += shares * price
return amt
Python 3.4.0 (default, Jun 19 2015, 14:20:21)
[GCC 4.8.2] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from portfolio import Portfolio
>>> p = Portfolio()
>>> print(p.cost())
0.0
>>> p.buy('Google', 100, 176.48)
>>> p.cost()
17648.0
>>> p.buy('Yahoo', 100, 36.15)
>>> p.cost()
21263.0
from portfolio import Portfolio
p = Portfolio()
print("Empty portfolio cost: %s, should be 0.0" % p.cost())
p.buy('Google', 100, 176.48)
print("With 100 Google @ 176.48: %s, should be 17648.0" % p.cost())
p.buy('Yahoo', 100, 36.15)
print("With 100 Yahoo @ 36.15: %s, should be 21263.0" % p.cost())
Empty portfolio cost: 0.0, should be 0.0
With 100 Google @ 176.48: 17648.0, should be 17648.0
With 100 Yahoo @ 36.15: 21263.0, should be 21263.0
from portfolio import Portfolio
p = Portfolio()
print("Empty portfolio cost: %s, should be 0.0" % p.cost())
p.buy('Google', 100, 176.48)
assert p.cost() == 17649.0 # Failed
print("With 100 Google @ 176.48: %s, should be 17648.0" % p.cost())
p.buy('Yahoo', 100, 36.15)
assert p.cost() == 21263.0
print("With 100 Yahoo @ 36.15: %s, should be 21263.0" % p.cost())
Empty portfolio cost: 0.0, should be 0.0
Traceback (most recent call last):
File "portfolio_test2.py", line 6, in <module>
assert p.cost() == 17649.0 # Failed
AssertionError
(들어봤지만 친하지 않은 그대)
# portfolio_test3.py
import unittest
from portfolio import Portfolio
class PortfolioTest(unittest.TestCase):
def test_google(self):
p = Portfolio()
p.buy("Google", 100, 176.48)
self.assertEqual(17648.0, p.cost())
if __name__ == '__main__':
unittest.main()
$ python portfolio_test3.py
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
- 실패하라고 테스트는 만드는 겁니다.
# portfolio_test4.py
import unittest
from portfolio import Portfolio
class PortfolioTestCase(unittest.TestCase):
def test_google(self):
p = Portfolio()
p.buy("Goole", 100, 176.48)
self.assertEqual(17648.0, p.cost())
def test_google_yahoo(self):
p = Portfolio()
p.buy("Google", 100, 176.48)
p.buy("Yahoo", 100, 36.15)
self.assertEqual(21264.0, p.cost()) # 21263.0
if __name__ == '__main__':
unittest.main()
$ python portfolio_test4.py
.F
======================================================================
FAIL: test_google_yahoo (__main__.PortfolioTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
File "portfolio_test4.py", line 15, in test_google_yahoo
self.assertEqual(21264.0, p.cost())
AssertionError: 21264.0 != 21263.0
----------------------------------------------------------------------
Ran 2 tests in 0.005s
FAILED (failures=1)
$ python portfolio_test4.py PortfolioTestCase.test_google_yahoo
F
======================================================================
FAIL: test_google_yahoo (__main__.PortfolioTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
File "portfolio_test4.py", line 15, in test_google_yahoo
self.assertEqual(21264.0, p.cost())
AssertionError: 21264.0 != 21263.0
----------------------------------------------------------------------
Ran 1 test in 0.005s
FAILED (failures=1)
$ python -m unittest discover --help
Usage: python -m unittest discover [options]
Options:
,,,
-s START, --start-directory=START
Directory to start discovery ('.' default)
-p PATTERN, --pattern=PATTERN
Pattern to match tests ('test*.py' default)
-t TOP, --top-level-directory=TOP
Top level directory of project (defaults to start
directory)
$ python -m unittest discover
----------------------------------------------------------------------
Ran 15 tests in 0.130s
OK
class Portfolio(object):
"""간단한 주식 포트폴리오"""
def __init__(self):
# stocks is a list of lists:
# [[name, shares, price], ...]
self.stocks = []
def buy(self, name, shares, price):
"""name 주식을 shares 만큼 주당 price 에 삽니다"""
self.stocks.append([name, shares, price])
if not isinstance(shares, int):
raise Exception("shares must be an integer")
def cost(self):
"""이 포트폴리오의 총액은 얼마일까요?"""
amt = 0.0
for name, shares, price in self.stocks:
amt += shares * price
return amt
import unittest
from portfolio import Portfolio
class PortfolioTestCase(unittest.TestCase):
def test_google(self):
p = Portfolio()
p.buy("Goole", "many", 176.48)
self.assertEqual(17648.0, p.cost())
if __name__ == '__main__':
unittest.main()
$ python ./portfolio_test5.py
E
======================================================================
ERROR: test_google (__main__.PortfolioTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
File "./portfolio_test5.py", line 8, in test_google
p.buy("Goole", "many", 176.48)
File "/home/leclipse/git/pycon-testing/unit_test/portfolio.py", line 14, in buy
raise Exception("shares must be an integer")
Exception: shares must be an integer
----------------------------------------------------------------------
Ran 1 test in 0.001s
FAILED (errors=1)
import unittest
from portfolio import Portfolio
class PortfolioTestCase(unittest.TestCase):
def test_google(self):
p = Portfolio()
with self.assertRaises(Exception) as context:
p.buy("Goole", "many", 176.48)
self.assertTrue("shares must be an integer", context.exception)
if __name__ == '__main__':
unittest.main()
$ python ./portfolio_test6.py
.
----------------------------------------------------------------------
Ran 1 test in 0.004s
OK
import unittest
class WidgetTestCase(unittest.TestCase):
def setUp(self):
self.widget = Widget('The widget')
def tearDown(self):
self.widget.dispose()
self.widget = None
def test_default_size(self):
self.assertEqual(self.widget.size(), (50,50),
'incorrect default size')
def test_resize(self):
self.widget.resize(100,150)
self.assertEqual(self.widget.size(), (100,150),
'wrong size after resize')
import unittest
class NumberTest(unittest.TestCase):
def test_even(self):
for i in range(0, 6):
self.assertEqual(i % 2, 0)
def test_even_with_subtest(self):
for i in range(0, 6):
with self.subTest(i=i):
self.assertEqual(i % 2, 0)
unittest.main()
$ python ./subtest.py
F
======================================================================
FAIL: test_even (__main__.NumberTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "./subtest.py", line 7, in test_even
self.assertEqual(i % 2, 0)
AssertionError: 1 != 0
======================================================================
FAIL: test_even_with_subtest (__main__.NumberTest) (i=1)
----------------------------------------------------------------------
Traceback (most recent call last):
File "./subtest.py", line 12, in test_even_with_subtest
self.assertEqual(i % 2, 0)
AssertionError: 1 != 0
======================================================================
FAIL: test_even_with_subtest (__main__.NumberTest) (i=3)
----------------------------------------------------------------------
Traceback (most recent call last):
File "./subtest.py", line 12, in test_even_with_subtest
self.assertEqual(i % 2, 0)
AssertionError: 1 != 0
======================================================================
FAIL: test_even_with_subtest (__main__.NumberTest) (i=5)
----------------------------------------------------------------------
Traceback (most recent call last):
File "./subtest.py", line 12, in test_even_with_subtest
self.assertEqual(i % 2, 0)
AssertionError: 1 != 0
----------------------------------------------------------------------
Ran 2 tests in 0.001s
FAILED (failures=4)
python 3.4 에 추가 되었음
# test_runner.py
def do_test():
for testcase in testsuite:
for test_method in testcase.test_methods:
try:
testcase.setUp()
except:
[record error]
else:
try:
test_method()
except AssertionError:
[record failure]
except:
[record error]
else:
[record success]
finally:
try:
testcase.tearDown()
except:
[record error]
print(do_test())
def get_username(user_id):
user = db.user.query(user_id = user_id)
return user.name
def delete_user(user_id):
return db.user.delete(user_id = user_id)
의존성이 있는것들을 실제로 실행시키지 말고 호출 여부, 인터페이스만 확인 하자
>>> class Class():
... def add(self, x, y):
... return x + y
...
>>> inst = Class()
>>> def not_exactly_add(self, x, y):
... return x * y
...
>>> Class.add = not_exactly_add
>>> inst.add(3, 3)
9
>>> from unittest.mock import MagicMock
>>> thing = ProductionClass()
>>> thing.method = MagicMock(return_value=3)
>>> thing.method(3, 4, 5, key='value')
3
>>> thing.method.assert_called_with(3, 4, 5, key='value')
# Rm.py
import os
def rm(filename):
os.remove(filename)
# test_rm.py
from Rm import rm
class RmTestCase(unittest.TestCase):
tmpfilepath = os.path.join(tempfile.gettempdir(), 'temp-testfile')
def setUp(self):
with open(self.tmpfilepath, 'w') as f:
f.write('Delete me!')
def tearDown(self):
if os.path.isfile(self.tmpfilepath):
os.remove(self.tmpfilepath)
def test_rm(self):
rm(self.tmpfilepath)
self.assertFalse(os.path.isfile(self.tmpfilepath), 'Failed to remove the file')
import os.path
import tempfile
import unittest
from unittest import mock
from Rm import rm
class RmTestCase(unittest.TestCase):
@mock.patch('Rm.os')
def test_rm(self, mock_os):
rm('/tmp/tmpfile')
mock_os.remove.assert_called_with('/tmp/tmpfile')
if __name__ == '__main__':
unittest.main()
test_rm
Rm.rm
os.remove
mock_os.
remove
# Rm.py
import os
def rm(filename):
print(os.remove)
os.remove(filename)
$ python ./test_rm.py
<built-in function remove>
.
----------------------------------------------------------------------
Ran 1 test in 0.007s
OK
$ python ./mock_rm.py
<MagicMock name='os.remove' id='139901238735592'>
.
----------------------------------------------------------------------
Ran 1 test in 0.002s
OK
class SimpleFacebook(object):
def __init__(self, oauth_token):
self.graph = facebook.GraphAPI(oauth_token)
def post_message(self, message):
self.graph.put_object('me', 'feed', message=message)
class SimpleFacebookTestCase(unittest.TestCase):
@mock.patch.object(facebook.GraphAPI, 'put_object', autospect=True)
def test_post_message(self, mock_put_object):
sf = SimpleFacebook('fake oauth token')
sf.post_message('Hello World!')
mock_put_object.assert_called_with('me', 'feed', message='Hello World!')
facebook 이 다운되어도 내 테스트는 실패 하지 않는다.
# 24시간이 지난 경우에만 삭제 합니다.
def rm(filename):
file_modified = datetime.datetime.fromtimestamp(os.path.getmtime(filename))
if datetime.datetime.now() - file_modified > datetime.timedelta(hours=24):
os.remove(filename)
class RmTestCase(unittest.TestCase):
@mock.patch('__main__.os')
def test_rm(self, mock_os):
mock_os.path.getmtime.return_value = time.time()
rm('/tmp/tmpfile')
self.assertFalse(mock_os.remove.called)
mock_os.path.getmtime.return_value = time.time() - 86400*2
rm('/tmp/tmpfile')
mock_os.remove.assert_called_with('/tmp/tmpfile')
보통 분기를 따라가면서 테스트 하기는 쉽지 않음
# Rm.py
class MyError(Exception):
pass
def rm(filename):
try:
os.remove(filename)
except FileNotFoundError:
raise MyError
class RmTestCase(unittest.TestCase):
@mock.patch.object(os, 'remove', side_effect=FileNotFoundError)
def test_rm_without_file(self, mock_remove):
with self.assertRaises(MyError) as context:
rm('not_exist_file')
Exception 이 발생되는 경우를 만들지 않아도 됨
다른 모듈,서비스 등을 붙여서 그 관계에서의
문제점을 확인하는 과정
나만 쓰는 프로덕션 환경과 동일한 테스트 환경이 있으면 좋겠다.
# client.py
import sys
import redis
class Client():
def __init__(self):
self.conn = redis.StrictRedis("redis")
def set(self, key, value):
self.conn.set(key, value)
def get(self, key):
return self.conn.get(key)
실제 Redis 와 연결했을 때 잘 동작하는지 확인이 필요하다.
# test_client.py
import unittest
from client import Client
class ClientTestCase(unittest.TestCase):
def test_with_redis(self):
client = Client()
client.set('tomato', 2)
self.assertEqual(2, int(client.get('tomato')))
나만 쓰는 프로덕션 환경과 동일한 테스트 환경이 있으면 좋겠다.
Host
Host
FROM python:3.4
MAINTAINER lee ho sung
RUN pip install redis
RUN mkdir /code
ADD . /code
WORKDIR /code
CMD python -m unittest discover
Docker 이미지를 정의한다.
client:
build: .
links:
- redis
redis:
image: redis
ports:
- 6379:6379
192.168.0.2
192.168.0.1
/etc/hosts
redis 192.168.0.2
$ docker-compose up
redis_1 | 1:M 28 Aug 06:05:53.613 # Server started, Redis version 3.0.3
redis_1 | 1:M 28 Aug 06:05:53.613 # WARNING overcommit_memory is set to 0! Background save may fail under low memory condition. To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf and then reboot or run the command 'sysctl vm.overcommit_memory=1' for this to take effect.
redis_1 | 1:M 28 Aug 06:05:53.613 # WARNING you have Transparent Huge Pages (THP) support enabled in your kernel. This will create latency and memory usage issues with Redis. To fix this issue run the command 'echo never > /sys/kernel/mm/transparent_hugepage/enabled' as root, and add it to your /etc/rc.local in order to retain the setting after a reboot. Redis must be restarted after THP is disabled.
redis_1 | 1:M 28 Aug 06:05:53.613 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.
redis_1 | 1:M 28 Aug 06:05:53.614 * DB loaded from disk: 0.000 seconds
redis_1 | 1:M 28 Aug 06:05:53.614 * The server is now ready to accept connections on port 6379
client_1 | .
client_1 | ----------------------------------------------------------------------
client_1 | Ran 1 test in 1.003s
client_1 |
client_1 | OK
integrationtest_client_1 exited with code 0
Gracefully stopping... (press Ctrl+C again to force)
Stopping integrationtest_redis_1... done
$
from selenium import webdriver
driver = webdriver.Firefox()
driver.get("http://www.python.org")
assert "Python" in driver.title
import unittest
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
class PyconUserTestCase(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
def test_log_in_pycon_2015(self):
driver = self.driver
driver.get("http://www.pycon.kr/2015/login")
# US1 : 사용자는 Pycon Korea 2015 페이지 제목을 볼 수 있습니다.
self.assertIn("PyCon Korea 2015", driver.title)
# US2 : 사용자는 로그인을 할 수 있습니다.
# 로그인 하면 "One-time login token ..." 메시지를 볼 수 있습니다.
elem = driver.find_element_by_id("id_email")
elem.send_keys("email-me@gmail.com")
elem.send_keys(Keys.RETURN)
self.assertIn("One-time login token url was sent to your mail",
driver.page_source)
def tearDown(self):
self.driver.close()
if __name__ == "__main__":
unittest.main(warnings='ignore')
최소 : 테스트 케이스가 메인 로직을 검증 한다.
최대 : 개발 시간의 2배 이상을 쓰지 않는다.
이 사이에서 가장 가성비 높은 지점을
찾아내는 것이 좋은 개발자!
테스트는 힘들지만
가치 있는 일입니다
지금 시작하세요!