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
>>> fromcontent.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
잘 짜여진 테스트코드가 있으면
테스트 코드는 거의 그대로 읽을 수 있음
높은 가독성 - 요구사항을 놓치기가 더 힘듦
잘못짜는게 더 힘듦
잘못짜서 수정시 테스트를 실행만 해보면 바로 확인
코드 수정 - 확인에 걸리는 시간 감소
자주 코드를 고칠 수 있음 - 실력 향상!
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
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 plushttps://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
23.
자주 쓰는 Shortcut을확장하여 사용해보기
class TestCategoryAPIView(TestCase):
def test_create(self):
self.post_check(201, 'api:admin:category-list', data={
'service_name': 'foo', 'slug': 'drama', 'kind': 'category',
'title_ko': '드라마', 'title_en': 'Drama'
})
class TestCase(PlusTestCase):
def request_check(self, method, status_code, url, *args, **kwargs):
if method != 'get' and 'data' in kwargs:
try:
kwargs['data'] = json.dumps(kwargs.get('data', {}))
kwargs['extra'] = {'content_type': 'application/json'}
except TypeError:
pass
response = self.request(method, url, *args, **kwargs)
self.assertEqual(status_code, response.status_code)
return response
def post_check(self, status_code, url, *args, **kwargs):
return self.request_check('post', status_code, url, *args, **kwargs)
ODK Media LTE 2020.4.23 / 정경업
23
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 freezegunimport 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