장고, DRF 테스트

Larry Jung·2022년 4월 2일
3
post-thumbnail

로앤굿에서 "장고 테스트" 세션을 진행할 때 사용한 자료입니다.

자동화된 테스트


장고 튜토리얼 part 5에서 발췌

자동화된 테스트란 무엇입니까?
테스트는 코드 작동을 확인하는 루틴입니다.

테스트는 다양한 수준에서 작동합니다. 일부 테스트는 작은 세부 사항에 적용될 수 있습니다 (특정 모델 메서드는 예상대로 값을 반환합니까?) 또 다른 테스트는 소프트웨어의 전반적인 작동을 검사합니다 (사이트에서 사용자 입력 시퀀스가 원하는 결과를 생성합니까?). 이것은 쉘을 사용하여 메소드의 동작을 검사하거나 사람이 애플리케이션을 실행하고 어떻게 작동하는지 확인하기 위해 데이터를 입력해서 테스트했던 것과 다르지 않습니다.

자동화 된 테스트에서 다른 점은 테스트 작업이 시스템에서 수행된다는 것입니다. 한 번 테스트 세트를 작성한 이후에는 앱을 변경할 때 수동 테스트를 수행하지 않아도 원래 의도대로 코드가 작동하는지 확인할 수 있습니다.

자동화된 테스트의 효능

  • 테스트를 통해 시간을 절약할 수 있습니다.
    -> 프로그램을 위해 자신의 시간을 써주는 고객들이 더 이상 시간 낭비를 하지 않게 된다.
  • 테스트는 문제를 그저 식별하는 것이 아니라 예방합니다
    -> 최악의 가독성 속에서도 제 역할을 하는 코드인지 밝혀낸다
  • 테스트는 코드를 더 매력적으로 만듭니다
    -> 동료가 나를 향한 최소한의 믿음을 갖게된다
  • 테스트는 팀이 함께 일하는 것을 돕습니다
    -> 동료의 실수를 알아차리기 쉽다

어디서 어떻게 시작해야 할지 모르더라도, 무작정 일단 만드는 것이 좋습니다.

  • 수정(추가)된 코드가 제대로 작동하는지 확인 쉬워짐
  • 수정되지 않은 코드가 제대로 작동하는지 확인 쉬워짐 (특히 마이그레이션 이슈)
  • 문제 발생 시 문제의 원인 점검하기 쉬워짐
  • 리팩터링이 쉬워지고, 전반적인 코드 퀄리티 보장

파이썬 테스트


테스트의 종류

유닛 테스트

  • 전체 코드 중 쪼갤 수 있는 가장 작은 부분(함수, 메소드 등)을 테스트하는 것이다.
  • 매우 간단하고 명확하다.
  • 스파게티 코드는 유닛 테스트를 어렵게한다. -> 바꿔 말하면, 주니어 개발자에게 유닛 테스트까지 시키면 스파게티를 방지하고 조금 더 질 높은 코드를 쓸 수 있게 해줌 -> 테스트코드도 가독성이 중요
  • 네트워크 연결이나 데이터베이스 등 외부 리소스가 포함되면 유닛테스트가 아니다.
    - 필요하다면 통신 됐음을 가정하고 가짜 결과값을 넣어줘야한다.
  • 테스트된 함수가 안전하게 수행되는 것(적어도 주어진 상황에서)을 보장해준다.
  • 문제 발생 지점이 명확함
  • 변경 사항이 있을 때 신뢰의 근거

통합 테스트

  • 분리된 모듈들을 같이 사용했을 때에도 신뢰를 구축하고 싶을 때 테스트한다
  • 실제 데이터베이스와 연결 등, 분리된 시스템을 통합하여 확인한다.
  • 일반적으로 유닛 테스트보다 복잡하고 작성시간이 오래 걸리므로 유닛 테스트를 더 많이 사용해야한다.
    - 단, 장고에서는 데이터베이스와의 연결이 유닛테스트 만큼이나 쉽도록 지원하므로 모든 뷰와 모델을 테스트하는 것을 권장한다.

기능 테스트

  • 기능 테스트는 어떤 어플리케이션이 제대로 동작하는지 완전한 기능을 테스트하는 것을 의미한다.
  • 예를 들어 웹 어플리케이션 기능 테스트를 위해 브라우저 자동화 도구에서 특정한 페이지를 클릭해보는 것
  • 기능 테스트는 작성하기 매우 어렵고, 높은 복잡성을 가지고 있기 때문에 많은 시간이 걸리며, 테스트가 실패하더라도 정확한 문제 지점을 알기 어렵다.

깨끗한 테스트 코드는 FIRST라는 5가지 규칙을 따라야 한다. clean code에서 발췌
1. Fast: 테스트는 빠르게 동작하여 자주 돌릴 수 있어야 한다.
2. Independent: 각각의 테스트는 독립적이며 서로 의존해서는 안된다.
3. Repeatable: 어느 환경에서도 반복 가능해야 한다.
4. Self-Validating: 테스트는 성공 또는 실패로 결과를 내어 자체적으로 검증되어야 한다.
5. Timely: 테스트는 적시에 구현해야 한다.

테스트 라이브러리

표준 라이브러리 unittest
표준 라이브러리 doctest
유명한 라이브러리 pytest
유명한 라이브러리 coverage

# doctest 공식문서에서 발췌. 실습용.
"""
This is the "example" module.

The example module supplies one function, factorial().  For example,

>>> factorial(5)
120
"""

def factorial(n):
    """Return the factorial of n, an exact integer >= 0.

 >>> [factorial(n) for n in range(6)]
 [1, 1, 2, 6, 24, 120]
 >>> factorial(30)
 265252859812191058636308480000000
 >>> factorial(-1)
 Traceback (most recent call last):
 ...
 ValueError: n must be >= 0

 Factorials of floats are OK, but the float must be an exact integer:
 >>> factorial(30.1)
 Traceback (most recent call last):
 ...
 ValueError: n must be exact integer
 >>> factorial(30.0)
 265252859812191058636308480000000

 It must also not be ridiculously large:
 >>> factorial(1e100)
 Traceback (most recent call last):
 ...
 OverflowError: n too large
 """

    import math
    if not n >= 0:
        raise ValueError("n must be >= 0")
    if math.floor(n) != n:
        raise ValueError("n must be exact integer")
    if n+1 == n:  # catch a value like 1e300
        raise OverflowError("n too large")
    result = 1
    factor = 2
    while factor <= n:
        result *= factor
        factor += 1
    return result

if __name__ == "__main__":
    import doctest
    doctest.testmod()
$ python example.py -v
Trying:
    factorial(5)
Expecting:
    120
ok
Trying:
    [factorial(n) for n in range(6)]
Expecting:
    [1, 1, 2, 6, 24, 120]
ok
Trying:
    factorial(30)
Expecting:
    265252859812191058636308480000000
ok
Trying:
    factorial(-1)
Expecting:
    Traceback (most recent call last):
        ...
    ValueError: n must be >= 0
ok
Trying:
    factorial(30.1)
Expecting:
    Traceback (most recent call last):
        ...
    ValueError: n must be exact integer
ok
Trying:
    factorial(30.0)
Expecting:
    265252859812191058636308480000000
ok
Trying:
    factorial(1e100)
Expecting:
    Traceback (most recent call last):
        ...
    OverflowError: n too large
ok
2 items passed all tests:
   1 tests in __main__
   6 tests in __main__.factorial
7 tests in 2 items.
7 passed and 0 failed.
Test passed.

파이썬 유닛 테스트의 원리

# unittest 공식문서에서 발췌
import unittest

class TestStringMethods(unittest.TestCase):

    def test_upper(self):
        self.assertEqual('foo'.upper(), 'FOO')

    def test_isupper(self):
        self.assertTrue('FOO'.isupper())
        self.assertFalse('Foo'.isupper())

    def test_split(self):
        s = 'hello world'
        self.assertEqual(s.split(), ['hello', 'world'])
        # check that s.split fails when the separator is not a string
        with self.assertRaises(TypeError):
            s.split(2)

if __name__ == '__main__':
    unittest.main()
...
----------------------------------------------------------------------
Ran 3 tests in 0.000s

OK

유닛 테스트는 test로 이름이 시작하는 모든 메소드들을 실행한다.
setup 메소드를 통해 해당 클래스의 사전 작업을 명시할 수 있다.
assert 메소드들을 통해 테스트의 실패를 명시할 수 있다. 실패하지 않으면 성공이다.
주로 밑의 메소드를 많이 사용

  • assertIs : is, is not 과 비슷
  • assertEqual : == 과 비슷
  • assertLess 또는 assertLessEqual
  • assertIn == assertTrue(a in b)

-v {0,1,2,3} 옵션으로 verbosity(상세도) 설정 가능
-r 옵션으로 유닛테스트를 역순으로 돌려볼 수 있음

장고 테스트

장고 테스트는 unittest를 상속했다. 다른 테스트도 있지만...

manage.py test를 했을 때 일어나는 일

  • 앱 안에서 test가 들어간 모듈 중 django.test.TestCase의 서브클래스를 찾는다
  • 테스트를 위한 데이터베이스를 생성한다 (아주 용이함!)
  • 이름이 test로 시작하는(prefix) method가 실행된다
  • self.assertSOMETHING 메소드를 통해서 테스트 실패여부를 알아낸다
  • 성공한 경우 쉘에 0을 리턴하고 실패하면 1을 리턴한다.

The request factory

장고 공식문서에서 발췌

from django.views.generic import TemplateView

class HomeView(TemplateView):
    template_name = 'myapp/home.html'

    def get_context_data(self, **kwargs):
        kwargs['environment'] = 'Production'
        return super().get_context_data(**kwargs)
from django.test import RequestFactory, TestCase
from .views import HomeView

class HomePageTest(TestCase):
    def test_environment_set_in_context(self):
        request = RequestFactory().get('/')
        view = HomeView()
        view.setup(request)

        context = view.get_context_data()
        self.assertIn('environment', context)

Test client

장고 공식문서에서 발췌

class QuestionIndexViewTests(TestCase):
    def test_no_questions(self):
        """
 If no questions exist, an appropriate message is displayed.
 """
        response = self.client.get(reverse('polls:index'))
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, "No polls are available.")
		self.assertQuerysetEqual(response.context['latest_question_list'], [])

CapturedQueriesContext, multiple databases 사용 (실사례)

from django.test.utils import CaptureQueriesContext
from rest_framework.test import APIRequestFactory, force_authenticate
from django.test import TestCase

class TestVideoViewSet(TestCase):
	databases = {'default', 'managerdb'}

	def setUp(self):
	
		new_videos = []
		
		for i in range(5000):
		
			video = Video(title= "video"+str(i))
		
			new_videos.append(video)
		
		created_videos = Video.objects.bulk_create(new_videos)


	def test_get_list_authenticated(self):	

		view = VideoViewSet.as_view({"get":"list"})
		
		factory = APIRequestFactory()
		
		user = User.objects.get(username= "larry")
		
	
		request = factory.get("videos/")
		
		force_authenticatxe(request, user= user)
		
		with CaptureQueriesContext(connection) as queries:
			response = view(request)
		self.assertEqual(response.status_code, 200)
		self.assertLessEqual(len(queries.captured_queries), 30)
  • assertContains 를 통해 손쉽게 HttpResponse에 필요한 텍스트가 들어있는지 확인 가능하다.

LiveServerTest (번외)

장고 공식문서에서 발췌

from django.contrib.staticfiles.testing import StaticLiveServerTestCase
from selenium.webdriver.firefox.webdriver import WebDriver

class MySeleniumTests(StaticLiveServerTestCase):
    fixtures = ['user-data.json']

    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        cls.selenium = WebDriver()
        cls.selenium.implicitly_wait(10)

    @classmethod
    def tearDownClass(cls):
        cls.selenium.quit()
        super().tearDownClass()

    def test_login(self):
        self.selenium.get('%s%s' % (self.live_server_url, '/login/'))
        username_input = self.selenium.find_element_by_name("username")
        username_input.send_keys('myuser')
        password_input = self.selenium.find_element_by_name("password")
        password_input.send_keys('secret')
        self.selenium.find_element_by_xpath('//input[@value="Log in"]').click()

DRF 테스트

유닛 테스트

  • 장고와 마찬가지로 공식 라이브러리 unittest와 별반 다르지 않게 손쉽게 테스트 가능

API 테스트

활용


Github action 적용

어떤 브랜치에서의 작업을 끝내면 PR을 올리기 전 로컬에서 직접 테스트를 돌려본다.
또는 PR을 올렸을 때 테스트가 통과되어야만 merge할 수 있도록 설정한다.
develop, main 브랜치에서는 테스트를 문제 없이 통과해야만 배포하도록 처리한다.

- name: Run test

env:

VIRTUAL_ENV: ./venv

run: |

source venv/bin/activate

python manage.py test

한 개의 테스트라도 실패하면 반환 값이 0이 아니므로 컨테이너는 종료되고 다음 액션을 하지 않는다.

테스트에 대한 팁들

더 많은 테스트를위한 아이디어

  • 테스트를 만들다보면 어플리케이션을 개선할 방법들이 떠오를 것이다.

  • 무엇이든 추가할거라면 테스트가 필요하다.

  • 테스트는 결코 관리가 어렵지 않다. 그냥 냅두면 된다. 꼭 TDD여야할 필요도 없다.

  • 상속되는 테스트 클래스를 정의하여 setUp 등을 매번 작성하지 않아도 되도록 조치할 수 있다.

  • auto field를 직접 비교하는 것은 부적합하다. 유닛테스트 순서에 따라 바뀔 수 있기 때문이다.

  • CI/CD에서는 테스트를 역순으로 하도록 하자

  • 커버리지를 주기적으로 돌려서 부족한 테스트를 채워넣자.

  • 커버리지는 절대적이지 않다. 커버된 코드여도 상황이 바뀌면(int->float) 문제가 일어날 수 있다. 중요하거나 복잡할 수록 가능할 수 있는 다양한 상황을 대입해야한다.

  • 너무 자명하고 간단한 것은 테스트할 필요가 없다. 오픈소스 개발자들이 이미 테스트를 했다.

  • 핵심적인 로직은 항상 테스트가 있어야한다.

테스트 설계 해보기

무엇을 테스트할까?

  • 뭐든 테스트 해야하지만, API (통합 테스트), 서비스 로직을 담은 함수나 메소드 등 (유닛 테스트)
  • 각 장고 앱에 파일구조에 맞춰서 테스트도 똑같이 만들기

어떤 값으로 비교할까?

  • 함수나 메소드의 경우
    - 가장 핵심적인 로직(리턴이나 객체의 속성 값 변경 등)을 직접 값 비교.
    - 경계값 분석이 효과적. (condition 마다)

  • API의 경우
    - POST, PATCH, PUT은 어떤 값을 넣어보고 db에서 해당 값을 갖고있는 row를 확인
    - DELETE 하고 db에서 해당 row가 존재하지 않는 것을 확인
    - GET(list, retrieve) 는 주로 serializer가 잘 작동하는지 확인
    - 복잡한 권한 또한 정상 실행되는지 안되는지를 시험

  • 커버리지로 빈틈 없는지 주기적으로 조사하기 (실습)

coverage run --omit='*/venv/*','*/migrations/*','*/manager/*' manage.py test && coverage report

  • htmlcov 리포트 보기
profile
로앤굿 유쾌한 백엔드 개발자

0개의 댓글