unittest 로 API 자동화 테스트 설계해보기

Seunghoon Yoo·2024년 6월 27일
0
post-thumbnail

개요

  • unittest 는 Python에서 별도의 설치 없이 사용할 수 있는 단위 테스트 설계 프레임워크이다. unittest 를 이용하여, api 자동화 테스트 스크립트를 설계해보고자 한다.
  • 테스트 스크립트를 작성할 대상은 구글에서 제공하는 캘린더 오픈 API

Setup

  • Python 언어 사용
  • Unittest 프레임워크 사용
  • Google Libarary (Python) 사용

스크립트 구조

  • 크게 아래와 같이 디렉토리를 구성한다.
  • authorize : 구글 계정 토큰 정보를 저장하는 커스텀 패키지
  • scripts : unittest 함수 및 메소드를 이용한 실제 테스트 스크립트를 설계하는 패키지

authorize > authorization.py

  • 실제 테스트용 구글 계정의 Personal token 정보를 저장하는 함수 정의
  • 실제 토큰값을 하드코딩해야 하므로 보안상 이슈가 될 수 있음
def get_token():
    """
    하드코딩된 Google API 토큰을 반환
    """
    token = 'ya29.a0AXooCgt......'
    return token

scripts > test_001_calendar_insert.py

  • unittest 테스트 스크립트를 작성하기 위한 모듈이다.
  • 새로운 캘린더를 생성 > 조회 > 업데이트 > 삭제하는 플로우를 자동화하기 위해, 구글에서 제공하는 오픈 API 문서를 참조한다. https://developers.google.com/calendar/api/v3/reference/calendars
  • 구글 오픈API 문서에 따르면 아래와 같은 엔드포인트로 구성되는 듯 하다.
    • 생성 : insert
    • 조회 : get
    • 업데이트 : patch, update
    • 삭제 : clear, delete
  • 모듈 이름을 지을 때 규칙성을 부여하고, 나는 "test_001_calendar_insert.py" 같은 형식으로 지었다.
  • 캘린더를 생성하는 실제 스크립트는 아래와 같이 작성했다. 정상 동작을 기대하는 함수와, 예외 케이스를 고려하는 함수 등을 다양하게 작성할 수 있다.
import unittest

from authorize.authorization import get_token
from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError


class TestCalendarInsert(unittest.TestCase):

    def setUp(self):
        """
        테스트 전에 실행되어 필요한 설정을 수행함.
        Google Calendar API 가 필요하므로, auth 셋업을 위해 OAuth 2 Token 값을 가져옴.
        """
        self.token = get_token()
        credentials = Credentials(self.token)
        self.service = build('calendar', 'v3', credentials=credentials)

    # 보조 캘린더를 생성하는 API TestCase
    def test_TC_CALENDAR_001(self):
        """
        현재 로그인된 계정의 보조 캘린더 생성하기
        실제 생성한 캘린더의 calendarId 를 txt 파일로 생성하여 다른 API 에서 사용할 수 있도록 처리
        :return:
        """
        try:
            # 생성할 캘린더에 추가할 필드 (summary, timeZone)
            calendar = {
                "summary": "test calendar",
                "timeZone": "Asia/Seoul"
            }
            # insert API 호출 (구글 라이브러리 사용)
            created_calendar = self.service.calendars().insert(body=calendar).execute()
            print("생성된 calendarId : " + created_calendar["id"])
            # 생성된 캘린더의 calendarId 를 .txt 파일로 저장, 다른 API 에서 인자로 사용할 수 있도록 설계
            with open("../calendar_id.txt", "w") as file:
                file.write(created_calendar["id"])
            # id 필드, summary 필드가 존재하는지 검증
            self.assertIsNotNone(created_calendar.get("id"))
            self.assertIn("id", created_calendar, "\"id\" 필드가 존재하지 않음")
            self.assertIn("summary", created_calendar, "\"summary\" 필드가 존재하지 않음")
            print("TC_CALENDAR_001 : Passed")
        except HttpError as error:
            print("TC_CALENDAR_001 : Failed")
            self.fail(f"{error} 발생")
        except Exception as error:
            print("TC_CALENDAR_001 : Failed")
            self.fail(f"{error} 발생")

    def test_TC_CALENDAR_002(self):
        """
        summary 필드를 누락한 상태로 현재 로그인된 계정의 보조 캘린더 생성하기
        이 케이스는 예외 케이스로, 캘린더 생성 시 HttpError 가 발생하는 경우를 검증
        캘린더 생성이 성공적으로 수행되지 않을 때 Passed 처리
        :return:
        """
        try:
            calendar = {
                "summary": "",
                "timeZone": "Asia/Seoul"
            }
            # HttpError 발생할 것을 예상하고, 이를 통해 예외 처리 로직 검증
            with self.assertRaises(HttpError) as context:
                self.service.calendars().insert(body=calendar).execute()
            # HttpError 예외가 발생했고, 그 상태 코드가 400인지 확인
            self.assertEqual(context.exception.resp.status, 400, "400 응답 코드 반환")
            print("TC_CALENDAR_002 : Passed")
        except HttpError as error:
            print("TC_CALENDAR_002 : Failed")
            self.fail(f"{error} 발생")
        except Exception as error:
            print("TC_CALENDAR_002 : Failed")
            self.fail(f"{error} 발생")


if __name__ == "__main__":
    unittest.main()
  • 해당 API 를 호출하면 새로운 테스트용 캘린더가 생성된다.

scripts > test_002_calendar_get.py

  • 이제 새롭게 생성한 테스트용 캘린더가 정상적으로 조회되는 지 확인하는 테스트 케이스를 설계한다.
  • 조회하기 위해 구글 오픈API 문서에서 제공하는 get API 를 호출한다.
  • 테스트 스크립트는 아래와 같으며, 정상 동작을 기대하는 함수와, 예외 케이스를 고려하는 함수 등을 다양하게 작성할 수 있다.
import unittest

from authorize.authorization import get_token
from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError


class TestCalendarGet(unittest.TestCase):

    def setUp(self):
        """
        테스트 전에 실행되어 필요한 설정을 수행함.
        Google Calendar API 가 필요하므로, auth 셋업을 위해 OAuth 2 Token 값을 가져옴.
        """
        self.token = get_token()
        credentials = Credentials(self.token)
        self.service = build('calendar', 'v3', credentials=credentials)

    # 보조 캘린더 목록을 조회하는 API TestCase
    def test_TC_CALENDAR_003(self):
        """
        현재 로그인된 계정 기본 캘린더의 메타데이터 확인하기
        insert API 를 통해 생성한 calendarId 사용
        :return:
        """
        try:
            # insert API 를 통해 생성한 캘린더의 calendarId 가져옴
            with open("../calendar_id.txt", "r") as file:
                calendar_id = file.read().strip()
            # get API 호출 (구글 라이브러리 사용)
            calendar = self.service.calendars().get(calendarId=calendar_id).execute()
            # calendarId 를 통해 캘린더 호출 시, 각 리소스 필드 (kind, etag, id, summary) 가 존재하는 지 확인
            self.assertIn("kind", calendar, "\"kind\" 필드가 존재하지 않음")
            self.assertIn("etag", calendar, "\"etag\" 필드가 존재하지 않음")
            self.assertIn("id", calendar, "\"id\" 필드가 존재하지 않음")
            self.assertIn("summary", calendar, "\"summary\" 필드가 존재하지 않음")
            print("TC_CALENDAR_003 : Passed")
        except HttpError as error:
            print("TC_CALENDAR_003 : Failed")
            self.fail(f"{error} 발생")
        except Exception as error:
            print("TC_CALENDAR_003 : Failed")
            self.fail(f"{error} 발생")

    def test_TC_CALENDAR_004(self):
        """
        유효하지 않은 타입의 calendarId 를 입력하여 해당 계정의 기본 캘린더 메타데이터 확인하기
        이 케이스는 예외 케이스로, 캘린더 조회 시 HttpError 가 발생하는 경우를 검증
        캘린더 조회가 성공적으로 수행되지 않을 때 Passed 처리
        :return:
        """
        try:
            invalid_calendar_id = "1"
            # HttpError 발생할 것을 예상하고, 이를 통해 예외 처리 로직 검증
            with self.assertRaises(HttpError) as error_context:
                self.service.calendars().get(calendarId=invalid_calendar_id).execute()
            # HttpError 예외가 발생했고, 그 상태 코드가 404인지 확인
            self.assertEqual(error_context.exception.resp.status, 404, "404 응답 코드 반환")
            print("TC_CALENDAR_004 : Passed")
        except HttpError as error:
            print("TC_CALENDAR_004 : Failed")
            self.fail(f"{error} 발생")
        except Exception as error:
            print("TC_CALENDAR_004 : Failed")
            self.fail(str(error))


if __name__ == "__main__":
    unittest.main()
  • 해당 API 를 호출하면 생성한 테스트용 캘린더를 조회할 수 있다.

scripts > test_003_calendar_update.py

  • 생성한 테스트용 캘린더를 조회하였으니, 이번엔 캘린더 내용을 업데이트해보는 스크립트를 설계한다.
  • 구글 오픈 API문서에서 제공하는 update API 를 호출한다.
import unittest

from authorize.authorization import get_token
from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError


class TestCalendarUpdate(unittest.TestCase):

    def setUp(self):
        """
        테스트 전에 실행되어 필요한 설정을 수행함.
        Google Calendar API 가 필요하므로, auth 셋업을 위해 OAuth 2 Token 값을 가져옴.
        """
        self.token = get_token()
        credentials = Credentials(self.token)
        self.service = build('calendar', 'v3', credentials=credentials)

    def test_TC_CALENDAR_005(self):
        """
        현재 로그인된 계정의 캘린더 summary 필드 업데이트하기
        insert API 를 통해 생성한 calendarId 사용
        :return:
        """
        try:
            # insert API 를 통해 생성한 캘린더의 calendarId 가져옴
            with open("../calendar_id.txt", "r") as file:
                calendar_id = file.read().strip()
            # 지정한 calendarId 를 인자로 전달한 get API 호출 (구글 라이브러리 사용)
            calendar = self.service.calendars().get(calendarId=calendar_id).execute()
            # summary 필드를 'New Summary' 로 수정
            calendar['summary'] = 'New Summary'
            # update API 호출 (구글 라이브러리 사용)
            self.service.calendars().update(calendarId=calendar['id'], body=calendar).execute()
            print("TC_CALENDAR_005 : Passed")
        except HttpError as error:
            print("TC_CALENDAR_005 : Failed")
            self.fail(f"{error} 발생")
        except Exception as error:
            print("TC_CALENDAR_001 : Failed")
            self.fail(f"{error} 발생")

    def test_TC_CALENDAR_006(self):
        """
        summary 필드를 누락한 상태로 현재 로그인된 계정의 캘린더 업데이트하기
        이 케이스는 예외 케이스로, 캘린더 업데이트 시 HttpError 가 발생하는 경우를 검증
        캘린더 업데이트가 성공적으로 수행되지 않을 때 Passed 처리
        :return:
        """
        try:
            # insert API 를 통해 생성한 캘린더의 calendarId 가져옴
            with open("../calendar_id.txt", "r") as file:
                calendar_id = file.read().strip()
            # 지정한 calendarId 를 인자로 전달한 get API 호출 (구글 라이브러리 사용)
            calendar = self.service.calendars().get(calendarId=calendar_id).execute()
            # summary 필드를 빈 값으로 수정
            calendar['summary'] = ''
            # HttpError 발생할 것을 예상하고, 이를 통해 예외 처리 로직 검증
            with self.assertRaises(HttpError) as error_context:
                self.service.calendars().update(calendarId=calendar['id'], body=calendar).execute()
            # HttpError 예외가 발생했고, 그 상태 코드가 400인지 확인
            self.assertEqual(error_context.exception.resp.status, 400, "400 응답 코드 반환")
            print("TC_CALENDAR_006 : Passed")
        except HttpError as error:
            print("TC_CALENDAR_006 : Failed")
            self.fail(f"{error} 발생")
        except Exception as error:
            print("TC_CALENDAR_001 : Failed")
            self.fail(f"{error} 발생")

    def test_TC_CALENDAR_007(self):
        """
        현재 로그인된 계정의 캘린더 description 필드 업데이트하기
        insert API 를 통해 생성한 calendarId 사용
        :return:
        """
        try:
            # insert API 를 통해 생성한 캘린더의 calendarId 가져옴
            with open("../calendar_id.txt", "r") as file:
                calendar_id = file.read().strip()
            # 지정한 calendarId 를 인자로 전달한 get API 호출 (구글 라이브러리 사용)
            calendar = self.service.calendars().get(calendarId=calendar_id).execute()
            # description 필드를 'New description' 으로 수정
            calendar['description'] = 'New description'
            # update API 호출 (구글 라이브러리 사용)
            self.service.calendars().update(calendarId=calendar['id'], body=calendar).execute()
            print("TC_CALENDAR_007 : Passed")
        except HttpError as error:
            print("TC_CALENDAR_007 : Failed")
            self.fail(f"{error} 발생")
        except Exception as error:
            print("TC_CALENDAR_001 : Failed")
            self.fail(f"{error} 발생")

    def test_TC_CALENDAR_008(self):
        """
        현재 로그인된 계정의 캘린더 location 필드 업데이트하기
        insert API 를 통해 생성한 calendarId 사용
        :return:
        """
        try:
            # insert API 를 통해 생성한 캘린더의 calendarId 가져옴
            with open("../calendar_id.txt", "r") as file:
                calendar_id = file.read().strip()
            # 지정한 calendarId 를 인자로 전달한 get API 호출 (구글 라이브러리 사용)
            calendar = self.service.calendars().get(calendarId=calendar_id).execute()
            # location 필드를 'New location' 으로 수정
            calendar['location'] = 'New location'
            # update API 호출 (구글 라이브러리 사용)
            self.service.calendars().update(calendarId=calendar['id'], body=calendar).execute()
            print("TC_CALENDAR_008 : Passed")
        except HttpError as error:
            print("TC_CALENDAR_008 : Failed")
            self.fail(f"{error} 발생")
        except Exception as error:
            print("TC_CALENDAR_001 : Failed")
            self.fail(f"{error} 발생")

    def test_TC_CALENDAR_009(self):
        """
        현재 로그인된 계정의 캘린더 timeZone 필드 업데이트하기
        insert API 를 통해 생성한 calendarId 사용
        :return:
        """
        try:
            # insert API 를 통해 생성한 캘린더의 calendarId 가져옴
            with open("../calendar_id.txt", "r") as file:
                calendar_id = file.read().strip()
            # 지정한 calendarId 를 인자로 전달한 get API 호출 (구글 라이브러리 사용)
            calendar = self.service.calendars().get(calendarId=calendar_id).execute()
            # timeZone 필드를 'Europe/Zurich' 으로 수정
            calendar['timeZone'] = 'Europe/Zurich'
            # update API 호출 (구글 라이브러리 사용)
            self.service.calendars().update(calendarId=calendar['id'], body=calendar).execute()
            print("TC_CALENDAR_009 : Passed")
        except HttpError as error:
            print("TC_CALENDAR_009 : Failed")
            self.fail(f"{error} 발생")
        except Exception as error:
            print("TC_CALENDAR_001 : Failed")
            self.fail(f"{error} 발생")

    def test_TC_CALENDAR_010(self):
        """
        현재 로그인된 계정의 캘린더 timeZone 비유효한 값으로 업데이트하기
        이 케이스는 예외 케이스로, 캘린더 업데이트 시 HttpError 가 발생하는 경우를 검증
        캘린더 업데이트가 성공적으로 수행되지 않을 때 Passed 처리
        :return:
        """
        try:
            # insert API 를 통해 생성한 캘린더의 calendarId 가져옴
            with open("../calendar_id.txt", "r") as file:
                calendar_id = file.read().strip()
            # 지정한 calendarId 를 인자로 전달한 get API 호출 (구글 라이브러리 사용)
            calendar = self.service.calendars().get(calendarId=calendar_id).execute()
            # timeZone 필드를 'New timezone' 으로 수정
            calendar['timeZone'] = 'New timezone'
            # HttpError 발생할 것을 예상하고, 이를 통해 예외 처리 로직 검증
            with self.assertRaises(HttpError) as error_context:
                self.service.calendars().update(calendarId=calendar['id'], body=calendar).execute()
            # HttpError 예외가 발생했고, 그 상태 코드가 400인지 확인
            self.assertEqual(error_context.exception.resp.status, 400, "400 응답 코드 반환")
            print("TC_CALENDAR_010 : Passed")
        except HttpError as error:
            print("TC_CALENDAR_010 : Failed")
            self.fail(f"{error} 발생")
        except Exception as error:
            print("TC_CALENDAR_010 : Failed")
            self.fail(f"{error} 발생")

    def test_TC_CALENDAR_011(self):
        """
        현재 로그인된 계정의 캘린더 모든 필드 업데이트하기
        insert API 를 통해 생성한 calendarId 사용
        :return:
        """
        try:
            # insert API 를 통해 생성한 캘린더의 calendarId 가져옴
            with open("../calendar_id.txt", "r") as file:
                calendar_id = file.read().strip()
            # 지정한 calendarId 를 인자로 전달한 get API 호출 (구글 라이브러리 사용)
            calendar = self.service.calendars().get(calendarId=calendar_id).execute()
            # 모든 필드 수정 (summary, description, location, timeZone)
            calendar['summary'] = 'update summary'
            calendar['description'] = 'update description'
            calendar['location'] = 'update location'
            calendar['timeZone'] = 'America/Los_Angeles'
            # update API 호출 (구글 라이브러리 사용)
            self.service.calendars().update(calendarId=calendar['id'], body=calendar).execute()
            print("TC_CALENDAR_011 : Passed")
        except HttpError as error:
            print("TC_CALENDAR_011 : Failed")
            self.fail(f"{error} 발생")
        except Exception as error:
            print("TC_CALENDAR_011 : Failed")
            self.fail(f"{error} 발생")


if __name__ == "__main__":
    unittest.main()
  • 해당 API 를 호출하면 테스트용 캘린더의 정보를 업데이트한다.

scripts > test_004_calendar_delete.py

  • 마지막 단계인 캘린더를 삭제하는 과정을 스크립트로 설계한다.
  • 생성했던 캘린더를 조회하고, 캘린더의 정보를 수정한 다음 삭제하는 과정으로, 테스트 실행 전 초기 상태를 되돌리는 역할을 하게 된다.
  • 구글 오픈 API문서에서 제공하는 delete API 를 호출한다.
import unittest

from authorize.authorization import get_token
from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError


class TestCalendarDelete(unittest.TestCase):

    def setUp(self):
        """
        테스트 전에 실행되어 필요한 설정을 수행함.
        Google Calendar API 가 필요하므로, auth 셋업을 위해 OAuth 2 Token 값을 가져옴.
        """
        self.token = get_token()
        credentials = Credentials(self.token)
        self.service = build('calendar', 'v3', credentials=credentials)

    def test_CALENDAR_019(self):
        """
        현재 로그인된 계정 기본 캘린더 삭제하기
        insert API 를 통해 생성한 calendarId 사용
        :return:
        """
        try:
            # insert API 를 통해 생성한 캘린더의 calendarId 가져옴
            with open("../calendar_id.txt", "r") as file:
                calendar_id = file.read().strip()
            # delete API 호출 (구글 라이브러리 사용)
            self.service.calendars().delete(calendarId=calendar_id).execute()
            print("TC_CALENDAR_019 : Passed")
        except HttpError as error:
            print("TC_CALENDAR_019 : Failed")
            self.fail(f"{error} 발생")
        except Exception as error:
            print("TC_CALENDAR_019 : Failed")
            self.fail(f"{error} 발생")

    def test_CALENDAR_020(self):
        """
        유효하지 않은 타입의 calendarId 를 입력하여 해당 계정 기본 캘린더 삭제하기
        이 케이스는 예외 케이스로, 캘린더 삭제 시 HttpError 가 발생하는 경우를 검증
        캘린더 삭제가 성공적으로 수행되지 않을 때 Passed 처리
        :return:
        """
        try:
            calendar_id = "123"
            # HttpError 발생할 것을 예상하고, 이를 통해 예외 처리 로직 검증
            with self.assertRaises(HttpError) as error_context:
                self.service.calendars().delete(calendarId=calendar_id).execute()
            # HttpError 예외가 발생했고, 그 상태 코드가 404인지 확인
            self.assertEqual(error_context.exception.resp.status, 404, "404 응답 코드 반환")
            print("TC_CALENDAR_020 : Passed")
        except HttpError as error:
            print("TC_CALENDAR_020 : Failed")
            self.fail(f"{error} 발생")
        except Exception as error:
            print("TC_CALENDAR_020 : Failed")
            self.fail(f"{error} 발생")


if __name__ == "__main__":
    unittest.main()
  • 해당 API 를 호출하면 생성했던 테스트용 캘린더를 삭제한다.

후기

  • Python의 unittest 프레임워크와 구글 캘린더 오픈 API 를 이용하여 간단하게 API 테스트 자동화 스크립트를 작성해보았다. 실무에서 사용한다면 더욱 복잡한 스크립트 구조가 될 것으로 예상된다. 이에 따라 코드를 효율적으로 작성하거나 개인 정보를 담는 토큰을 하드코딩하지 않고 다른 방법을 연구하는 등의 개선이 필요할 것 같다는 생각을 했다.
  • postman + newman 을 이용한 API 자동화 스크립트만 설계하다가 unittest 를 이용하여 새롭게 설계해 보았는데, 다행히 긍정적으로 동작하고 생각보다 쉽게 도입할 수 있었던 것 같다.
profile
QA Engineer

0개의 댓글