[0811 - 0813] django 테스트 코드 작성

nikevapormax·2022년 8월 13일
0

TIL

목록 보기
89/116

iPark Project

테스트 코드 작성

  • 내가 프로젝트를 진행하면서 맡았던 user app 대다수와 park app의 공원 검색, 공원 토글 리스트, 인기순 공원 나열에 대한 테스트 코드를 작성하였다.
  • 이번 사용자 피드백 기간에는 기존에 작성했던 테스트 코드를 좀 더 보완하는 방향으로 해 최대한 많은 케이스를 아우를 수 있도록 작성해보았다.
  • 테스트 코드를 작성하며 문제없이 돌아갔던 코드지만 잘못된 부분을 찾아 수정하며 테스트 코드가 왜 중요한지 알 수 있는 작업이었다고 생각한다.

user app

  • user app에서 내가 작성한 테스트 코드는 아래와 같다.
    • 회원가입, 로그인, 회원정보 수정 및 탈퇴, 아이디 찾기, 계정관리, 비밀번호 변경
  • 모든 테스트 코드를 작성해보니 1000줄이 넘어 중요한 부분만 몇 가지 짚도록 하겠다.

회원가입

  • 회원가입을 하려면 사용자가 데이터를 입력해야 하며, 이중에서 ForeignKey를 통해 가져온 regionsetUpTestData를 통해 만들어 테스트 클래스가 끝날 때까지 데이터를 유지시키는 방식을 택했다.
@classmethod
def setUpTestData(cls):
    region_data = ["강남구", "강서구", "강북구", "은평구", "동작구", "중구"]
    for region in region_data:
        cls.region = RegionModel.objects.create(region_name=region)
        
    cls.region.save()
  • 회원가입은 매번 데이터를 넣어주어 db에 저장하는 것이므로 따로 사용자에 대한 db는 생성하지 않았다. 따라서 아래와 같이 매번 테스트 케이스를 돌릴 때마다 사용자의 데이터를 넣어주었다.
# 회원가입 테스트
class UserRegistrationTest(APITestCase):
    @classmethod
    def setUpTestData(cls):
        region_data = ["강남구", "강서구", "강북구", "은평구", "동작구", "중구"]
        for region in region_data:
            cls.region = RegionModel.objects.create(region_name=region)
        
        cls.region.save()

    # 정상적인 회원가입
    def test_registration_all_data(self):
        url = reverse("user_view")
        user_data = {
            "username" : "user10",
            "password" : "1010abc!",
            "fullname" : "user10",
            "email" : "user10@gmail.com",
            "phone" : "010-1010-1010",
            "region" : 2
        }
        
        response = self.client.post(url, user_data)

        self.assertEqual(response.status_code, 201)
        self.assertEqual(response.data["username"], "user10")
        self.assertEqual(response.data["region"], 2)
  • 회원가입에서 눈여겨 봐야 할 부분은 사용자가 입력한 데이터의 유효성을 검증하는 것이다. 따라서 아래와 같은 패턴을 정하고 각 항목이 해당하는 부분을 테스트 코드로 작성해 보았다.
    • (___)가 아예 없을 경우
    • (___)의 자릿수가 모자랄 경우
    • (___)가 중복되는 경우
  • 이 중 대부분은 기본 validator로 걸러졌고, 나머지는 custom validator로 걸러 이에 맞는 에러 메세지와 status code를 제대로 받고 있는지 확인하였다.
# username이 없을 때
def test_registration_no_username(self):
    url = reverse("user_view")
    user_data = {
        "username" : "",
        "password" : "1010abc!",
        "fullname" : "user10",
        "email" : "user10@gmail.com",
        "phone" : "010-1010-1010",
        "region" : 2
    }
    
    response = self.client.post(url, user_data)

    self.assertEqual(response.status_code, 400)
    self.assertEqual(response.data["username"][0], "이 필드는 blank일 수 없습니다.")
    
# username의 자릿수가 모자랄 때
def test_registration_wrong_username(self):
    url = reverse("user_view")
    user_data = {
        "username" : "user",
        "password" : "1010abc!",
        "fullname" : "user10",
        "email" : "user10@gmail.com",
        "phone" : "010-1010-1010",
        "region" : 2
    }
    
    response = self.client.post(url, user_data)

    self.assertEqual(response.status_code, 400)
    self.assertEqual(response.data["username"][0], "username의 길이는 6자리 이상이어야 합니다.")
    
# 중복 username인 경우
def test_registration_same_username(self):
    url = reverse("user_view")
    user_data = {
        "username" : "user10",
        "password" : "1010abc!",
        "fullname" : "user10",
        "email" : "user10@gmail.com",
        "phone" : "010-1010-1010",
        "region" : 2
    }
    
    user_data_2 = {
        "username" : "user10",
        "password" : "2020abc!",
        "fullname" : "user20",
        "email" : "user20@gmail.com",
        "phone" : "010-2020-2020",
        "region" : 2
    }
    
    response = self.client.post(url, user_data)
    response_2 = self.client.post(url, user_data_2)

    self.assertEqual(response.status_code, 201)
    self.assertEqual(response_2.status_code, 400)
    self.assertEqual(response_2.data["username"][0], "user의 사용자 계정은/는 이미 존재합니다.")

로그인

  • 로그인 테스트 코드는 create_user를 사용해 로그인을 할 사용자 db를 간단하게 만들고, 이를 로그인시켜 db에 로그인할 사용자의 데이터가 있는지 확인하는 방식으로 진행했다.
  • JWT를 사용하고 있어 로그인을 시도하면 토큰들이 나오는데, 이것을 비교해보고 싶었으나 매번 값이 달라져 어쩔 수 없이 status_code만 확인하였다.
# 로그인 테스트
class UserLoginTest(APITestCase):
    @classmethod
    def setUpTestData(cls):
        cls.user_data = {"username" : "user10", "password" : "1010abc!"}
        cls.user = UserModel.objects.create_user("user10", "1010abc!")
    
    # 모든 정보를 기입한 경우
    def test_login_all_data(self):
        url = reverse("ipark_token")
        user_data = {
            "username" : "user10",
            "password" : "1010abc!"
        }
        
        response = self.client.post(url, user_data)
        
        self.assertEqual(response.status_code, 200)
        
    # username을 기입하지 않은 경우
    def test_login_no_username(self):
        url = reverse("ipark_token")
        user_data = {
            "username" : "",
            "password" : "1010abc!"
        }
        
        response = self.client.post(url, user_data)

        self.assertEqual(response.status_code, 400)
        self.assertEqual(response.data["username"][0], "이 필드는 blank일 수 없습니다.")
    
    # password를 기입하지 않은 경우
    def test_login_no_password(self):
        url = reverse("ipark_token")
        user_data = {
            "username" : "user1010",
            "password" : ""
        }
        
        response = self.client.post(url, user_data)

        self.assertEqual(response.status_code, 400)
        self.assertEqual(response.data["password"][0], "이 필드는 blank일 수 없습니다.")

회원정보 수정 및 회원탈퇴

  • 회원정보의 수정 및 탈퇴를 진행하기 위해 setUpTestData를 통해 사용자 db를 만들어 테스트 클래스가 끝날 때까지 db를 유지하는 방식을 사용했다.
    • 사용자의 기본 정보들은 모델을 사용해 db를 생성하는 것처럼 진행하면 되며, foreignKeyregion은 아래와 같이 작성해 값이 잘 들어가지는 것을 볼 수 있다.
    • client의 경우, cls를 사용하는 setUpTestData 안에서는 사용하지 못하는 것으로 알고 있어 그냥 setUp을 만들어 사용했었다. 하지만 아래과 같이 APIClient()를 임포트해 사용하게 되면 작성할 수 있다는 것을 알게 되었다.
@classmethod
def setUpTestData(cls):
    region_data = ["강남구", "강서구", "강북구", "은평구", "동작구", "중구"]
    for region in region_data:
        cls.region = RegionModel.objects.create(region_name=region)
    
    cls.region.save()
    
    cls.user = UserModel.objects.create(
        username="user10",
        password=make_password("1010abc!"),
        fullname="user10",
        email="user10@gmail.com",
        phone="010-1010-1010",
        region=RegionModel.objects.get(id=3))
    
    cls.user_1 = UserModel.objects.create(
        username="user30",
        password=make_password("3030abc!"),
        fullname="user30",
        email="user30@gmail.com",
        phone="010-3030-3030",
        region=RegionModel.objects.get(id=3))

    cls.client = APIClient()
    cls.login_data = {"username": "user10", "password" : "1010abc!"}

    cls.access_token = cls.client.post(reverse("ipark_token"), cls.login_data).data["access"]
  • 회원정보 수정은 아래의 조건을 염두에 두고 테스트를 하였다.
- 회원정보를 수정할 때, 두 가지의 경우로 나누어진다. 
	1. 비밀번호를 제외한 정보들만 변경할 때
	2. 비밀번호까지 전부 다 변경할 떄

- 회원정보를 변경할 때 partial=True로 인해 변경하고 싶은 정보만 변경할 수 있다. 
    - 빈 값을 넣으면 기본 validator에 의해 걸러지기 때문에 원래 가지고 있던 값을 넣어줘야 한다. 
    
* region의 경우, 프론트에서 기본값이 강남구로 되어 있기 때문에 없을 경우는 따로 테스트하지 않음
  • 프론트에서 데이터를 보낼 때도 아예 위의 두 가지 케이스로 나누어 보내기 때문에 특별한 예외 상황은 발생할 수 없다고 생각한다.
  • 회원정보 수정의 모든 코드는 위에서 진행했던 방식과 똑같은 흐름을 가지고 있다. 하지만 회원정보 수정을 하면서 코드의 잘못된 부분을 수정하였고, 그 부분을 작성해 보겠다.
# password가 중복될 때
def test_modify_same_password(self):
    url = reverse("user_view")
    data_for_change = {
        "username" : "user10",
        "password" : "1010abc!",
        "fullname" : "user10",
        "email" : "user10@gmail.com",
        "phone" : "010-1010-1010",
        "region" : 4
    }
    
    response = self.client.put(
        path=url, 
        data=data_for_change,
        HTTP_AUTHORIZATION=f"Bearer {self.access_token}"
    )

    self.assertEqual(response.status_code, 400)
    self.assertEqual(response.data["password"], "현재 사용중인 비밀번호와 동일한 비밀번호는 입력할 수 없습니다.")
  • 위의 코드를 작성하며 처음에 에러 메세지가 제대로 오지 않아 serializer를 확인해 보았고, 코드는 문제없이 돌아갔지만 의미가 잘못된 것을 볼 수 있었다. 따라서 아래와 같이 수정하였다.
    • 주석에 작성되어 있는 것과 같이 작성되어 있었는데(아래 조건문 부분은 생략), 코드의 의미는 validator를 통해 검증된 데이터와 validator를 통해 검증된 데이터를 비교하는 것이었다.
    • 제대로 된 코드였다면 사용자의 db에 있는 값인 instance와 사용자가 수정하기 위해 입력한 데이터인 validated_data를 비교해야 된다. 따라서 그 부분을 수정할 수 있었다.
def update(self, instance, validated_data):
  for key, value in validated_data.items():
      if key == "password":
      	  # user = UserModel.objects.get(Q(username=validated_data["username"]) & Q(email=validated_data["email"]))
          user = instance
          if check_password(value, user.password):
              raise serializers.ValidationError(
                  detail={"password": "현재 사용중인 비밀번호와 동일한 비밀번호는 입력할 수 없습니다."})
          else:
              instance.set_password(value)
          continue
      setattr(instance, key, value)
  instance.save()

  return instance
  • 회원탈퇴의 경우, 탈퇴는 항상 성공적으로 진행되었지만 테스트 코드 안에서 테스트 db에서 사용자가 삭제되는 것을 어떻게 확인해야할지 몰라 status_code만 작성하였다.
# 회원탈퇴 테스트
def test_delete_user(self):
    url = reverse("user_view")
        
    response = self.client.delete(url, HTTP_AUTHORIZATION=f"Bearer {self.access_token}")

    self.assertEqual(response.data["message"], "회원탈퇴 성공")

아이디 찾기

  • 아이디를 찾기 위해 필요한 값은 이메일핸드폰 번호이다.
  • 따라서 값의 유무와 더불어 데이터의 유효성까지 같이 검증해 보았다.
# 아이디 찾기 테스트
class SearchUsernameTest(APITestCase):
    @classmethod
    def setUpTestData(cls):
        user_data = {
            "username" : "user10",
            "password" : "1010abc!",
            "fullname" : "user10",
            "email" : "user10@gmail.com",
            "phone" : "010-1010-1010"
        }
        cls.user = UserModel.objects.create(**user_data)
        
    # 이메일과 핸드폰 번호를 제대로 입력한 경우
    def test_search_username(self):
        url = reverse("myid_view")
        data = {
            "email" : "user10@gmail.com",
            "phone" : "010-1010-1010"
        }
        
        response = self.client.post(url, data)
        
        self.assertEqual(response.data["username"], "user10")
        
    # 이메일을 입력하지 않은 경우
    def test_search_username_no_email(self):
        url = reverse("myid_view")
        data = {
            "email" : "",
            "phone" : "010-1010-1010"
        }
        
        response = self.client.post(url, data)
        
        self.assertEqual(response.status_code, 404)
        self.assertEqual(response.data["message"], "사용자가 존재하지 않습니다")
        
    # 이메일을 제대로 입력하지 않은 경우
    def test_search_username_no_email(self):
        url = reverse("myid_view")
        data = {
            "email" : "user10@gmail.",
            "phone" : "010-1010-1010"
        }
        
        data_2 = {
            "email" : "user10",
            "phone" : "010-1010-1010"
        }
        
        response = self.client.post(url, data)
        response_2 = self.client.post(url, data_2)
        
        self.assertEqual(response.status_code, 404)
        self.assertEqual(response.data["message"], "사용자가 존재하지 않습니다")
        self.assertEqual(response_2.status_code, 404)
        self.assertEqual(response_2.data["message"], "사용자가 존재하지 않습니다")
        
    # 핸드폰 번호를 입력하지 않은 경우
    def test_search_username_no_phone(self):
        url = reverse("myid_view")
        data = {
            "email" : "user10@gmail.com",
            "phone" : ""
        }
        
        response = self.client.post(url, data)
        
        self.assertEqual(response.status_code, 404)
        self.assertEqual(response.data["message"], "사용자가 존재하지 않습니다")

비밀번호 변경

  • 비밀번호 변경은 로그인 창에서 사용자가 비밀번호를 기억하지 못할 때 사용할 수 있는 기능이다. 따라서 로그인은 필요하지 않다.
  • 비밀번호를 변경하기 위해서는 2 가지의 단계를 밟아야 한다.
    • 비밀번호 변경 자격 확인
    • 변경할 비밀번호 입력
  • 테스트 db를 만들 때 중요한 점은 비밀번호를 해싱해야 하는 것이다.
  • django의 특성 상 비밀번호를 해싱하지 않으면 일반 문자열로 저장되어 로그인이 되지 않을 뿐더러 비밀번호 비교에 사용되는 check_password를 통과하지 못한다.
# 비밀번호 변경 테스트
class AlterPasswordTest(APITestCase):
    @classmethod
    def setUpTestData(cls):
        user_data = {
            "username" : "user10",
            "fullname" : "user10",
            "email" : "user10@gmail.com",
            "phone" : "010-1010-1010"
        }
        cls.user = UserModel.objects.create(**user_data)
        cls.user.set_password("1010abc!")
        cls.user.save()
    
    # 비밀번호를 변경할 자격이 있는지 확인
    def test_post_user_info(self):
        url = reverse("alter_password_view")
        user_data = {
            "username" : "user10",
            "email" : "user10@gmail.com"
        }
        
        response = self.client.post(url, user_data)
        
        self.assertEqual(response.data["username"], "user10")
        
    # 비밀번호를 변경할 자격이 없는 사람일 경우
    def test_post_wrong_user_info(self):
        url = reverse("alter_password_view")
        user_data = {
            "username" : "user30",
            "email" : "user30@gmail.com"
        }
        
        response = self.client.post(url, user_data)

        self.assertEqual(response.status_code, 404)
        self.assertEqual(response.data["message"], "존재하지 않는 사용자입니다.")
    
    # username 또는 email을 잘못 입력할 경우
    def test_post_wrong_username_or_email(self):
        url = reverse("alter_password_view")
        user_data = {
            "username" : "user30",
            "email" : "user10@gmail.com"
        }
        user_data_2 = {
            "username" : "user10",
            "email" : "user30@gmail.com"
        }
        user_data_3 = {
            "username" : "user10",
            "email" : "user10@gmail"
        }
        
        response = self.client.post(url, user_data)
        response_2 = self.client.post(url, user_data_2)
        response_3 = self.client.post(url, user_data_3)

        self.assertEqual(response.status_code, 404)
        self.assertEqual(response.data["message"], "존재하지 않는 사용자입니다.")
        self.assertEqual(response_2.status_code, 404)
        self.assertEqual(response_2.data["message"], "존재하지 않는 사용자입니다.")
        self.assertEqual(response_3.status_code, 400)
        self.assertEqual(response_3.data["message"], "이메일 형식에 맞게 작성해주세요.")
        
    # username 또는 email을 입력하지 경우
    def test_post_no_username_or_email(self):
        url = reverse("alter_password_view")
        user_data = {
            "username" : "",
            "email" : "user10@gmail.com"
        }
        user_data_2 = {
            "username" : "user10",
            "email" : ""
        }
        
        response = self.client.post(url, user_data)
        response_2 = self.client.post(url, user_data_2)

        self.assertEqual(response.status_code, 400)
        self.assertEqual(response.data["message"], "아이디 또는 이메일 값을 제대로 입력해주세요.")
        self.assertEqual(response_2.status_code, 400)
        self.assertEqual(response_2.data["message"], "아이디 또는 이메일 값을 제대로 입력해주세요.")
        
    # 비밀번호 변경    
    def test_alter_password(self):
        url = reverse("alter_password_view")
        password_data = {
            "username" : "user10",
            "email" : "user10@gmail.com",
            "new_password" : "abcde10!",
            "rewrite_password" : "abcde10!"
        }
        
        response = self.client.put(url, password_data)
        
        self.assertEqual(response.data["message"], "비밀번호 변경이 완료되었습니다! 다시 로그인해주세요.")
        
    # 두 비밀번호가 일치하지 않을 경우    
    def test_alter_password_not_same_password(self):
        url = reverse("alter_password_view")
        password_data = {
            "username" : "user10",
            "email" : "user10@gmail.com",
            "new_password" : "abcde10!",
            "rewrite_password" : "abcde20!"
        }
        password_data_2 = {
            "username" : "user10",
            "email" : "user10@gmail.com",
            "new_password" : "abcde10!",
            "rewrite_password" : ""
        }
        password_data_3 = {
            "username" : "user10",
            "email" : "user10@gmail.com",
            "new_password" : "",
            "rewrite_password" : "abcde10!"
        }
        
        response = self.client.put(url, password_data)
        response_2 = self.client.put(url, password_data_2)
        response_3 = self.client.put(url, password_data_3)
        
        self.assertEqual(response.data["message"], "두 비밀번호가 일치하지 않습니다.")
        self.assertEqual(response_2.data["message"], "비밀번호를 제대로 입력해주세요.")
        self.assertEqual(response_3.data["message"], "비밀번호를 제대로 입력해주세요.")
        
    # 비밀번호 양식이 틀린 경우    
    def test_alter_wrong_password(self):
        url = reverse("alter_password_view")
        password_data = {
            "username" : "user10",
            "email" : "user10@gmail.com",
            "new_password" : "ab10!",
            "rewrite_password" : "ab10!"
        }
        
        response = self.client.put(url, password_data)
        
        self.assertEqual(response.data["message"], "비밀번호를 양식에 맞게 작성해주세요.")
        
    # 이전과 동일한 비밀번호를 입력한 경우    
    def test_alter_same_password(self):
        url = reverse("alter_password_view")
        password_data = {
            "username" : "user10",
            "email" : "user10@gmail.com",
            "new_password" : "1010abc!",
            "rewrite_password" : "1010abc!"
        }
        
        response = self.client.put(url, password_data)

        self.assertEqual(response.data["message"], "현재 사용중인 비밀번호와 동일한 비밀번호는 입력할 수 없습니다.")

계정관리 페이지 접근 권한 확인

  • 사용자가 자신의 정보를 수정하기 위해서는 권한을 인증해야 한다.
  • 프론트에서는 사용자의 username을 payload에서 꺼내 값을 보여주기 때문에 사용자가 해당 값을 수정하지 않는 이상 username에 대한 에러는 발생하지 않는다.
# 계정관리 페이지 접근 권한 확인 테스트
class UserVerifyTest(APITestCase):
    @classmethod
    def setUpTestData(cls):        
        cls.user = UserModel.objects.create(
            username="user10",
            password=make_password("1010abc!"),
            fullname="user10",
            email="user10@gmail.com",
            phone="010-1010-1010"
        )
        
        cls.client = APIClient()
        cls.login_data = {"username": "user10", "password" : "1010abc!"}
 
        cls.access_token = cls.client.post(reverse("ipark_token"), cls.login_data).data["access"]
    
    # 계정관리 페이지 접근을 할 수 있는 경우
    def test_user_verify(self):
        url = reverse("user_verification_view")
        data = {
            "username" : "user10",
            "password" : "1010abc!"
        }
        
        response = self.client.post(
            path=url, 
            data=data,
            HTTP_AUTHORIZATION=f"Bearer {self.access_token}"
        )

        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.data["email"], "user10@gmail.com")
        self.assertEqual(response.data["phone"], "010-1010-1010")
        
    # 정보를 잘못 입력한 경우
    def test_user_verify_with_wrong_info(self):
        url = reverse("user_verification_view")
        data = {
            "username" : "user20",
            "password" : "1010abc!"
        }
        data_2 = {
            "username" : "user10",
            "password" : "1011abc!"
        }
        
        response = self.client.post(
            path=url, 
            data=data,
            HTTP_AUTHORIZATION=f"Bearer {self.access_token}"
        )
        response_2 = self.client.post(
            path=url, 
            data=data_2,
            HTTP_AUTHORIZATION=f"Bearer {self.access_token}"
        )
        
        self.assertEqual(response.status_code, 404)
        self.assertEqual(response.data["message"], "비밀번호가 일치하지 않습니다.")
        self.assertEqual(response_2.status_code, 404)
        self.assertEqual(response_2.data["message"], "비밀번호가 일치하지 않습니다.")
        
    # 정보를 입력하지 않은 경우
    def test_user_verify_with_wrong_info(self):
        url = reverse("user_verification_view")
        data = {
            "username" : "",
            "password" : "1010abc!"
        }
        data_2 = {
            "username" : "user10",
            "password" : ""
        }
        
        response = self.client.post(
            path=url, 
            data=data,
            HTTP_AUTHORIZATION=f"Bearer {self.access_token}"
        )
        response_2 = self.client.post(
            path=url, 
            data=data_2,
            HTTP_AUTHORIZATION=f"Bearer {self.access_token}"
        )
        
        self.assertEqual(response.status_code, 400)
        self.assertEqual(response.data["message"], "아이디를 제대로 입력해주세요.")
        self.assertEqual(response_2.status_code, 400)
        self.assertEqual(response_2.data["message"], "비밀번호 값을 제대로 입력해주세요.")

park app

  • park app에서 내가 작성한 테스트 코드는 아래와 같다.
    • 공원 검색, 토글 공원 리스트, 인기순 공원 나열

공원 검색

  • 공원 검색은 총 네 가지의 경우로 이루어진다.
    • 공원 이름 검색
    • 공원 옵션 검색
    • 공원 지역 검색
    • 공원 옵션 & 공원 지역 검색
  • 해당 테스트 db 작성에 엄청난 시간이 들었다. 공원의 경우, 공원과 공원 옵션이 ManyToMany 관계로 이루어져 있기 때문이다.
  • 맨 처음 db를 짤 때는 bulk_create 기능을 사용해 공원 테스트 db를 작성했었다. 하지만 이렇게 만들게 되면 공원 옵션을 한 번에 여러 개 생성할 수 있어 좋지만 정작 공원의 테스트 db를 불러와보면 옵션 값이 None으로 불러와졌다.
  • 따라서 아래와 같이 through table을 사용하는 방식으로 공원 db를 작성하였다.
def create_park(self, park_data, option_list):
    # optionmodel 생성
    self.options = ["조경", "운동", "놀이공원", "역사", "학습테마", "교양", "편익", "주차장"]
    for option in self.options:
        self.option_model = OptionModel.objects.create(option_name=option)

    self.option_model.save()
    
    self.park = ParkModel.objects.create(**park_data)

    # through table을 활용해 공원과 옵션값을 매치시켜줌
    for option in option_list:
        ParkOptionModel.objects.create(park=self.park, option=OptionModel.objects.get(id=option))

    self.park.save()

@classmethod
def setUpTestData(cls):        
    park_data_1 = {
        "park_name": "서울대공원",
        "zone": "과천시",
        "image": "1",
        "check_count": "5"
    }
    option_list_1 = [1, 2, 3]
    
    park_data_2 = {
        "park_name": "남산공원",
        "zone": "중구",
        "image": "2",
        "check_count": "10"
    }
    option_list_2 = [1, 2, 4, 5, 7, 8]
    
    park_data_3 = {
        "park_name": "간데메공원",
        "zone": "동대문구",
        "image": "3",
        "check_count": "1"
    }
    option_list_3 = [1, 2, 7]

    cls.create_park(cls, park_data_1, option_list_1)
    cls.create_park(cls, park_data_2, option_list_2)
    cls.create_park(cls, park_data_3, option_list_3)
  • 위에서 나열한 네 가지 방식을 성공과 실패 케이스를 나눠 작성해보았다.
# 공원 옵션만 들어올 경우(성공)
def test_option_find(self):
    url = reverse("park_search")
    response = self.client.get(f"{url}?param=1&param=2&param=3")
    response_2 = self.client.get(f"{url}?param=4&param=8")
    response_3 = self.client.get(f"{url}?param=7")

    self.assertEqual(response.data[0]["park_name"], "서울대공원")
    self.assertEqual(response.data[1]["park_name"], "남산공원")
    self.assertEqual(response.data[2]["park_name"], "간데메공원")
    self.assertEqual(response_2.data[0]["park_name"], "남산공원")
    self.assertEqual(response_3.data[0]["park_name"], "남산공원")
    self.assertEqual(response_3.data[1]["park_name"], "간데메공원")
    
# 공원 옵션만 들어올 경우(실패)
def test_option_find_fail(self):
    url = reverse("park_search")
    # 전체 옵션 리스트에 아예 없는 옵션
    response = self.client.get(f"{url}?param=9")
    # 전체 옵션 리스트에는 있지만 공원들이 갖고 있지 않은 옵션
    response_1 = self.client.get(f"{url}?param=6")

    self.assertEqual(response.status_code, 404)
    self.assertEqual(response_1.data["message"], "공원을 찾을 수 없습니다.")

# 공원 지역만 들어올 경우(성공)
def test_zone_find(self):
    url = reverse("park_search")
    response = self.client.get(f"{url}?param=과천시")
    response_2 = self.client.get(f"{url}?param=중구")
    response_3 = self.client.get(f"{url}?param=동대문구")
    
    self.assertEqual(response.data[0]["park_name"], "서울대공원")
    self.assertEqual(response_2.data[0]["park_name"], "남산공원")
    self.assertEqual(response_3.data[0]["park_name"], "간데메공원")

# 공원 지역만 들어올 경우(실패)
def test_zone_find_fail(self):
    url = reverse("park_search")
    response = self.client.get(f"{url}?param=강남구")
    response_2 = self.client.get(f"{url}?param=은평구")
    response_3 = self.client.get(f"{url}?param=강서구")

    self.assertEqual(response.status_code, 404)
    self.assertEqual(response_2.status_code, 404)
    self.assertEqual(response_3.data["message"], "공원을 찾을 수 없습니다.")
    
# 공원 옵션과 지역 모두 들어올 경우(성공)
def test_option_and_zone_find(self):
    url = reverse("park_search")
    response = self.client.get(f"{url}?param=2&param=과천시")
    response_2 = self.client.get(f"{url}?param=4&param=중구")
    response_3 = self.client.get(f"{url}?param=1&param=7&param=동대문구")
    
    self.assertEqual(response.data[0]["park_name"], "서울대공원")
    self.assertEqual(response_2.data[0]["park_name"], "남산공원")
    self.assertEqual(response_3.data[0]["park_name"], "간데메공원")

# 공원 옵션과 지역 모두 들어올 경우(실패)
def test_option_and_zone_find_fail(self):
    url = reverse("park_search")
    response = self.client.get(f"{url}?param=6&param=과천시")
    response_2 = self.client.get(f"{url}?param=3&param=중구")
    response_3 = self.client.get(f"{url}?param=3&param=6&param=동대문구")

    self.assertEqual(response.status_code, 404)
    self.assertEqual(response_2.status_code, 404)
    self.assertEqual(response_3.data["message"], "공원을 찾을 수 없습니다.")

# 공원 이름으로 검색(성공)    
def test_park_by_name(self):
    url = reverse("park_search")
    response = self.client.get(f"{url}?param=서")
    response_2 = self.client.get(f"{url}?param=원")

    self.assertEqual(response.data[0]["park_name"], "서울대공원")
    self.assertEqual(response_2.data[0]["park_name"], "서울대공원")
    self.assertEqual(response_2.data[1]["park_name"], "남산공원")
    self.assertEqual(response_2.data[2]["park_name"], "간데메공원")
    
# 공원 이름으로 검색(실패)    
def test_park_by_name_fail(self):
    url = reverse("park_search")
    response = self.client.get(f"{url}?param=탑골공원")
    response_2 = self.client.get(f"{url}?param=상도근린공원")

    self.assertEqual(response.status_code, 404)
    self.assertEqual(response_2.data["message"], "공원을 찾을 수 없습니다.")

토글 공원 리스트

  • 내가 만든 프로젝트의 오른쪽 상단에는 공원의 전체 리스트를 볼 수 있도록 토글을 만들어 놓았다.
  • 맨 처음 프로젝트를 만들었을 당시에는 그저 가나다 순으로 공원들을 전부 나열했었는데, 사용자 피드백에서 효율적이지 못하다는 피드백을 받았고 이를 공원 맨 앞글자 초성을 사용해 공원을 분리하는 방식으로 수정했다.
  • 위의 방식을 테스트하는 것으로 코드를 리팩토링하였고, 해당 테스트 코드에서 참조하는 view는 get 요청으로 공원 데이터를 불러다 프론트에 전송하기만 하므로 실패하는 케이스가 존재하지 않는다.
  • 따라서 아래와 같이 간단히 작성해보았다.
# 토글 공원 리스트 테스트
class ToggleParkListTest(APITestCase):
    def create_park(self, park_data):        
        self.park = ParkModel.objects.create(**park_data)

        self.park.save()

    @classmethod
    def setUpTestData(cls):        
        park_data_1 = {
            "park_name": "서울대공원",
            "zone": "과천시",
            "addr_dong": "ㄱ",
            "image": "1",
            "check_count": "5"
        }
        
        park_data_2 = {
            "park_name": "남산공원",
            "zone": "중구",
            "addr_dong": "ㅈ",
            "image": "2",
            "check_count": "10"
        }
        
        park_data_3 = {
            "park_name": "간데메공원",
            "zone": "동대문구",
            "addr_dong": "ㄷ",
            "image": "3",
            "check_count": "1"
        }
        
        park_data_4 = {
            "park_name": "효창공원",
            "zone": "용산구",
            "addr_dong": "ㅇ",
            "image": "4",
            "check_count": "0"
        }

        cls.create_park(cls, park_data_1)
        cls.create_park(cls, park_data_2)
        cls.create_park(cls, park_data_3)
        cls.create_park(cls, park_data_4)
                
    def test_get_toggle_list(self):
        url = reverse("toggle_park")
        response = self.client.post(url,{"data": "ㄱ"})
        response_2 = self.client.post(url,{"data": "ㅈ"})
        response_3 = self.client.post(url,{"data": "ㄷ"})
        response_4 = self.client.post(url,{"data": "ㅇ"})
        
        self.assertEqual(response.data[0]["park_name"], "서울대공원")
        self.assertEqual(response_2.data[0]["park_name"], "남산공원")
        self.assertEqual(response_3.data[0]["park_name"], "간데메공원")
        self.assertEqual(response_4.data[0]["park_name"], "효창공원")

인기순 공원 나열

  • 사용자들이 조회를 많이 한 공원이 맨 앞으로 나오게 해 공원을 나열하는 기능에 대한 테스트 코드이다.
  • 해당 부분도 단순한 get 요청이고 별다른 조건이 걸려있지 않았다. 특징을 꼽으라면 조회수가 0인 공원들은 나열하지 않는다는 것이다.
  • 하지만 이 부분도 filter(check_count__gte=1).order_by("-check_count")를 사용해 단순하게 필터링하는 방식이라 에러가 날 부분이 없다.
  • 테스트 db에서 조회수가 0인 효창공원은 assertEqual에 존재하지 않는 것을 볼 수 있다.
# 인기순 공원 검색 테스트
class ParkPopularityTest(APITestCase):
    def create_park(self, park_data):        
        self.park = ParkModel.objects.create(**park_data)

        self.park.save()

    @classmethod
    def setUpTestData(cls):        
        park_data_1 = {
            "park_name": "서울대공원",
            "zone": "과천시",
            "image": "1",
            "check_count": "5"
        }
        
        park_data_2 = {
            "park_name": "남산공원",
            "zone": "중구",
            "image": "2",
            "check_count": "10"
        }
        
        park_data_3 = {
            "park_name": "간데메공원",
            "zone": "동대문구",
            "image": "3",
            "check_count": "1"
        }
        
        park_data_4 = {
            "park_name": "효창공원",
            "zone": "용산구",
            "image": "4",
            "check_count": "0"
        }

        cls.create_park(cls, park_data_1)
        cls.create_park(cls, park_data_2)
        cls.create_park(cls, park_data_3)
        cls.create_park(cls, park_data_4)
    
    def test_popularity_park(self):
        url = reverse("park_popularity")
        response = self.client.get(url)

        self.assertEqual(response.data[0]["park_name"], "남산공원")
        self.assertEqual(response.data[1]["park_name"], "서울대공원")
        self.assertEqual(response.data[2]["park_name"], "간데메공원")
profile
https://github.com/nikevapormax

0개의 댓글