unit test

백승진·2020년 12월 4일
0

Today I Learned

  1. unit test
  2. django 에서 unit test 하는 방법
    2-1. test를 위한 Data 생성/삭제
    2-2. function에 대한 unit test
    2-3. CBV(Class Based View)에서 url 별 동작에 대한 unit test

1. unit test

web service를 이용할 때 Error가 발생하거나 성능이 떨어져 원하는 바를 얻지 못하면 어떤가? 아마도 그 Service를 손절하고 다른 Service를 이용할 것이다.
해당 Service를 만든 회사는 원인을 분석하여 조치를 한다고 해도 한번 무너진 신뢰도를 회복하는 것은 쉽지 않다. 이런 문제를 겪지 않기 위해 service open 전에 제품에 대한 Test를 철저히 해야 한다.
Test는 크게 3가지가 있다.

  1. E2E(End to End) Test
  2. Integration Test
  3. Unit Test


위 그림은 3가지 Test의 형태를 보여주는 그림이다.

E2E Test 는 UI Test라고도 하는데 이는 web page 화면에서 사용자의 액션으로 발생하는 request를 backend가 처리하여 다시 web page에 반영되기까지의 전체 과정에 대한 Test로 시간과 금액적 resource를 가장 많이 소비하는 Test 이다. 전체 Test 과정 중 약 10%를 점유한다.
Integration Test 는 개발된 feature들이 연동해서 내보내는 출력을 Test하는 것을 말한다.
예를 들어 Backend에서 request를 받아 Database CRUD 진행 후 실제로 Database 에 Data가 들어갔는지 확인하는 과정을 들 수 있겠다. 전체 Test 과정 중 약 20%를 점유한다.

Unit Test 는 개발한 feature나 view에 대해 발생할 수 있는 상황(Success, Fail, exception)을 Test하는 과정으로 Feature간 의존성없이 독립적으로 할 수 있는 Test 이다.
전체 Test 과정 중 약 70%를 점유하며 Test 속도가 가장 빠르며 소모비용이 가장 적다.

2. django 에서 unit test 하는 방법

django project내에 app 생성시 app 안에 tests.py 파일이 자동으로 생성된다. tests.py는 unit test용 파일로 이 안에서 test 시나리오 별 함수를 작성하면 된다. 아래는 작업중인 SUWEE project중 book app에 대한 unit test code 이다.

# book/tests.py

from datetime import datetime

from django.test import TestCase

from user.models import (
        User,
        UserBook,
)
from .models import (
        Book,
        Category
)
from .views import (
        get_reading_numeric,
        get_searched_books,
        get_searched_books_with_title,
        get_searched_books_with_author,
        get_searched_books_with_company,
)

class BookTest(TestCase):
    def setUp(self):
        # 완독률/시간 수치 계산 test용 Data
        User.objects.create(id=1, nickname='test1')
        User.objects.create(id=2, nickname='test2')
        User.objects.create(id=3, nickname='test3')
        User.objects.create(id=4, nickname='test4')

        category = Category.objects.create(id=1, name='소설')

        Book.objects.create(id=1, title='문은 닫혔다', page=300, author="김문주", publication_date=datetime.now(), category_id=1, company='늘빛출판사')
        Book.objects.create(id=2, title='그렇게 개발자가 되어간다', page=500, author="고수희", publication_date=datetime.now(), category_id=1, company='(주)위고두다')
        Book.objects.create(id=3, title='광수 생각', page=250, author="김광수", publication_date=datetime.now(), category_id=1, company='(주)늘빛')
        Book.objects.create(id=4, title='결전! 주식투자 2020', page=600, author="마광수", publication_date=datetime.now(), category_id=1, company='(주)한빛IT')

        UserBook.objects.bulk_create([
            UserBook(user_id=1, book_id=1, page=129, time=130),
            UserBook(user_id=2, book_id=1, page=300, time=260),
            UserBook(user_id=3, book_id=1, page=159, time=30),
            UserBook(user_id=4, book_id=1, page=20, time=1130),
            UserBook(user_id=1, book_id=2, page=500, time=230),
            UserBook(user_id=1, book_id=3, page=250, time=90),
            UserBook(user_id=1, book_id=4, page=351, time=120),
        ])

    def tearDown(self):
        Book.objects.all().delete()
        Category.objects.all().delete()

    def test_get_numeric_reading_success(self):
        data = get_reading_numeric(Book.objects.get(id=1))

        self.assertEqual(data['avg_finish'], 25.0)                                          # 완독 확률
        self.assertEqual(data['expected_reading_minutes'], 260.0)                           # 완독 예상시간
        self.assertEqual(data['category_avg_finish'], 75.0)                                 # 해당 분야 완독 평균 확률
        self.assertEqual(data['category_expected_reading_minutes'], int((260+230+90)/3))    # 해당 분야 완독 평균 예상시간

    def test_get_numeric_reading_not_exist(self):
        data = get_reading_numeric(-1)

        self.assertEqual(data, {"message":"NOT_EXIST"})

    def test_get_searched_books_all(self):
        target = '주'
        datas  = get_searched_books(target)

        for data in datas:
            result = target in data['title'] or \
                        target in data['author'] or \
                        target in data['company']

            self.assertTrue(result)

    def test_get_searched_books_with_author(self):
        target = '고수희'
        datas  = get_searched_books_with_author(target)

        self.assertEqual(len(datas), 1)
        self.assertEqual(datas[0]['title'], '그렇게 개발자가 되어간다')

    def test_get_searched_books_with_title(self):
        target = '결전'
        datas  = get_searched_books_with_title(target)

        self.assertEqual(len(datas), 1)
        self.assertEqual(datas[0]['title'], '결전! 주식투자 2020')

    def test_get_searched_books_with_company(self):
        target = '빛'
        datas  = get_searched_books_with_company(target)

        self.assertEqual(len(datas), 3)
        self.assertEqual(datas[0]['company'], '늘빛출판사')
        self.assertEqual(datas[1]['company'], '(주)늘빛')
        self.assertEqual(datas[2]['company'], '(주)한빛IT')

tests.py에는 'from django.test import TestCase' 가 기본으로 기재되어 있는데 TestCase를 Based class로 하여 test용 class를 만들고 그 안에 test scenario별 method를 만들면 된다.
method 생성이 naming은 test 하고자 하는 기능과 예상결과를 모두 담는 것이 중요하며 접두어 부분에 'test_' 를 달아주어야 한다. 그래야 django tester가 이를 test 대상으로 인식한다.

2-1. test를 위한 Data 생성/삭제

BookTest class의 메소드 중 'setUp'을 보자. 이 함수는 Test를 위한 Data, object를 만드는 사전과정에 해당한다. 위 code를 보면 Test를 위해 User, Book, UserBook 데이터를 생성하고 있다.
두번째로 'tearDown'을 보자. 이는 테스트 종료시 실행되어야 하는 기능으로 생성한 Data나 object resource를 해제하는 과정으로 사용한다.

2-2. function에 대한 unit test

'tearDown' 밑으로 Test scenario 별 method가 정의된다. 각 함수별 이름을 보면 'test_'를 접두어로 두어 django test 기능이 대상으로 인식하도록 했으며 이후는 test 하고자 하는 기능과 예상결과에 대해 서술적으로 작성했다.

2-3. CBV(Class Based View)에서 url 별 동작에 대한 unit test

위의 code는 function에 대한 unit test들이 전부였다. 여기에 추가로 requester가 http request 시 이에 대한 처리 여부를 test하는 방법을 알아보자. 아래는 작업중인 SUWEE project 중 User app 관련 tests.py 이다.

# user/tests.py
from django.test import TestCase, Client

class UserTest(TestCase):
    def setUp(self):
        client = Client()

        password = bcrypt.hashpw('12345678'.encode('utf-8'), bcrypt.gensalt())
        User.objects.create(phone_number='01011112222', password = password.decode(), nickname='test_user')

    def tearDown(self):
        User.objects.all().delete()

    def test_user_signup_post_success(self):
        body = {
                'phone_number' : '01012347654',
                'password'     : 'SuweePW1?',
                'nickname'     : 'goblin',
                }

        response = self.client.post(
                        '/user/sign_up',
                        json.dumps(body),
                        content_type = 'application/json'
                    )
        self.assertEqual(response.status_code, 201)

    def test_user_signup_post_key_error(self):
        body = {
                'phone'    : '01012347654',
                'password' : 'SuweePW1?',
                }

        response = self.client.post(
                        '/user/sign_up',
                        json.dumps(body),
                        content_type = 'application/json'
                    )

        self.assertEqual(response.status_code, 400)
        self.assertEqual(response.json(), {'message':'KEY_ERROR'})

'Client' 객체를 만들면 이를 통해 가상으로 http request(get, post...)를 실행할 수 있다. 이를 통해 REST 통신시의 Test scenario를 구성하여 성공(1), 예외(0), 실패(-1) 에 대한 Test를 해 볼 수 있다.

profile
12년 .NET 개발 경력을 가진 웹 초짜 개발자입니다 :)

0개의 댓글