본문 바로가기
파이썬/장고(django)

[Django] 장고를 통한 테스트 주도 개발(TDD)

by 책 읽는 개발자_테드 2022. 4. 6.
반응형

Do it! 장고+부트스트랩 파이썬 웹 개발의 정석를 읽고, 정리한 글입니다.

 

목록

· 테스트 주도 개발이란?

· 테스트 주도 개발 준비하기

· 포스트 목록 페이지 테스트하기

 


앞서 만든 장고 프로젝트를 기반으로 실습을 진행합니다.

 

테스트 주도 개발이란?


· 테스트 주도 개발이란 무언가를 개발할 때 바로 개발부터 하는 것이 아니라 개발하려는 항목에 대한 점검 사항을 테스트 코드로 만들고 그 테스트를 통과시키는 방식으로 개발을 진행하는 방법이다.

 

테스트 주도 개발을 적용하는 이유

· 개발한 코드가 테스트를 만족하는지 자동으로 확인하면서 개발을 진행하므로 매번 직접 테스트하느라 지치지 않고, 사고가 발생할 확률도 훨씬 줄어 든다. 

 

테스트 주도 개발 과정

테스트 코드 작성
- 만들고 싶은 기능을 점검할 코드 작성
- 아직 기능을 구현하지 않았으므로 테스트 결과는 실패한다.
기능 구현
- 테스트 코드를 만족시킬 수 있는 기능 구현
- 테스트 통과를 최우선으로 생각하고 작업
리팩토링 - 기능의 성능을 향상시키거나, 재사용성이 좋거나, 가독성이 코드로 개선
- 테스트 코드로 다시 기능 점검

 

테스트 주도 개발 준비하기


1. 터미널에 python manage.py test라고 입력한다. 0개의 테스트를 실행한 결과 OK가 나왔다.

 

2. blog/test.py를 수정한다.

from django.test import TestCase


class TestView(TestCase):
    def test_port_list(self):
        self.assertEqual(2, 3)

- TestCase 클래스를 상속받고, 'Test'로 시작하는 이름을 가진 클래스를 하나 정의한다.

- 장고 MTV 구조 중 뷰 측면에서 테스트하겠다는 의미로 TestView라는 이름으로 정의한다.

- TestCase를 상속 받은 클래스 내부에 'test'로 시작하는 함수를 정의한다. 장고에서는 실행할 테스트 함수를 해당 이름을 통해 찾는다.

 

테스트 내용은 2와 3이 같은지를 테스트하는 것이다.

 

3. 터미널에 다시 python manage.py test를 입력한다. 그러면, 2와 3은 다르기 때문에 다음과 같이 테스트가 실패한다.

 

4. blog/test.py에서 self.assertEqual(2, 2)로 수정한 다음 다시 테스트한다.

from django.test import TestCase


class TestView(TestCase):
    def test_port_list(self):
        self.assertEqual(2, 2)

    

테스트 결과 다음과 같이 OK 메세지가 나온다.

 

5. beautifulsoup4 라이브러리를 설치하기 위해 터미널에 다음과 같이 입력한다.

pip install beautifulsoup4

- 웹 개발에서는 개발자가 구현한 요소들이 웹 브라우저에 의도한 대로 잘 나타나는지 주로 테스트한다. 이때 HTML로 나타나는 페이지의 요소를 쉽게 다루기 위한 도구로 beautifulsoup4를 사용할 수 있다.

ex) 웹 브라우저의 타이틀을 무엇인지, 버튼에 써 있는 내용은 무엇인지

 

포스트  목록 페이지 테스트하기


· 테스트 코드 작성은 만들고자 하는 코드의 내용을 정리하는 것부터 시작하는 게 좋다. 

 

만들어야 하는 포스트 목록 페이지의 디자인이 다음과 같다고 가정한다.

- 맨 위에 내비 게이션 바가 있다.

- 그 아래 포스트를 목록 형태로 보여주는 공간이 있다.

- 포스트를 목록 형태로 보여주는 공간에는 여러 개의 포스트가 제목과 작성일 등이 정리되어 있다.

- 그 옆에는 검색 창과 카테고리를 보여주는 공간이 있다.

위에 구상한 내용을 바탕으로 구체화한 테스트 코드를 작성하자. 테스트하는 포스트 목록 페이지의 초기 html 파일은 다음과 같다.

 

post_list.html

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <title>Blog</title>
</head>
<body>
    <nav>
        <div class="container">
            <a href="./index.html">내비게이션바</a>
            <div>
                <ul>
                    <li>
                        <a href="./index.html">Home <span>(current)</span></a>
                    </li>
                    <li>
                        <a href="./blog_list.html">Blog</a>
                    </li>
                    <li>
                        <a href="./about_me.html">About Me</a>
                    </li>
                </ul>
                <ul>
                    <li>
                        <a href="#" data-toggle="modal" data-target="#loginModal">Log In</a>
                    </li>
                </ul>
            </div>
        </div>
    </nav>
<div>
    <h1>Blog</h1>

    {% for p in post_list %}
    <hr/>
    <h2><a href="{{ p.get_absolute_url }}">{{ p.title }}</a></h2>
    <h4>{{ p.created_at }}</h4>
    <p>{{ p.content }}</p>
    {% endfor %}
</div>
</body>
</html>

 

1. blog/tests.py를 수정한다.

from django.test import TestCase, Client

class TestView(TestCase):
    def setUp(self):
        self.client = Client()
        
    def test_post_list(self):
        # 1.1 포스트 목록 페이지를 가져온다.
        # 1.2 정상적으로 페이지가 로드된다.
        # 1.3 페이지 타이틀은 'Blog'이다.
        # 1.4 내비게이션 바가 있다.
        # 1.5 Blog, About Me라는 문구가 내비게이션 바에 있다.
        
        # 2.1 메인 영역에 게시물이 하나도 없다면, 
        # 2.2 '아직 게시물이 없습니다.'라는 문구가 보인다.

        # 3.1 게시물이 2개 있다면
        # 3.2 포스트 목록 페이지를 새로고침했을 때
        # 3.3 메인 영역에 포스트 2개의 타이틀이 존재한다.
        # 3.4 '아직 게시물이 없습니다'라는 문구는 더 이상 보이지 않는다.

- setUp() 함수에는 TestCase 내에서 기본적으로 설정되어야 하는 내용을 작성한다. 현재는 Client()를 통해 테스트를 위한 가상의 사용자를 사용하겠다는 내용만 담고있다.

 

2. 앞에서 작성한 시나리오를 점검할 수 있는 테스트 코드를 작성한다.

from django.test import TestCase, Client
from bs4 import BeautifulSoup
from .models import Post


class TestView(TestCase):
    def setUp(self):
        self.client = Client()

    def test_post_list(self):
        # 1.1 포스트 목록 페이지를 가져온다.
        response = self.client.get('/blog/')
        # 1.2 정상적으로 페이지가 로드된다.
        self.assertEquals(response.status_code, 200)
        # 1.3 페이지 타이틀은 'Blog'이다.
        soup = BeautifulSoup(response.content, 'html.parser')
        self.assertEquals(soup.title.text, 'Blog')
        # 1.4 내비게이션 바가 있다.
        navbar = soup.nav
        # 1.5 Blog, About Me라는 문구가 내비게이션 바에 있다.
        self.assertIn('Blog',  navbar.text)
        self.assertIn('About Me', navbar.text)

        # 2.1 메인 영역에 게시물이 하나도 없다면,
        self.assertEqual(Post.objects.count(), 0)
        # 2.2 '아직 게시물이 없습니다.'라는 문구가 보인다.
        main_area = soup.find('div', id='main-area')
        self.assertIn('아직 게시물이 없습니다.', main_area.text)

        # 3.1 포스트가 2개 있다면
        post_001 = Post.objects.create(
            title='첫 번째 포스트입니다.',
            content='Hello World.',
        )
        post_002 = Post.objects.create(
            title='두 번째 포스트입니다.',
            content='We are the World.',
        )
        self.assertEqual(Post.objects.count(), 2)

        # 3.2 포스트 목록 페이지를 새로고침했을 때
        response = self.client.get('/blog/')
        soup = BeautifulSoup(response.content, 'html.parser')
        self.assertEqual(response.status_code, 200)
        # 3.3 메인 영역에 포스트 2개의 타이틀이 존재한다.
        main_area = soup.find('div', id='main-area')
        self.assertIn(post_001.title, main_area.text)
        self.assertIn(post_002.title, main_area.text)
        # 3.4 '아직 게시물이 없습니다'라는 문구는 더 이상 보이지 않는다.
        self.assertNotIn('아직 게시물이 없습니다.', main_area.text)

- 1.1: self.client.get('/blog/')로 사용자가 웹 브라우저에 '127.0.0.1:8000/blog/'를 입력했다고 가정하고, 그때 열리는 웹 페이지 정보 response에 저장한다.

- 1.2: 웹 개발 분야에서는 서버에서 요청한 페이지를 찾을 수 없을 때 404 오류를, 성공적으로 결과를 돌려줄 때 200을 보내도록 약속되어 있다.

- 1.3: 불러온 페이지 내용은 HTML로 되어 있다. HTML 요소에 쉽게 접근하기 위해 먼저 Beautifulsoup으로 읽어들이고, html.parser 명령어로 파싱한 결과를 soup에 담는다. 그리고 self.assertEqual(soup.title.text, 'Blog')로 title 요소에서 텍스트만 가져와 그 텍스트가 Blog인지 확인한다.

- 1.4: soup.nav로 soup에 담긴 내용 중 nav 요소만 가져와 navbar에 저장한다.

- 1.5: navbar의 텍스트 중 Blog와 About me가 있는지 확인한다.

- 2.1: 작성된 포스트가 0개인지 확인한다. 테스트가 시작되면 테스트를 위한 새 데이터베이스가 임시로 만들어져 진행된다. 단 setUp() 함수에서 설정한 요소는 포함 시킨다.

- 2.2: id가 'main-area'인 div 요소를 찾아 main_area에 저장한다. 데이터베이스에 저장된 Post 레코드가 하나도 없으므로 메인 영역에 '아직 게시물이 없습니다.'라는 문구가 나타나는지 점검한다.

- 3.1: Post 레코드가 데이터베이스에 존재하는 상황을 테스트하기 위해 포스트를 2개 만든다. 이를 위해 Post.objects.create()를 사용한다.

- 3.2: 페이지를 새로고침하기 위해 1.1부터 1.3의 과정을 일부 반복한다.

- 3.3: 새로 만든 두 포스트의 타이틀이 id가 'main-area'인 요소에 있는지 확인한다.

- 3.4: 포스트가 생성되었으니 '아직 게시물이 없습니다.'라는 문구가 메인 영역에 더 이상 나타나지 않아야 한다.

- TestCase를 이용한 테스트방식은 실제 데이터베이스는 건드리지 않고, 테스트할 때 마다 가상의 데이터베이스를 새로 만들어 테스트한다.

 

3. 터미널에 python manage.py test를 입력하여 테스트한다.

 

다음과 같이 오류가 발생할 것이다.

 

원인: post_list.html 파일에 id가 main-area인 div 요소를 만들지 않았기 때문에 main_area = soup.find('div', id='main-area')를 실행할 때 main_area에 None이 저장되었다.

 

해결: 본문 div 요소에 id="main-area"를 추가한다.

...생략...
<div id="main-area">
    <h1>Blog</h1>

    {% for p in post_list %}
    <hr/>
    <h2><a href="{{ p.get_absolute_url }}">{{ p.title }}</a></h2>
    <h4>{{ p.created_at }}</h4>
    <p>{{ p.content }}</p>
    {% endfor %}
</div>
...생략...

 

다시 테스트를 진행한다. 여전히 오류가 발생한다.

 

원인: 게시물이 없을 때 '아직 게시물이 없습니다'라는 문구가 보이기 않기 때문이다.

해당 기능을 추가하자.

 

...생략...
{% if post_list.exists %}
    {% for p in post_list %}
    <hr/>
    <h2><a href="{{ p.get_absolute_url }}">{{ p.title }}</a></h2>
    <h4>{{ p.created_at }}</h4>
    <p>{{ p.content }}</p>
    {% endfor %}
{% else %}
    <h3>아직 게시물이 없습니다.</h3>
{% endif %}
...생략...

 

다시 테스트를 진행한다. 테스트가 통과하였다.

 

포스트 상세 페이지 테스트하기


반응형

댓글