1. 테스트가 뭐예요?
ODK Media LTE 2020.4.23 / 정경업
테스트가 뭐예요?
어디서 쓰나요?
내 프로젝트에는 어떻게 넣나요?
1
2. 정경업(별명: 파이)
ODK 입사 2주년(아직 청정수)
ODK Lighting Talk Engineering (강제)진행자(발표 브로커)
다른 발표들
Media IO가 뭐죠? (ODK Media 2020.1.10)
신입에서 CTO까지, 야근하지 않는 웹개발 (PyCon Korea 2017)
Django로 쇼핑몰 만들자 (PyCon Korea 2016)
ODK Media LTE 2020.4.23 / 정경업
ODK Media / Backend Lead Senior Software Engineer
2
3. 먼 옛날
석우징: 짜잔, 이렇게 짜면 됩니다.
정경업: 뭐 하러 그렇게까지 만들어요???
* 실화
미안하다 이거 보여주려고 어그로끌었다.. 나루토 사스케 싸움수준 ㄹㅇ실화냐? 진짜 세
계관최강자들의 싸움이다.. 그찐따같던 나루토가 맞나? 진짜 나루토는 전설이다..
ODK Media LTE 2020.4.23 / 정경업
석우징(갓우징>가두징) 본좌께서
Django 스터디 그룹에 코딩 도장을 운영하며
직접 테스트 케이스를 짜며 시범을 보이시던 때
3
4. 테스트가 뭐고 어떻게 쓰는지
경험을 공유합니다.
ODK Media LTE 2020.4.23 / 정경업
Python/Django 프로젝트에서
4
5. 우리는 코드를 짜고 확인합니다.
ODK Media LTE 2020.4.23 / 정경업
5
7. if 될까() == 'A':
print('A')
if 될까() == 'B':
print('B')
if 될까() == 'C':
print('C')
if 될까() == 'D':
print('D')
코드 작성 -> 실행 -> print 확인 X 확인할 숫자
더 많아지면..?
ODK Media LTE 2020.4.23 / 정경업
조건이 복잡하면?
7
8. Program - Episode 모델이 있을 때
Episode의 재생을 허용하는 국가인지 확인하는 함수
episode.is_allowed_country('US')
요구사항
기본적으로 Program으로 전체적인 허용 국가를 정하고요
특정 Episode별 허용 국가를 달리하고 싶어요
아무것도 설정된게 없으면 다 나오고요
ODK Media LTE 2020.4.23 / 정경업
하나 짜봅시다.
8
9. class AllowedCountriesModel(models.Model):
allowed_countries = ArrayField(
models.CharField(max_length=2, blank=True, default=list,
help_text='Allowed Countries(ISO-3166 Alpha-2 code)')
class Meta:
abstract = True
class Program(AllowedCountriesModel, models.Model):
# 생략했지만 뭔가 많은 필드들 1
class Episode(AllowedCountriesModel, models.Model):
program = models.ForeignKey(Program)
# 생략했지만 뭔가 많은 필드들 2
def is_allowed_country(self, country: str) -> bool:
if self.allowed_countries:
return country in self.allowed_countries
if self.program.allowed_countries:
return country in self.program.allowed_countries
return True
ODK Media LTE 2020.4.23 / 정경업
9
10. 잘 짰나 확인을 해봐야죠?
ODK Media LTE 2020.4.23 / 정경업
어떻게?
10
11. 1. Shell
>>> from content.models import Program, Episode
>>> program = Program.objects.create(allowed_countries=['KR'])
>>> episode = Episode.objects.create(allowed_countries=[])
>>> episode.is_allowed_country('US')
>>> False
>>> episode.allowed_countries=['US']
>>> episode.is_allowed_country('US')
>>> True
>>> # To be coutinue...!
가능한 시나리오를 모두 열심히 타이핑
실패하면 코드를 바꾸고 처음부터 다시(혹은 복사 붙여넣기)
ODK Media LTE 2020.4.23 / 정경업
11
12. 2. Admin
Program 생성 - Episode 생성 - is_allowed_country 확인 x 시나리오 수
ODK Media LTE 2020.4.23 / 정경업
12
13. 3. Test code
from parameterized import parameterized
from django.test import TestCase
class TestEpisodeModel(TestCase):
@parameterized.expand([
# (program_countries, episode_countries, expected)
(['KR', 'CA'], [], False),
(['US', 'CA'], ['CA', 'KR'], False),
(['CA', 'KR'], ['US', 'KR'], True),
([], ['US', 'KR'], True),
([], [], True),
])
def test_is_allowed_country(self, pr_countries, ep_countries, expected):
program = ProgramFactory(allowed_countries=pr_countries)
episode = EpisodeFactory(program=program, allowed_countries=ep_countries)
self.assertEqual(episode.is_allowed_country('US'), expected)
> python manage.py test
ODK Media LTE 2020.4.23 / 정경업
13
14. 잘 짜여진 테스트 코드가 있으면
테스트 코드는 거의 그대로 읽을 수 있음
높은 가독성 - 요구사항을 놓치기가 더 힘듦
잘못짜는게 더 힘듦
잘못짜서 수정시 테스트를 실행만 해보면 바로 확인
코드 수정 - 확인에 걸리는 시간 감소
자주 코드를 고칠 수 있음 - 실력 향상!
ODK Media LTE 2020.4.23 / 정경업
14
15. 코드를 믿을 수 있음 - 장애 덜 남 - 남는 시간에 기능 업데이트
라이브러리 업데이트 쉬움 - 최신 코드 유지
코드 리펙토링에 부담이 적음 - 자주함 - 실력 향상!
코드를 고쳤을때 영향이 가는 모든 곳을 직접 확인 해야함
힘드니까 업데이트 안함 - 낡아가는 코드 - 점점 안(못) 고침
뭘 고치면 장애도 같이 남 - 장애 대응 - 실력 향상은 언제?
ODK Media LTE 2020.4.23 / 정경업
모든 코드에 테스트가 있다면?
없으면?
15
16. 테스트는 작은 단위 일수록 짜기 쉽다
분할 정복 실현 - 한 함수에서 하는 일은 하나만
의존성이 낮을수록 짜기 쉽다
의존성 관리 - 불필요한 의존을 제거
자연스럽게 클린 아키텍처에 가까워 질 수 있다
기능 추가 쉬워짐
요구사항에 유연하게 대응 가능
ODK Media LTE 2020.4.23 / 정경업
테스트 잘 짜려고 하다보면?
16
17. 고칠 용기를 주는 코드 - 최대한 명확하고 짧고 간결
테스트 코드 간 의존성 없게 - 멱등성*, 사이드 이펙트 X, 병렬 실행(속도)
얼마나? 최대한, 가능한 모든 코드에 테스트 코드 작성
언제? 코드를 먼저 짜든, 테스트를 먼저 짜든, 상황에 따라
* 멱등성: 수학이나 전산학에서 연산의 한 성질을 나타내는 것으로, 연산을 여러 번 적용
하더라도 결과가 달라지지 않는 성질을 의미
ODK Media LTE 2020.4.23 / 정경업
어떤 테스트 코드가 잘 짠걸까?
17
19. Pycharm 같은 IDE 추천
레벨이 딸리면 장비라도 좋아야 사냥을 할 수 있다.
장비빨은 현질이 짱임
ODK Media LTE 2020.4.23 / 정경업
일단 쉽게 돌릴 수 있는 환경을 만들자
19
20. 테스트 코드에 필요한 구조화 된 값(모델 등)을 손쉽게 생성 / 대체
반복적인 테스트 코드의 가독성을 극적으로 향상
Factory Boy https://factoryboy.readthedocs.io/
데이터 구조 정의를 쉽게 할 수 있음
유연하고 풍부한 기능들
여러 ORM 지원 (Django 등)
ODK Media LTE 2020.4.23 / 정경업
Fixtures tool
20
21. # Factory Boy 같은게 없다면? 매번 모든 필드 할당
category = Category.objects.create(**{
'service_name': 'foo',
'slug': 'drama',
'kind': CategoryType.CATEGORY.value
'title': '드라마'
})
# Factory Boy를 쓰면? 상황에 따라 필요한 필드만 할당
category = CategoryFactory(slug='drama')
# Factory 정의
class CategoryFactory(factory.django.DjangoModelFactory):
class Meta:
model = Category
service_name = factory.fuzzy.FuzzyChoice(choices=['foo', 'bar'])
slug = factory.Sequence(lambda n: f'{fake.slug()[:10]}-{n}')
kind = CategoryType.CATEGORY.value
title = factory.Faker('text', max_nb_chars=5)
ODK Media LTE 2020.4.23 / 정경업
21
22. Django test plus https://django-test-plus.readthedocs.io/
Django 기본 TestCase 보다
풍부한 Shortcut을 제공하여 코드량을 줄이고 가독성도 높임
from django.core.urlresolvers import reverse
def test_status(self):
response = self.client.get(reverse('my-url-name'))
self.assertEqual(response.status_code, 200)
def test_better_status(self):
response = self.get('my-url-name')
self.assert_http_200_ok(response)
ODK Media LTE 2020.4.23 / 정경업
TestCase Shortcuts
22
24. 회원 가입 API의 Test code 예시
class TestUserAPIView(TestCase):
def test_sign_up(self):
username = 'new@test.com'
response = self.post_check(201, 'api:v1:user-sign-up', data={
'username': username,
'password': 'password',
'confirm_password': 'password',
'code': generate_verification_code(username, 'email')
})
detail = response.json()['detail']
user = User.objects.get(username=username)
self.assertEqual(user.username, detail['user']['username'])
self.assertTrue(user.is_active)
self.assertEqual(Token.objects.get(user=user).key, detail['token'])
# welcome email
sent = mail.outbox[0]
self.assertEqual(sent.subject, 'Welcome!')
self.assertIn(user.username, sent.variables)
ODK Media LTE 2020.4.23 / 정경업
24
25. parameterized https://pypi.org/project/parameterized/
테스트 함수를 데코레이터로 정의된 내용대로 반복 실행
class TestCategoryAdminAPIViewPermissions(TestCase):
@parameterized.expand([(None,), 'user']) # 한번 정의 해서 두번 확인
def test_permissions(self, user):
if user:
self.login(getattr(self, user))
category = CategoryFactory()
self.get_check(403, 'api:admin:category-list')
self.post_check(403, 'api:admin:category-list')
self.get_check(403, 'api:admin:category-detail', pk=category.id)
self.put_check(403, 'api:admin:category-detail', pk=category.id)
self.patch_check(403, 'api:admin:category-detail', pk=category.id)
self.delete_check(403, 'api:admin:category-detail', pk=category.id)
ODK Media LTE 2020.4.23 / 정경업
반복적인 테스트 줄이기
25
26. unittest.subTest
https://docs.python.org/3/library/unittest.html#distinguishing-test-
iterations-using-subtests
반복문 안의 assert를 각각 다른 테스트로 추적해 줌
class TestCategoryAPIView(TestCase):
def test_detail(self):
drama = CategoryFactory(slug='drama')
self.get_check(200, 'api:admin:category-detail', pk=drama.id)
data = self.last_response.json()
for field in ['service_name', 'slug', 'kind', 'title_ko', 'title_en']:
with self.subTest(field=field):
# field가 각각 다르게 적혀있는 테스트로 인식함
self.assertEqual(data[field], getattr(drama, field))
ODK Media LTE 2020.4.23 / 정경업
26
27. unittest.mock https://docs.python.org/3/library/unittest.mock.html
class TestVideoAPIView(TestCase):
# 외부에 있는 Video API를 호출하는 함수를 mock
@patch('api.common.video_api.requests.get')
def test_list(self, mock_get):
# 응답값을 상황에 맞춰 가짜로 생성
mock_get.return_value = FakeResponse(status_code=200, content={'count': 3})
# 목록이 뜨는지 확인
response = self.get_check(200, 'api:admin:video-list')
self.assertEqual(response.json()['count'], 3)
* mock: 모조품, 가짜
ODK Media LTE 2020.4.23 / 정경업
외부 서비스 연동된 코드 테스트
27
28. freezegun https://pypi.org/project/freezegun/
from freezegun import freeze_time
class TestBannerAPIViewPick(TestCase):
def test_lives(self):
# 오늘(지금) 시작된 배너가
banner = BannerFactory(start=timezone.now())
# 시간을 어제로 돌려서 없는지 확인
yesterday = timezone.now() - timedelta(days=1)
with freeze_time(yesterday):
response = self.get_check(200, 'api:v1:banner-lives')
ODK Media LTE 2020.4.23 / 정경업
테스트 시간 바꾸기
28
29. 결론
테스트 코드 안 짤 이유가 없다
짤 때는 가독성을 신경 쓰자
여러 툴/라이브러리 잘 쓰자
ODK Media LTE 2020.4.23 / 정경업
테스트 코드로 빠르게 실력을 올리고
안정된 서비스로 개발 시간 확보하자
29