Django | Test

Jihun Kim·2022년 1월 6일
0

Django

목록 보기
6/8
post-thumbnail
post-custom-banner

왜 테스트 코드가 필요할까?

테스트 코드를 통해 세부적인 내용까지도 테스트 할수 있으며, 매번 정확히 같은 기능을 테스트할 수 있어 사람이 일일이 클릭하고 터치해 가며 확인하는 것보다 훨씬 더 신뢰성이 높다. 또한, 자동화 테스트는 빠르기 때문에 좀 더 정기적으로 실행할 수 있고, 테스트 실패시 코드가 기대대로 동작하지 않았던 부분을 정확히 지목할 수 있다.

테스트 케이스 작성시 어려운 부분이 “의존성”이다. 이 의존성 문제를 해결하기 위해 Mock을 사용할 수 있는데 여러 이유가 있을 수 있겠지만, 1) 특정 모듈을 갖고 있지 않아서 테스트 환경 구축이 어려운 경우, 2) 테스트가 특정 경우나 순간에 의존적일 때, 3) 테스트 시간이 오래 걸리는 경우 등의 케이스에서 사용할 수 있다.



테스트 구조

장고는 테스트가 진행되기 전, 현재 작업 디렉토리에 있는 모든 test_로 시작하는 파일들을 확인한다. 이 때, 폼 테스트면 test_forms.py, 뷰 테스트면 test_views.py라고 이름 짓는 것이 좋다.

Django에서 제공하는 테스트는 다음과 같이 TestCase를 임포트 하여사용한다.

from django.test import TestCase

이 TestCase는 파이썬 표준 라이브러리인 unittest로 만들어진 클래스로, 유닛 테스트 + 통합 테스트 모두에 적당하다고 한다. 각 TestCase는 assert 메소드를 통해 두 값이 동등한지 또는 결과가 True/False인 지를 체크하며 test_로 시작하는 이름의 메소드를 실행한다.

장고의 TestCase는 tearDown 메소드를 이미 가지고 있다. 따라서 이 메소드를 굳이 사용할 필요가 없다.

아래와 같은 방법으로 테스트케이스 클래스를 만들 수 있다.

class YourTestClass(TestCase):
    @classmethod
    def setUpTestData(cls):
        print("setUpTestData: Run once to set up non-modified data for all class methods.")
        pass

    def setUp(self):
        print("setUp: Run once for every test method to setup clean data.")
        pass

    def test_false_is_false(self):
        print("Method: test_false_is_false.")
        self.assertFalse(False)

그렇다면 setUpTestDatasetUp의 차이점은 뭘까?

  • setUp: 각 테스트 메소드가 실행될 때마다 가장 먼저 실행된다. 여기에는 테스트 중에 내용이 변경될 수 있는 객체를 생성한다. → 매번 테스트 메소드에서 객체를 생성하기 때문에 속도가 비교적 느린 편이다.
  • setUpTestData: 클래스 전체에서 사용되는 설정을 위해 테스트 시작시 딱 한 번만 실행된다. → “read_only”로 사용되는 객체는 이 메소드를 사용하는 것이 좋다. 왜냐하면, 테스트시 1번만 생성되기 때문에 setUp 메소드에 비해 비교적 속도가 빨라진다. 그렇기 때문에 빠르게 테스트를 실행할 수 있다.

⇒ 따라서, setUp 보다는 setUpTestData를 사용하는 것이 권장된다.



Views

뷰 동작을 테스트할 때는 Client를 사용한다. 장고 테스트에서 제공하는 Client가 있는데, restframework를 사용할 경우 여기서 제공하는 APIClient를 사용할 수 있다. 둘이 똑같이 동작하며 이는 더미 웹 브라우저와 같은 역할을 한다.

사용 방법은 다음과 같다.

from django.test import TestCase

from catalog.models import Author

class AuthorListViewTest(TestCase):
    @classmethod
    def setUpTestData(cls):
        # Create 13 authors for pagination tests
        number_of_authors = 13

        for author_id in range(number_of_authors):
            Author.objects.create(
                first_name=f'Christian {author_id}',
                last_name=f'Surname {author_id}',
            )

    def test_view_url_exists_at_desired_location(self):
        response = self.client.get('/catalog/authors/')
        self.assertEqual(response.status_code, 200)

만약 APIClient를 사용한다면 restframework에서 임포트 해서 사용해야 하며, setUp시 미리 APIClient를 정의해서 사용하는 것이 편할 것이다.



Mocking

테스트 작성시 ‘외부에 의존하는 부분’에 대해 가짜로 대체하는 기법을 Mocking이라 한다. 따라서, 외부 서비스를 사용해야 하는 경우가 생길 때 test시 Mocking을 사용하면 된다.

mocking을 사용할 때는 파이썬 내장 라이브러리인 unittest.mock을 사용한다.

아래와 같이 Mock의 확장 버전인 MagicMock도 있다.

from unittest.mock import Mock, MagicMock

Mock

mock 객체를 생성한 후 mock이 호출되었을 때 특정 값을 리턴하려면 return_value를 사용한다.

옵션을 주어 mock 객체를 생성하거나, mock 객체 생성 이후에 변경하는 방법이 있다.

from unittest.mock import Mock

mock = Mock(return_value="I'm a mock")
mock()
>>> "I'm a mock"

mock.return_value = 1
mock()
>>> 1

이밖에도 여러 옵션들을 위와 같은 방법으로 설정해 줄 수 있다.

  • side_effect: exception 또는 리스트, 람다 함수와 같은 여러 설정들을 할 수 있다.
    • exception을 넘기면 mock 객체호출시 에러가 뜬다.
    • side_effect에 리스트를 넘기면 mock 객체를 호출할 때마다 리스트 안의 원소들을 하나씩 리턴할 수 있다.

MagicMock

MagicMock은 Mock의 subclass이다. 파이썬 공식 문서를 보면 “매직 메소드”를 기본적으로 구현해 놓았다고 한다. 조금 더 다양한 메소드들을 가지고 있는 것 같다.

가령, 특정 형의 객체를 반환하도록 할 경우 반환 값에 관심이 없어서 아무런 조치를 하지 않더라도 사용할 수 있다. 아래 예시와 같이 int 혹은 list로 형을 변환하는 것이 가능하다.

mock = MagicMock()
int(mock)
>>> 1
len(mock)
>>> 0
list(mock)
>>> []
object() in mock
>>> False

간단하게 사용하려면 Mock을, 여러 메소드를 추가해서 사용하려면 MagicMock을 사용하면 될 것 같다.



Patch 데코레이터

patch 데코레이터는 지정한 모듈을 임시로 mocking해서 사용할 수 있도록 만들어 준다. 이 때 patch는 MagicMock을 한다. 또한 특정 범위 내에서만 mocking이 가능하도록 만들어 준다. 그러면 이를 통해서 임시적으로 원하는 모듈이 내가 지정한 값을 리턴하도록 만들어 줄 수 있다. 그러면 테스트가 조금 더 간편해진다.

아래의 예시를 생각해 볼 수 있다.

<greetings.py>

def hello_world():
	return "Hello World!"

<test_hello.py>

from unittest import TestCase
from unittest.mock import patch

class GreetingTest(TestCase):
	@patch("greetings.hello_world", return_value="안뇽 세상!")
	def test_hello(self, mock_hello_world):
		self.assertEqual(hello_world(), "안뇽 세상!")
		selef.assertIs(hello_world, mock_hello_world)		
  • 위의 테스트를 실행해 보면 두 케이스 모두 통과한다.
    • 첫 번째 케이스의 경우 @patch 데코레이터로 이미 hello_world 함수의 리턴 값을 “안뇽 세상!”으로 변경했기 때문에 원래의 “Hello World!”가 아니라 “안뇽 세상!”이 리턴 된다.
    • @patch 데코레이터를 통해 hello_world 함수는 mock_hello_world로 대체 되었다.
  • 테스트 클래스의 메소드에 인자로 “mock_”으로 시작하는 patch된 모듈의 함수명을 전달한다. 따라서, 여기서는 함수명이 hello_world니까 mock_hello_world가 된다.

@patch 사용해 보기

외부 api를 호출해야 하는 테스트의 경우, 실제로 네트워크를 연동해 selenium으로 확인해 보는 방법도 있겠지만 느리고 불편하다. 이 경우에 좋은 방법이 patch 데코레이터를 이용하는 것이다.

따라서, requests.get()을 통해 네트워크를 통해 api를 호출해야 했던 것을 mock 객체로 만들고 리턴 값을 원하는 값으로 고정해서 테스트를 해보면 된다.

아래의 예시에서 게시판의 작성자와 제목을 가져오는지 테스트 해본다.

from unittest import TestCase
from unittest.mock import patch
import post_manager

class PostExsitionConfirmTest(TestCase):
	@patch("requests.get")
	def test_post_existion(self, mock_get):
		mock_get.return_value.status_code = 200
		mock_get.return_value.json.return_value = {
			"author": "Jihun",
			"title": "Test succeed!"
		}
		post = post_manager.get_post()
		
		self.assertEqual(post["author"], "Jihun")
		self.assertEqual(post["title"], "Test succeed!")


참고

1) mozilla

https://developer.mozilla.org/ko/docs/Learn/Server-side/Django/Testing

2) 공식문서

https://docs.djangoproject.com/en/4.0/topics/testing/tools/

3) 블로그

https://www.daleseo.com/python-unittest-mock/
https://8percent.github.io/2017-05-31/test-guide/
https://eminentstar.github.io/2017/07/24/about-mock-test.html

profile
쿄쿄
post-custom-banner

1개의 댓글

comment-user-thumbnail
2022년 1월 7일

잘 보겠습니다

답글 달기