🌈 프로그래밍/Django

테스트 자동화 하기

반응형
 
Refactoring
"Whenever you read [Refactoring], it’s time to read it again. And if you haven’t read it yet, please do before writing another line of code." ?David Heinemeier Hansson, Creator of Ruby on Rails, Founder & CTO at Basecamp Fully Revised and Updated-Includes New Refactorings and Code Examples “Any fool can write code that a computer can understand. Good programmers write code that humans can understand.” ?M. Fowler (1999) For more than twenty years, experienced programmers worldwide have relied on Martin Fowler’s Refactoring to improve the design of existing code and to enhance software maintainability, as well as to make existing code easier to understand. This eagerly awaited new edition has been fully updated to reflect crucial changes in the programming landscape. Refactoring, Second Edition, features an updated catalog of refactorings and includes JavaScript code examples, as well as new functional examples that demonstrate refactoring without classes. Like the original, this edition explains what refactoring is; why you should refactor; how to recognize code that needs refactoring; and how to actually do it successfully, no matter what language you use. Understand the process and general principles of refactoring Quickly apply useful refactorings to make a program easier to comprehend and change Recognize “bad smells” in code that signal opportunities to refactor Explore the refactorings, each with explanations, motivation, mechanics, and simple examples Build solid tests for your refactorings Recognize tradeoffs and obstacles to refactoring Includes free access to the canonical web edition, with even more refactoring resources. (See inside the book for details about how to access the web edition.)
저자
Fowler Martin
출판
Addison-Wesley Professional
출판일
2018.12.07

 

django에서는 기본적으로 제공해 주는 유닛 테스트 도구가 존재한다.

다음과 같은 예시가 있을 것 같다.

from django.test import TestCase
from .models import MyModel

class MyModelTestCase(TestCase):
    def setUp(self):
        MyModel.objects.create(name="Test", value=123)

    def test_my_model(self):
        """Tests that the value is set correctly"""
        test_instance = MyModel.objects.get(name="Test")
        self.assertEqual(test_instance.value, 123)

테스트 코드만 작성하는 것도 중요하지만, 어떤 환경에서 어떻게 돌릴 것인지도 매우 중요하다.

위의 예시 테스트 케이스를 실행할 수 있는 환경을 만들었다고 가정하고 커맨드 창에서 실행은 다음과 같이 할 수 있다.

python manage.py test

그러면 알아서 착착 작성된 테스트 코드를 설정한 환경에 맞게 실행해 준다.

하지만, 이 테스트 코드들을 개발할 때 수동으로 돌리기에는 너무 귀찮다. 이를 자동화하는 방법에 대해 소개하려고 한다.

 

크게 두 가지의 방법이 있을 것 같다고 생각했다.

1. CI로 자동화하기

이는 CI 툴을 활용해서 테스트 코드를 실행시키도록 하게 하는 방법이다.

예를 들어 흔히 알고 있는 Github Action을 활용한 방법이 있을 수 있다.

프로젝트 내에 특정 Action (push 또는 pull_requests)가 발생했을 때 실행하도록 하여 내가 추가한 기능 혹은 수정된 기능에 대해 문제가 없는지 기존 테스트 코드들을 실행함으로 큰 문제가 없는지 확인할 수 있다. (그렇지만 100% 테스트 코드를 신뢰하진 말자)

예시로  다음과 같이 작성할 수 있다.

name: Django CI

on: [push, pull_request]

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v2
    - name: Set up Python 3.8
      uses: actions/setup-python@v2
      with:
        python-version: 3.8
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install -r requirements.txt
    - name: Run tests
      run: |
        python manage.py test

위의 간단한 설정 파일을 살펴보면 특정 Action(Event)가 발생하면 실행하도록 하는 것이다.

steps 하위에는 각 단계별 name이 존재하고 그 name에는 run이라는 하위 depth가 존재하여 그 명칭에 맞는 동작을 수행하도록 명시해 주는 것이다.

가장 마지막에 보면 프로젝트 루트 경로 (manage.py)가 존재하는 곳에서 실행하던 익숙한 명령어가 보인다.

이를 대신 이 녀석이 입력하게 해주는 것이다.

 

2. Git Hooks으로 자동화하기

또 다른 방법으로는 Git hooks가 있다.

이는 보통 커밋 전 (pre-commit)에 실행하도록 할 수 있다.

추가하는 단계는 크게 어렵지 않다.

1. 프로젝트 루트 경로에 `. git/hooks/`디렉터리를 생성한다.

2. 생성된 디렉터리 내부에 `pre-commit` 파일을 생성해 준다. 여기에는 테스트 코드를 실행해 주는 명령이 포함될 것이다.

3. 해당 파일을 실행 가능하도록 권한을 부여해 준다. 왜냐면 직접 실행하는 게 아니라 `pre-commit`이라는 친구가 대신 실행해 줄 것이기 때문이다.

#!/bin/sh

echo "Running pre-commit tests..."
python manage.py test

if [ $? -ne 0 ]; then
  echo "Tests failed, aborting commit."
  exit 1
fi

간단하게 예시로 `pre-commit` hook 스크립트 예시이다.

실제로 나는 프로젝트에서 이 `pre-commit`을 활용하여 프로젝트 전반적인 포매팅과 정적 검사 도구(mypy)를 적용하여 사용 중에 있다.

따라서 나는 테스트 자동화를 적용하기 더 쉬운 방법인 2번 방법으로 쉽게 추가할 수 있었다. (진행 중인 프로젝트의 구조에 따라 유연하게 설정이 가능!)

현재 아래와 같이 진행하고 있다.

repos:
  - repo: local
    hooks:
      - id: isort
        name: isort
        entry: isort --profile black
        language: system
        types: [python]

      - id: black
        name: black
        entry: black .
        language: system
        types: [python]

      - id: mypy
        name: mypy
        entry: mypy --config-file=mypy.ini
        language: system
        types: [python]

총 3가지의 도구를 활용하고 있다.

따라서 나는 자동 테스트를 돌리기 위해서는 가장 마지막에 테스트 코드를 실행하도록 하는 단계만 추가하면 끝이다.

하지만 나는 테스트 코드를 동작할 때 특정 세팅 파일을 사용하고 있다.

# root/config/settings/test.py

from .base import *

MIGRATION_MODULES = {app: f"{app}.test_migrations" for app in APPS}

DATABASES["default"] = {
    "ENGINE": "django.db.backends.sqlite3",
    "NAME": ":memory:",
}
ENV_NAME = "test"

이 파일을 사용하는 이유는 다음과 같다.

1. 가장 첫 줄에는 내 프로젝트 내의 설정값 혹은 키 및 정의되어 있는 앱들의 설정값들을 가져오는데 의미가 있다.

2. 다음 줄에서는 나의 프로젝트에 존재하는 django app들의 models.py 파일들을 불러와서 마이그레이션을 하기 위한 목적에 있다.
즉, 테스트를 실행할 당시에는 정의된 나의 모델들이 존재하지 않기 때문에 어떤 모델들을 불러올 것인지 테스트 마이그레이션을 진행한다는 데 의미가 있다.

3. 다음 DATABASES 세팅을 통해 인메모리 SQLite DB를 사용하도록 명시했다. 즉, 테스트 데이터베이스는 메모리상에 존재하고 테스트가 끝나면 해당 테스트 DB는 휘발되도록 했다. 이렇게 하면 다음과 같은 이점이 있다.
- 속도 : 메모리 DB는 디스크 입출력까지 도달하지 않으므로 속도적인 측면에서 빠르다. 이를 통해 테스트를 하는데 시간을 줄일 수 있다.
- 격리 : DB는 메모리에 있기 때문에 생성 후 기존 데이터 값과는 무관하게 테스트를 실행할 수 있도록 보장해 준다. 이렇게 하지 않고 기존 개발용 DB에 테스트를 한다면 DB에 테스트를 위해 생성된 데이터가 존재할 수 있기 때문이다.
- 단순성 : DB 파일을 관리하거나 테스트 후 정리할 필요가 없다. 위에서 설명했듯이 test 명령을 실행한 이후에 휘발되기 때문이다.

4. 마지막으로 테스트 환경을 정의했다.
- ENV_NAME을 명시함으로 특정 기능에 대해 활성/비활성화할 수 있다.
예를 들어 다음과 같은 코드가 있을 수 있다.

    def _send_message(self, message: str) -> None:
        if settings.ENV_NAME == "prod":
            self.slack.chat.post_message(self.channel, message)
        elif settings.ENV_NAME == "dev":
            self.slack.chat.post_message(self.test_channel, message)
        return

이러한 함수가 있다고 했을 때 실행하는 환경마다 달리 동작하도록 분기처리에 사용할 수 있다.
따라서, test ENV에서는 위의 함수가 실행되지 않도록 할 수 있다.

 

위의 yaml 파일에서 추가로 아래와 같이 hook 단계를 추가해주었다.

      - id: django-test
        name: Django Test
        entry: >
          sh -c 'PYTHONUNBUFFERED=1 DJANGO_SETTINGS_MODULE=config.settings.test python manage.py test'
        language: system
        pass_filenames: false
        always_run: true

entry에 정의된 내용으로 테스트 환경에 사용할 설정 파일을 명시할 수 있었다.

 

다음은 실제 나의 `pre-commit`을 실행시킨 터미널을 확인해 보고 마무리하면 될 것 같다.

❯ git commit -m "add: django-test 후크 설정"                                 ─╯
isort....................................................................Passed
black....................................................................Passed
mypy.....................................................................Passed
Django Test..............................................................Passed

이런 식으로 commit을 찍을 때마다 선언했던 모든 git hooks들이 동작하는 모습을 볼 수 있다.
만약 이 중 하나라도 실패한다면, commit에 실패하게 되고, 어떤 부분 혹은 어떤 테스트가 실패했는지 바로 확인할 수 있다.
따라서, 테스트를 기능별로 쪼갠 후 세세하게 작성을 해 나아간다면, 기존 기능에 대한 수정 작업을 하거나 리펙터링 모자를 쓰고 리펙터링 작업을 할 때에 디버그 하는데 굉장히 유용할 것이다.

그러면 자연스레 생산성도 향상될 것이다. 그러므로 어려운 게 아니니, 실제 프로젝트에 적용해 보는 것도 나쁘지 않아 보인다. 더 나은 SW를 위해서!

반응형