본문 바로가기
IT/Python

Django, 테스트코드 작성법

by Cyber_ 2024. 4. 7.

1. 테스트코드란 무엇인가?

  • 테스트 코드는 소프트웨어의 기능과 동작틀 테스트하는 데 사용되는 코드
  • 소프트웨어의 결함을 찾아내고 수정하는 과정에서 매우 중요
  • V모델의 따라 테스트 단위테스트, 통합테스트, 시스템 테스트, 인수 테스트가 있다.

단위 테스트(Unit Testing): 소프트웨어의 가장 작은 실행 단위인 '단위'를 검증하는 테스트
통합테스트(Integration Testing): 복수의 단위가 서로 올바르게 작동하는지 확인하는 테스트
시스템 테스트(System Testing): 소프트웨어 시스템이 명세서에 기술된 요구사항을 충족하는지 확인하는 종합적인 테스트
인수 테스트(Acceptance Testing): 실제 사용 환경에서 소프트웨어가 사용자의 요구사항을 만족하는지 확인하는 테스트

  • 테스트 코드의 작성은 인수테스트와 통합테스트에서 주로 이루어진다.

2. 테스트를 왜 해야 하는가?

  • 내가 무엇을 만들고 있는지 정확히 인지

테스트 코드 작성을 통해 요구사항의 기능적인 항목들을 저리하고 코너 케이스를 찾게 되며, 이는 문서의 역할을 수행

  • 리팩토링을 진행할 때 부담 줄여주기

테스트 코드가 있다면 코드 수정 후에도 기능이 정상적으로 작동하는지 검증할 수 있다.

  • 결합도와 의존성이 낮은 코드를 지향

테스트 코드 작성을 통해 의존성이 높은 부분을 개선하면 프로젝트의 코드 품질이 향상됨.

3. 테스트의 장단점

1) 장점

  • 코드 품질 향상, 회귀 테스트, 문서화, 리팩토링 지원

2) 단점

  • 개발 시간 증가, 불완전한 테스트, 오버 엔지니어링, 유지 보수 비용, 학습 곡선

단점을 생각하면 결국 개발의 비용문제다.
테스트를 잘 하는 사람이 된다면 단점을 상쇄하고 코드의 품질은 개선되며 장기적인 관점에서 서비스를 오래 지속 되게 하는 데 도움이 될 수 있다.
추가적으로 완벽한 테스트는 없다. 늘 변수는 있고 생각지도 못한 부분이 나온다. 오히려 좋다. 결국 개선점을 빨리 찾는 것이다.

3. Django에서의 단위테스트(Unit Tests)와

Django에서 테스트는 일반적으로 애플리케이션의 tests.py 파일에서 진행된다.
테스트 명령어:

python manage.py test appname
  • 여기서 다룰 단위 테스트는 모델 테스트, 뷰 테스트, 폼 테스트, Api 호출 테스트이다.
  • Django에서는 기본적으로 test를 위해 django.test라이브러리를 지원한다.

1) 모델 테스트 예시

import datetime

from django.test import TestCase
from django.utils import timezone

from .models import Question


class QuestionModelTests(TestCase):
    def test_was_published_recently_with_future_question(self):
        time = timezone.now() + datetime.timedelta(days=30)
        future_question = Question(pub_date=time)
        self.assertIs(future_question.was_published_recently(), False)
  • 테스트 명령어를 입력하면
    • polls 앱에서 테스트를 찾음
    • django.test.TestCase클래스의 서브 클래스를 찾음
    • 테스트 목적으로 특별한 테스트 데이터 생성
    • 이름이 test로 시작하는 것들을 찾음(test의 이름은 "test"로 시작해야 함)
    • .was_published_recentl()를 통해 테스트용 인스턴스 생성
    • assertIs() 메서드를 사용하여 .was_published_recently()가 Flase가 나오길 바란다고 함.
    • 테스트 결과가 True라면 어떤 테스트가 실패했는지와 실패가 발생한 행을 알려줌

2) 뷰 테스트 예시

def create_question(question_text, days):
	time = timezone.now() + datetime.timedelta(days=days)
    return Question.objects.create(question_text=question_text, pub_date=time)

class QuestionIndexViewTests(TestCase):
    def test_no_questions(self):
        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"], [])

    def test_past_question(self):
        
        question = create_question(question_text="Past question.", days=-30)
        response = self.client.get(reverse("polls:index"))
        self.assertQuerySetEqual(
            response.context["latest_question_list"],
            [question],
        )
        
    ...
  • 질문 생성 함수인 create_question은 테스트 과정 중 설문을 생성하는 부분에서 반복 사용
  • self.client는 Django의 테스트 클라이언트 객체
  • assertEqual: 두 값을 비교하여, 같은 경우에만 테스트를 통과하도록함
  • assertContains: 응답 본문에 특정 텍스트가 포함되어 있는지 확인
  • assertQuerySetEqual:: 주어진 리스트와 동일한지 비교

3) 폼테스트

from django import forms
from .forms import MyForm

class MyFormTest(TestCase):
    def test_valid_form(self):
        form_data = {'field1': 'value1', 'field2': 'value2'}
        form = MyForm(data=form_data)
        self.assertTrue(form.is_valid())

    def test_invalid_form(self):
        form_data = {'field1': '', 'field2': 'value2'}
        form = MyForm(data=form_data)
        self.assertFalse(form.is_valid())

4) mock 객체를 활요한 외부 api 호출 테스트

from django.test import TestCase
from unittest.mock import patch
from .views import my_view

class MyViewTest(TestCase):

    @patch('myapp.views.get_external_data')
    def test_my_view(self, mock_get_external_data):
        # 목 객체에 반환 값을 설정
        mock_get_external_data.return_value = {'key': 'value'}

        # 뷰를 호출
        response = self.client.get('/my-view-url/')

        # 반환값 검증
        self.assertEqual(response.status_code, 200)
        self.assertIn('key', response.context)
        self.assertEqual(response.context['key'], 'value')

4. 통합테스트를 위한 Selenium

1) Seleninm을 사용하는 이유

  • 웹 브라우저에서의 실제 사용자 상호작용 모방
  • 자동화된 브라우저 테스트
  • 다양한 브라우저와 환경 지원
  • 프론트엔드와 백엔드 통합
  • 비동기적 동작의 테스트
  • 디버깅 용이성

2) 통합 테스트 예시

from django.test import TestCase
from unittest.mock import patch
from myapp.utils import fetch_data_from_service

class FetchDataServiceTest(TestCase):
    
    @patch('myapp.utils.requests.get')
    def test_fetch_data(self, mock_get):
        # Mock 객체에 대한 반환 값 설정
        mock_get.return_value.json.return_value = {
            'key': 'value'
        }

        # 함수 호출
        result = fetch_data_from_service('https://example.com/data')

        # 반환된 데이터 검증
        self.assertEqual(result, {'key': 'value'})

        # Mock 객체가 적절한 URL로 호출되었는지 확인
        mock_get.assert_called_with('https://example.com/data')

5. 추가적으로 고려해볼만한 테스트 도구

  • pytest와 pytest-django: Python에서 널리 사용되는 강력한 테스트 도구
  • DRF: DRF(Django REST framework)는 테스트 도구가 따로 있다.
  • Factory Boy와 Faker: 테스트 데이터를 생성하는데 유용함
  • Mocking(unittest.mock 또는 response): 외부 API 호출이나 Django 외부의 다른 서비스에 대한 의존성을 가진경오 목(Mock)을 사용할 수 있습니다.
  • Postman 또는 Insomniz: 자동화된 API 테스트 스위트를 구축하는 기능도 제공함
  • HTTPPie: 커맨드 라인에서 사용할 수 있는 HTTP 클라이언트 툴로, API의 간단한 테스트와 디버깅에 유용
  • celery를 사용할경우: celery는 자체 테스트 유틸리티를 제공

6. 시스템 테스트와 인수 테스트는 어떻게?

  • 시스템 테스트는 전체적인 기능을 테스트하며, django.test.TestCase를 사용하여 시스템 전반에 걸친 테스트를 수행할 수 있다.
  • 인수테스트에는 종종 Selenium을 사용하여 요구사항을 만족하는지 검증

7. 결론

Django에서 테스트 하기 위해 사용한 예시들은 정답이 아니다.
우선 요구사항을 빠르게 파악해 어떤 기능을 만들어야 하는지
그 기능을 수행하기 위해서 어떤 데이터들이 필요한지
그 기능을 구현하기 위해서 어떤 모듈이 필요한지
등등을 우선적으로 결정을하고
이에 맞춰 필요한 라이브러리, 기술들을 활용하여 테스트 코드를 작성해 나가는 것이 순서이다.

Referenece