Software testing : Unittest in Django

2022. 5. 12. 11:32
  • Software testing
  • Manutal Testing vs Automation Testing
  • system test 전략 3가지
  • 유닛테스트(장고)

1. software testing

  • 배포전에 개발한 상품을 처음부터 끝까지 테스팅하는 것
  • 결함을 아주 직관적으로 + 상세히 확인하고, 에러를 사전에 방지할 수 있다.
  • 그 외에도, 시간 절약 / 코드의 효율적인 개선 / 품질개선
  • 실제로 테스팅을 위한 코드작성시간이 전체작업중에 40% 이상을 차지한다.

 

manual testing  VS automating testing

manual testing

  • 사람이 직접 손으로 이것저것 해보는 테스트
  • 불안정성, 비용, 인력소모가 증가 & 테스트 속도 감소

 

Automation Testing

  • testing 자체를 
  • 반복성, 재사용성, 안정성 증가 & 비용 및 인력소모 감소

 

System testing 전략 3가지

End-to-End(E2E) Test

  • 프론트와 백엔드가 각각 자신의 페이지와 서버를 띄워놓고 통신을 하며 테스트를 진행하는 것
  • 전체적인 flow를 테스트
  • 제품출시의 마지막 단계에서 이루어지는 테스트
  • Cypress 등을 사용

 

Integration Tests(통합테스트)

  • 전체적인 기능을 연계하여 통합적으로 테스트
  • 한쪽의 모든 것을 다 만들어 놓고, 한쪽 전부를 테스트한다고 생각하면 편함
  • 그래서 다 만들어놓고, Postman 등을 활용하여 진행
  • 모듈간의 호환성을 검증

 

Unit Tests(단위 테스트)

  • 각각의 개별적인 기능을 독립적으로 테스트
  • 테스트 할 수 있는 가장 작은 단위를 테스트하는 코드를 작성해서 테스트 하는 것(;;)
  • 유닛테스트를 최대한 빡세게 만들어서, integration test / E2E test에 소모되는 시간과 노력을 최소화 하는 것이 좋다.
    • 가장 많은 버그가 발견되고, 또 많은 경우의 수를 고려해야 한다
    • 따라서 가장 복잡도가 높고 또 그렇기에 비중이 높다. 
  • 잘 만든 유닛 테스트는 코드에 대한 문서(설명서)로도 확인할 수 있다!

 

  • 특히 파이썬이나 파이썬 기반의 프레임워크를 다루는 실력이 올라가면서 객체지향적으로 기능을 구현하게 된다.
    • 이 과정에서 각 클래스의 함수들은 기능별로 세세히 분해되어, 다른 기능들과 결합하게 됨.
    • 다르게 말하면, unittest의 테스트 단위가 되는 메서드들은 다른 메서드들(기능들)과 조합되어 작동된다.
    • 심지어 현업에서의 코드가 굉장히 길어진 다는 것을 감안하면, 특정 메서드가 잘 돌아가는지 모든 경우에 대하여 postman같은걸로 돌려보는 것은 굉장히 피곤하고 어려운 일임(확인해야 되는 메서드가 100개 200개를 넘어 1000개가 된다고 생각해보자)
    • 따라서 기능을 설계할 때부터 각 메서드별 발생할 수 있는 모든 경우의 수에 대해 유닛테스트를 설계함으로써, 문제가 발생했을 때 어떤 메서드가 어떤 경우에 문제가 발생하는지를 즉각적으로 확인할 수 있는 게 유지보수의 관점에서도 훨씬 편리함

2. 장고에서의 유닛테스트 흐름

  • 장고에서 유닛테스트를 진행 흐름
  • 각 클래스의 모든 메서드들이, 발생시킬 수 있는 모든 경우의 수에 대하여 적절한 응답을 반환하는지를 확인한다.

 

Fixture : 테스트 준비단계

  • 테스트 시나리오에 따라 '사전 준비작업 & 테스트 종료 후 테스트를 위해 만든 리소스의 정리'를 위한 기능을 Fixture라 함
  • 기본적으로 모델을 반영하여 만들어진 & 텅빈 테스트용 DB가 하나 생성된다.
    • 장고에서는 유닛테스트시 DB가 자동으로 생성되고, 테스트가 종료되면 DB역시 자동으로 삭제된다.
    • 장고가 자동으로 해주는 거지, 다른 프레임워크를 사용할 경우 테스트용 DB의 생성과 삭제도 실행시켜야 함(장고짱!)
    • 추가)이때 DB는 전체테스트 기간동안 단 한번만 생성된다. 따라서 동일한 테이블에 특정객체를 생성할 때, id값을 입력해주지 않으면 의도와 다른 값이 생길 수 있다. 
      • ex)A테스트 > 유저테이블에서 유저생성 > 삭제 > B테스트 유저테이블에서 유저 생성시 id를 설정해주지 않으면 자연스럽게 id = 2가 되어버림.(삭제되긴 했지만 id=1인 유저가 있었으므로)
      • 따라서 모든 테스트마다 객체의 id를 설정해주는 것이 좋음
  • 상품의 리스트를 반환하는 것처럼 해당 DB에 데이터가 들어있어야 할 경우, setUp메서드를 사용해 데이터를 생성해준다
    • 테스트 속도는 최대한 빨라야 한다. 따라서 여러개의 데이터를 사용할 경우 bulk_create을 사용하여 생성해준다.
    • 필요에 따라 Foreignkey로 맺어져 있는 다른 테이블의 데이터도 생성해야 할 수도 있다 ex)영화테이블 > foreignkey로 물려있는 스틸컷 테이블도 함께 생성해줘야 함)
    • 물론 카카오 소셜 로그인과 같이, DB에 데이터가 들어갈 필요조차 없을 때도 있다. 이럴 경우에는 생성해주지 않아도 된다
    • TEST가 끝나면 생성된 데이터는 삭제되어야 하므로, teardown메서드를 활용해 데이터를 삭제한다.
  • 장고테스트에서 일련의 과정은  setUp(사전준비) > 테스트 진행 > teardown(리소스정리)의 순서로 메서드들이 자동호출되어 진행된다. 
    • 하나의 클래스 내에 테스트가 여러개일경우 setup > 테스트A > teardown > setup > 테스트B > teardown..과 같은 식으로 진행

 

Test : 테스트 단계

  • 유닛테스트의 핵심은 결국, 어떤  url로 특정 결과를 얻기 위한 값들을 함께 요청을 보냈을 때 실제로 그 값이 나오는지를 확인하는 것
  • Client라는 함수를 사용하여 특정 url로 요청을 보내고, 요청시에 필요한 데이터 및 속성값을 함께 보내준다
  • DB에 setup함수를 통해 발생한 데이터만 있다고 가정할 때, 특정 url을 통해 호출된 view의 반환값이, 마지막에 assertEquald을 사용하여 비교해주는 목표값과 일치하는지를 확인하는 구조
  • 각 테스트들의 이름은, 로직에서 이름을 짤때와 다르게 최대한 상세하게 써주는 것이 좋다. 왜냐하면 테스트가 실패했을 경우에만 해당 메서드(테스트)의 이름이 나오는데, 그때 어떤 부분에서 문제가 발생했는지 한 눈에 바로 알 수 있도록 하기 위하여

 


3.장고 유닛테스트 예시

유닛테스트 예시 1

  • setUp메서드에서 bulk_create를 사용해서 생성함으로써, 객체들의 생성속도를 촉진시켜 테스트속도를 더 빠르게 함
  • image테이블이 영화리스트를 참조하고 있으므로, 이미지 객체들을 영화객체가 생성된 후에 생성
  • DB에 setup메서드를 통해 생성된 데이터만 존재한다고 가정할 때, Client메서드를 통해 호출된 뷰가 반환할 값을 response로 받은 후, assertEqual를 사용하여 목표값과 비교
    • 반환값이 view에서 jsonresponse >> .json()으로 값을 풀어줌
    • status code를 비교
from django.test import TestCase, Client
from .models     import Movie,MovieImage

class MovieListDetailTest(TestCase):
    def setUp(self):

        test_movies_list = [
            Movie(
            id             = 1,
            name           = 'test_name1',
            eng_name       = 'test_eng1',
            description    = 'des1',
            detail_text    = 'detail1',
            age_grade      = 'test@test.com',
            is_subtitle    = True,
            screening_type = 2,
            preview_url    = 'test@preview.com',
            running_time   = 120,
            ),
            Movie(
            id             = 2,
            name           = 'test_name2',
            eng_name       = 'test_eng2',
            description    = 'des2',
            detail_text    = 'detail2',
            age_grade      = 'test@test.com',
            is_subtitle    = True,
            screening_type = 2,
            preview_url    = 'test@preview.com',
            running_time   = 220,
            ),
        ]

        test_images_list = [
            MovieImage(
            movie_id=1,
            stillcut_url='test_img1'
            ),
            MovieImage(
            movie_id=2,
            stillcut_url='test_img2'
            ),
        ]

        Movie.objects.bulk_create(test_movies_list)
        MovieImage.objects.bulk_create(test_images_list)

    def tearDown(self):
        Movie.objects.all().delete()
    
    def test_success_get_product_list(self):
        client = Client()
        response = client.get('/movies')

        self.assertEqual(response.status_code,200)
        self.assertEqual(response.json(),{
            "result":[
                {
                    'id'           : 1,
                    'name'         : 'test_name1',
                    'stillcut_url' : 'test_img1'
                },
                {
                    'id'           : 2,
                    'name'         : 'test_name2',
                    'stillcut_url' : 'test_img2'
                }
            ],
        })

 

유닛테스트 예시 2

  • review를 생성하는 뷰에 관한 테스트.
  • post메서드를 기반으로 함 > client를 활용하여 필요한 값들 / 속성값을 함께 넘겨준다
    • body데이터 :  json형태로 보내줘야 하므로,  보내줄 데이터들을 딕셔너리 형태로 만들어준 후, 파이썬에서 딕셔너리 자료형을 json으로 정형화 시켜주는 json.dumps()를 활용해 json형태로 데이터를 보내준다
    • 그외에 content_type에 대한 값을 넣어주고
    • 헤더 형태로 jwt토큰을 값을 보내준다. 이때 jwt토큰을 보내주면서 dictionary kwarg를 사용함
  • Client의 요청결과 반환되는 값과, 내가 원래 의도한 목표값이 맞는지를 확인
  • 맨 아래 test_fail메서드의 경우, 의도적으로 body에 들어가는 데이터의 키값을 바꾸어 key_error를 유발시키도록 하고, 실제로 key_error가 발생했는지 확인하는 과정
from django.test       import TestCase, Client, TransactionTestCase
from datetime          import datetime,timedelta
from TerraBox.settings import *
from reviews.models    import MovieReview
from movies.models     import Movie
from users.models      import User
import jwt, json

class ReviewViewTest(TestCase):
    def setUp(self):
        Movie.objects.create(
            id             = 1,
            name           = 'test_name1',
            eng_name       = 'test_eng1',
            description    = 'des1',
            detail_text    = 'detail1',
            age_grade      = 'test@test.com',
            is_subtitle    = True,
            screening_type = 2,
            preview_url    = 'test@previewb.com',
            running_time   = 120,
            )
        
        User.objects.create(
            id =1,
            kakao_id=123456,
            nickname = 'tester',
            email = 'test@user.com',
            profile_image_url = 'http://test.com',
        )

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

    def test_success_reviewview_post_create_new_review(self):
        client = Client()
        
        expiration = timedelta(seconds=3600)
        token_expiration_time = datetime.utcnow() + expiration
        jwt_access_token = jwt.encode({'id':User.objects.last().id,'exp':token_expiration_time},SECRET_KEY,algorithm=ALGORITHM)
        
        content = {
            'content' : 'test_content'
        }
        
        headers = {'HTTP_Authorization':jwt_access_token}
        response = client.post(
            '/movies/1/reviews',#요청 url. 맨앞에 / 꼭 붙여줘야 한다!
            json.dumps(content),#content를 json형태로 보내주기 위해 json.dumps로 해줘야 함
            content_type='application/json',#json형태로 보내주므로 content_type을 json으로 선택
            **headers,#토큰을 딕셔너리 키워그 방식으로 전달
        )
        
        self.assertEqual(response.status_code,201)#코드를 비교
        self.assertEqual(response.json(),{#메세지를 비교
            "message": "created!"
        })
        
    def test_fail_reviewview_post_create_key_error(self):
        client = Client()
        
        content = {
            'error_key' : 'test_content'
        }
        expiration = timedelta(seconds=3600)
        token_expiration_time = datetime.utcnow() + expiration
        jwt_access_token = jwt.encode({'id':User.objects.last().id,'exp':token_expiration_time},SECRET_KEY,algorithm=ALGORITHM)
        
        headers = {'HTTP_Authorization':jwt_access_token}
        response = client.post(
            '/movies/1/reviews',#요청 url. 맨앞에 / 꼭 붙여줘야 한다!
            json.dumps(content),#content를 json형태로 보내주기 위해 json.dumps로 해줘야 함
            content_type='application/json',#json형태로 보내주므로 content_type을 json으로 선택
            **headers,#토큰을 딕셔너리 키워그 방식으로 전달
        )

        self.assertEqual(response.status_code,400)#코드를 비교
        self.assertEqual(response.json(),{'message:':'key_error!'})

 

유닛테스트 예시 3

  • 카카오 소셜로그인에 관한 유닛테스트
  • 카카오 소셜 로그인은 카카오서버와의 API통신이 필수 불가결. 그러나, 유닛 테스트는 다른 서버등에 의존하지 않고 진행되어야 하는 테스트이므로, 해당 테스트로부터 받을 데이터를 임의로 설정해줘야 하며, 그것을 위하여 patch와 MagicMock을 사용한다.
    • 실행하려는 테스트의 위에 patch를 데코레이터로 주면, patch데코레이터의 인자로 온 값을 해당 테스트의 두번째 인자로 온 값으로 바꿔준다.
      • 아래 test_success..._user 테스트에서는 patch의 인자로 users.views.requests를 줬고, 해당 테스트는 두번째 인자로 mocked_requests를 받는다
      • 즉 해당 테스트를 진행하는 동안 users.views에 등장하는 requests는 아래에서 설정할 mocked_requests가 되는 것
      • 그리고 mocked_requests.get의 값으로 MagicMock을 이용하여 MockedResponse클래스의 호출값을 준다.
      • MockedResponse클래스는 json()함수를 호출할 경우, 우리가 카카오서버로부터 받을거라고 가정한 값들이 반환된다
      • 실제로 뷰에서 requests.get은 카카오서버와의 결과값을 받으며, .json()을 사용해 카카오서버로부터의 값들을 받을 수 있다.
      • 따라서 MockedReponse의 클래스값을 받은 mocked_requests.get은 자연스럽게 logic에 따라 json()이 호출되고 우리가 넣어준 API통신값을 내뱉게 된다.
      • 결론적으로 테스트를 진행하는 동안, 마치 특정한 값을 카카오API서버로부터 받는 것처럼 만들 수 있는 것
  • API서버로부터 특정한 값을 받을 경우의 응답값을 client를 사용해 호출하여 response에 받아주고, 그렇게 reponse로 받은 값들이 정말로 우리가 의도한 값과 일치하는지 마지막에 확인해주면 완료
    • 이때 실제 뷰가 내뱉는 값의 종류에 맞게 데이터를 맞춰주면 된다
    • 아래 예시에서는 실제 뷰가 message, jwt_Access_token, nickname,profileimage, image_url을 던져주므로,  해당 키들에 맞는 목표값들을 적어준 것임
  • 아래 테스트를 진행하면서 오랜기간 고민했던 오류가 있는데, setUp함수에서 유저객체를 생성해줄 필요가 있느냐는 것이였음
    • 원래는 setUp부분을 지우고 users를 단독으로 테스트진행시 아무런 문제가 발생하지 않았음(의도한 대로 되었음)
    • 근데 모든 app을 동시에 테스트할 경우, user의 id가 1이 아니라 뒤로 밀리는 현상이 발생했음
    • 이는 user객체를 User테이블에서 지워도, 한번 생성되었던 값에 대한 기록이 남아있기 때문에 (id=1로 지정해주지 않을 경우) id=1이 아닌 id=2나 id=3인 user객체가 생성되었음
    • 이에 따라 jwt토큰의 값에 오류가 발생하게 되고, 테스트가 계속 실패하는 현상이 발생하였음
    • 따라서 해당 id=1인 유저객체를 만들고, MagicMock을 통해 받는 kakao_id정보를 해당 유저객체와 일치하도록 하게 하였음
    • 이렇게 할 경우 새로운 유저를 생성하는 게 아니라 기존 유저에 대한 정보를 사용하므로  jwt토큰에 사용되는 값을 유저의 id값을 의도한대로 1로 줄 수 있어, token비교에서 에러가 발생하지 않는다.
from django.test     import TestCase,Client
from unittest.mock    import patch, MagicMock
from datetime        import datetime,timedelta
from users.models    import User
import json, jwt
from TerraBox.settings import *


class KaKaoSignTest(TestCase):
    def setUp(self):
        User.objects.create(
            id= 1,
            nickname = 'YB',
            kakao_id = 12345,
            email = "test@email.com",
            profile_image_url = 'http://test@profile_image'
        )
        # 필요없음 > 필요함. 없어도 될줄 알았는데, 없으면 db가 통일되어 있어서 , 기존에 아이디가 만들어졌다 사라져서 id=1 이 될 수 없음
        # 즉, id =2 가 되어서 jwt토큰이 맛탱이감
        # 아니면 id=2를 jwt토큰에 넣어주던가
        
    def tearDown(slef):
        User.objects.all().delete()

    @patch('users.views.requests') 
    #users.views에 등장하는 모든 requests의 값을 magicmock이라는 객체로 바꿔줌
    def test_success_kakaologin_new_user(self,mocked_requests):
    #mocked_requests는 실제 view에 등장한느 request와 아무런 관련이 없다! 그냥 사용하기 위해 만들어준 객체일 뿐임
        client = Client()

        class MockedResponse:
            def json(self):
                return {
        "id": 12345,
        "connected_at": "2022-05-12T16:57:36Z",
        "properties": {
            "nickname": "YB"
        },
        "kakao_account": {
            "profile": {
                "nickname": "김영빈",
                "thumbnail_image_url": "http://test@profile_image",
                "profile_image_url": "http://test@profile_image",
            },
            "email": "test@email.com"
        }
    }
        mocked_requests.get = MagicMock(return_value=MockedResponse())
        #users.views.requests대신에 mocked_requests라는 게 들어가고,
        #mocked_requests.get에 Mockedresponse가 들어간다
        #mockedResponse는 결국 mockedResponse.json()와 같은 식으로 호출될 꺼니까, json함수를 넣어준 것임
        
        headers = {"HTTP_Authorization": "access_token"}
        response = client.post('/users/login',**headers)
        
        expiration = timedelta(seconds=3600)
        token_expiration_time = datetime.utcnow() + expiration
        jwt_access_token = jwt.encode({
            'id'  : 1,
            'exp' : token_expiration_time},SECRET_KEY,algorithm=ALGORITHM)
        
        self.assertEqual(response.status_code,201)
        self.assertEqual(response.json(),{
            'message'           : 'success!',
            'JWT_ACCESS_TOKEN'  : jwt_access_token,
            'profile_image_url' : 'http://test@profile_image', #설정을 바보같이 해놔서 프로필이 아니라 썸네일을 가져옴
            'nickname'          : 'YB',
            'email'             : "test@email.com",
        })
        #client로 요청을 보낸 결과가 json response > json으로 풀어줌 > 풀어준 내용이 mockdata를 만들면서 의도한 내용과 일치하는지를 확인해주는 것임

 

  • 참고)카카오 소셜 로그인 뷰
import json, jwt, requests

from django.http       import JsonResponse
from django.views      import View
from datetime          import datetime,timedelta
from TerraBox.settings import ALGORITHM,SECRET_KEY

from users.models import User

def get_kakao_info(token):
    kakao_url = "https://kapi.kakao.com/v2/user/me"
    header ={
        "Content-Type"  : "application/x-www-form-urlencoded",
        "Authorization" : token,
    }

    return requests.get(kakao_url,headers=header).json()

class KakaoLoginView(View):

    def post(self,request):
        try:
            token = f"Bearer {request.headers.get('Authorization')}"

            response          = get_kakao_info(token)
            kakao_id          = response.get('id')
            nickname          = response.get('properties').get('nickname')
            profile_image_url = response.get('kakao_account').get('profile').get('thumbnail_image_url')
            email             = response.get('kakao_account').get('email')
            
            user,created = User.objects.get_or_create(
                kakao_id=kakao_id, 
                defaults = {
                    "nickname"         :nickname,
                    "profile_image_url":profile_image_url,
                    "email"            :email,
                }
            )

            expiration = timedelta(seconds=3600)
            token_expiration_time = datetime.utcnow() + expiration
            
            jwt_access_token = jwt.encode({'id':user.id,'exp':token_expiration_time},SECRET_KEY,algorithm=ALGORITHM)
            
            return JsonResponse({
            'message':'success!',
            'JWT_ACCESS_TOKEN' :jwt_access_token,
            "profile_image_url":user.profile_image_url,
            "nickname":user.nickname,
            "email":user.email},status=201)

        except KeyError:
            return JsonResponse({'message':'KEY_ERROR'},status=400)

 

5. 추가내용

  • setUp vs setUptestData
  • bulk_create

setUp vs setUptestData

  • setUp()
    • 한 클래스 내부에 있는 모든 test를 진행할 때마다 호출 돼서, 매번 데이터를 생성함 > teardown
  • setUpTestData
    • 클래스 메서드를 선언해줘야 함!
    • 해당 클래스가 실행될 때 초기에 단 한번만 실행
    • 매번 데이터를 생성하는 setUp보다 당연히 빠름
    • 읽기 기능만 있는 경우, 해당 데이터를 사용하면 좋다.(읽기만 할 경우, 데이터의 재생성이 필요 없으니 사용하기에 적절)
class YourTestClass(TestCase): 
    @classmethod 
    def setUpTestData(cls): 
        Author.objects.create(first_name='test setUpTestData', last_name='once') 
        pass 
        
    def setUp(self): 
        Author.objects.create(first_name='test setup', last_name='once per call') 
        pass

 

bulk_create

  • unittest는 속도가 생명임. 따라서 유닛테스트의 속도를 올려줄 수 있는 수단은 가능한 동원하는 것이 좋다.
  • 그 중에 사용할 수 있는 효과적인 방안으로, 여러 객체를 한번에 생산해줄 수 있는 bulk_create가 있음.
  • 참고)bulk_create는 비단 유닛테스트에 종속되는 메서드가 아니다. 뷰에서 사용하려면 얼마든지 사용해도 됨
import json

from django.test import TestCase, Client
from .models     import Movie,MovieImage

class MovieListDetailTest(TestCase):
    def setUp(self):

        test_movies_list = [
            Movie(
            id             = 1,
            name           = 'test_name1',
            eng_name       = 'test_eng1',
            description    = 'des1',
            detail_text    = 'detail1',
            age_grade      = 'test@test.com',
            is_subtitle    = True,
            screening_type = 2,
            preview_url    = 'test@preview.com',
            running_time   = 120,
            ),
            Movie(
            id             = 2,
            name           = 'test_name2',
            eng_name       = 'test_eng2',
            description    = 'des2',
            detail_text    = 'detail2',
            age_grade      = 'test@test.com',
            is_subtitle    = True,
            screening_type = 2,
            preview_url    = 'test@preview.com',
            running_time   = 220,
            ),
        ]

        test_images_list = [
            MovieImage(
            movie_id=1,
            stillcut_url='test_img1'
            ),
            MovieImage(
            movie_id=2,
            stillcut_url='test_img2'
            ),
        ]

        Movie.objects.bulk_create(test_movies_list)
        MovieImage.objects.bulk_create(test_images_list)

    def tearDown(self):
        Movie.objects.all().delete()
    
    def test_success_get_product_list(self):
    	...
        ...
  • bulk_create를 활용하여 movie데이터와 image데이터를 생성.
  • Image데이터가 movie데이터를 foreignkey로 가지고 있으므로 movie데이터를 생성하고 나서 image데이터를 생성했다
  • teardown메서드를 활용해, 리소스들이 정리될 수 있도록 하였음

 

 

6. 유닛테스트의 원칙

 

Test의 일반 원칙

Unit test를 구현할때 지켜야 하는 일반적인 원칙들은 다음과 같습니다.

  • 테스트 유닛은 각 기능의 가장 작은 단위에 집중하여, 해당 기능이 정확히 동작하는지를 증명해야 합니다.
  • 각 테스트 유닛은 반드시 독립적이어야 합니다. 각 테스트는 혼자서도 실행 가능해야하고, 테스트 슈트로도 실행 가능해야 합니다. 이 때, 호출되는 순서와 무관하게 잘 동작해야 합니다. 이 규칙이 뜻하는 바, 새로운 데이터셋으로 각각의 테스트를 로딩해야 하고, 그 실행 결과는 꼭 삭제해야합니다. 보통 setUp() 과 tearDown() 메소드로 이런 작업을 합니다.
  • 테스트가 빠르게 돌 수 있도록 만들기 위해 노력해야 합니다. 테스트 하나가 실행하는데 몇 밀리세컨드 이상의 시간이 걸린다면, 개발 속도가 느려지거나 테스트가 충분히 자주 수행되지 못할 것입니다. 테스트에 필요한 데이터 구조가 너무 복잡하고, 테스트를 하려면 매번 이 복잡한 데이터를 불러와야 해서 테스트를 빠르게 만들 수 없는 경우도 있습니다. 이럴 때는 무거운 테스트는 따로 분리하여 별도의 테스트 슈트를 만들어 두고 스케쥴 작업을 걸어두면 됩니다. 그리고 그 외의 다른 모든 테스트는 필요한 만큼 자주 수행하면 됩니다.
  • 지금 사용하고 있는 툴이 개별 테스트나 테스트 케이스를 어떻게 수행하는지 배우셔야 합니다. 모듈 안에 들어있는 함수를 개발하고 있다면, 그 함수의 테스트를 자주, 가능하다면 코드를 저장할 때마다 자동으로 돌려야 합니다.
  • 그날의 코딩을 시작하기 전에 항상 풀 테스트 슈트를 돌려야 합니다. 끝난 후에도 마찬가지입니다. 이 작업은 당신이 다른 코드를 망가뜨리지 않았다는 더 큰 자신감을 심어줄 것입니다.
  • 모두가 공유하는 저장소에다가 코드를 집어넣기 전에 자동으로 모든 테스트를 수행하도록 하는 훅을 구현하는 하는 것이 좋습니다.
  • 지금 한창 개발 중인데 그만두고 잠시 다른 일을 해야한다면, 다음에 개발할 부분에다가 일부러 고장난 유닛 테스트를 작성하는 것도 좋은 생각입니다.
  • 코드를 디버깅할 때 가장 먼저 시작할 일은 버그를 찝어내는 새로운 테스트를 작성하는 것입니다. 이런 일이 언제나 가능한 것은 아니지만, 이런 버그 잡이 테스트들이야말로 당신의 프로젝트에서 가장 가치있는 코드 조각이 될 것입니다.
  • 테스트 함수에는 길고 서술적인 이름을 사용하셔야 합니다. 테스트에서의 스타일 안내서는 짧은 이름을 보다 선호하는 다른 일반적인 코드와는 조금 다릅니다. 테스트 함수는 절대 직접 호출되지 않기 때문입니다. 실제로 돌아가는 코드에서는 square() 라든가 심지어 sqr() 조차도 괜찮습니다. 하지만 테스트 코드에서는 test_square_of_number_2(), test_square_negative_number() 같은 이름을 붙여야 합니다. 이런 함수명들은 테스트가 실패할 때나 보입니다. 그러니 반드시 가능한 한 서술적인 이름을 붙여야 합니다.
  • 무언가 잘못되었거나 뜯어고쳐야만 할 경우, 괜찮은 코드에 테스트 셋이 있다면 당신이나 다른 유지보수 담당자들은 오류를 수정하거나 프로그램의 동작을 수정할 때 필시 그 테스트 슈트에 전적으로 의지할 것입니다.
  • 테스트 코드의 또다른 사용 방법은 새로운 개발자들을 위한 안내서로 쓰는 방법입니다. 이미 만들어져 있는 코드에서 작업해야할 경우, 관련 테스트 코드를 돌려보고 읽어보는 것이야말로 가장 좋은 시작점일 경우가 많습니다. 이렇게 테스트 코드를 돌려보면 어느 지점이 문제인지, 수정하기 어려운 곳은 어디일지, 막다른 골목은 어디일지를 발견하게 됩니다. 몇 가지 기능을 추가해야 한다면 가장 먼저 해야할 일은, 그 새로운 기능이 아직 돌아가지 않음을 확인할 수 있는 테스트를 붙여넣는 것입니다.

BELATED ARTICLES

more