개요
- 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)
def test_TC_CALENDAR_001(self):
"""
현재 로그인된 계정의 보조 캘린더 생성하기
실제 생성한 캘린더의 calendarId 를 txt 파일로 생성하여 다른 API 에서 사용할 수 있도록 처리
:return:
"""
try:
calendar = {
"summary": "test calendar",
"timeZone": "Asia/Seoul"
}
created_calendar = self.service.calendars().insert(body=calendar).execute()
print("생성된 calendarId : " + created_calendar["id"])
with open("../calendar_id.txt", "w") as file:
file.write(created_calendar["id"])
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"
}
with self.assertRaises(HttpError) as context:
self.service.calendars().insert(body=calendar).execute()
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)
def test_TC_CALENDAR_003(self):
"""
현재 로그인된 계정 기본 캘린더의 메타데이터 확인하기
insert API 를 통해 생성한 calendarId 사용
:return:
"""
try:
with open("../calendar_id.txt", "r") as file:
calendar_id = file.read().strip()
calendar = self.service.calendars().get(calendarId=calendar_id).execute()
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"
with self.assertRaises(HttpError) as error_context:
self.service.calendars().get(calendarId=invalid_calendar_id).execute()
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:
with open("../calendar_id.txt", "r") as file:
calendar_id = file.read().strip()
calendar = self.service.calendars().get(calendarId=calendar_id).execute()
calendar['summary'] = 'New Summary'
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:
with open("../calendar_id.txt", "r") as file:
calendar_id = file.read().strip()
calendar = self.service.calendars().get(calendarId=calendar_id).execute()
calendar['summary'] = ''
with self.assertRaises(HttpError) as error_context:
self.service.calendars().update(calendarId=calendar['id'], body=calendar).execute()
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:
with open("../calendar_id.txt", "r") as file:
calendar_id = file.read().strip()
calendar = self.service.calendars().get(calendarId=calendar_id).execute()
calendar['description'] = 'New description'
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:
with open("../calendar_id.txt", "r") as file:
calendar_id = file.read().strip()
calendar = self.service.calendars().get(calendarId=calendar_id).execute()
calendar['location'] = 'New location'
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:
with open("../calendar_id.txt", "r") as file:
calendar_id = file.read().strip()
calendar = self.service.calendars().get(calendarId=calendar_id).execute()
calendar['timeZone'] = 'Europe/Zurich'
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:
with open("../calendar_id.txt", "r") as file:
calendar_id = file.read().strip()
calendar = self.service.calendars().get(calendarId=calendar_id).execute()
calendar['timeZone'] = 'New timezone'
with self.assertRaises(HttpError) as error_context:
self.service.calendars().update(calendarId=calendar['id'], body=calendar).execute()
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:
with open("../calendar_id.txt", "r") as file:
calendar_id = file.read().strip()
calendar = self.service.calendars().get(calendarId=calendar_id).execute()
calendar['summary'] = 'update summary'
calendar['description'] = 'update description'
calendar['location'] = 'update location'
calendar['timeZone'] = 'America/Los_Angeles'
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:
with open("../calendar_id.txt", "r") as file:
calendar_id = file.read().strip()
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"
with self.assertRaises(HttpError) as error_context:
self.service.calendars().delete(calendarId=calendar_id).execute()
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 를 이용하여 새롭게 설계해 보았는데, 다행히 긍정적으로 동작하고 생각보다 쉽게 도입할 수 있었던 것 같다.