Today I Learned#
2024년 5월 24일 금요일
테스트 코드 작성을 위한 DB 모델링#
우리 서비스의 테스트 코드는 pytest로 작성되어 있고, 데이터베이스는 SQLite다. SQLite는 파일 형식이라 만들고 뚝딱 삭제해버리면 금방이다. 우리 데이터베이스인 Postgres 와 마찬가지로 SQL이긴 하나 약간의 기능 차이는 존재한다. 하나 꼽자면 jsonb 타입의 필드가 없다. jsonb는 json과 달리 내부의 키로 검색할 수 있는 나름 유용한 타입이다.
쇼피파이에서 넘어오는 스토어 데이터는 Shop 테이블에 담긴다. 가공없이 그대로. 스토어와 관련해서 우리가 필요하고 자주쓰는 데이터는 이 자식테이블 격인 shop_detail에 담긴다. 여기에 apps_log라는 이름으로, 그 스토어가 활성화한 서비스가 어떤 것인지를 담는데 이 필드가 jsonb 타입이다.
이번 스프린트로 새로운 앱이 추가되면서, 사용중인 서비스 뿐만 아니라 설치한 앱에 관한 데이터 처리도 복잡하고 중요해졌다. 현재 테스트코드에 이 부분이 없어 꼭 추가하고 싶었다. 어떤 서비스를 설치할 때 필요한 것이 모두 제대로 생성되는지, 중복은 없는지 확인이 필요하다. 이번 QA 때 온갖 곳에서 문제가 터지는 걸 경험하고 꼭 만들어야 겠다 다짐했다.
본론으로 돌아와, SQLite에는 JSONB 타입이 없어서 기존에 작성되있는 데이터베이스 모델링을 그대로 쓸수가 없다. 따라서,
- 기존 데이터베이스 모델링대로 만들되
- 필드에 JSONB 타입을 사용하는 친구들은 제외시키고
- 이 친구들은 JSONB를 JSON으로 바꾸어 별도로 정의해서
- 추가해준다.
로 처리했다. 말이야 뭐 쉽지만.. 문제는 shop_detail이 shop과 연관이 있는 테이블이라는 것이고 별도로 정의해서 처리하다보니 이 관계가 제대로 생성되지 않아 오류처리됐다. 더 정확히는 Factory로 테스트용 가짜 데이터를 생성하는 과정에서 오류가 발생했다. shop에 달려있지 않은 shop_detail이 제대로 테스트 될리가 없으니 꼭 해결해야 하는 문제였다.
온갖짓을 다해봤지만 안됐다. 결국 이에 딸린 Shop 테이블도 테스트 파일에 따로 정의해야 했다. 그러고 난 후에는 shop 테이블에 딸린 또다른 테이블인 shop_service_email_history라는 테이블이 없어서 난리가 났다. 결국 애도 따로 정의했다. 테스트코드를 위해 따로 정의하는게 맞나…
사실 JSONB를 사용하게 된 가장 큰 이유는 키값으로 내부 데이터를 검색할 수 있기 떄문이다. JSONB 타입으로 선언된 필드에 “A”라는 키를 가진 데이터를 찾을 수 있다. 그 외의 연산도 가능하다. 하지만 왜인지 현재 코드 내에 작성하는 쿼리에선 안되고 직접 pgadmin4로 쿼리 입력할때만 된다. JSON보다 나은 장점은 쓰지도 못하고, JSONB라서 단점만 있으니 골칫거리다. 바꾸던가, 검색이 되게 하던가 해야할듯
Faker#
대상혁. 사실 파이썬 라이브러리 중 하나로 테스트를 위한 가짜 데이터를 만들어준다. 임의의 문자열, 쉇자도 가능하고 임의의 주소나 휴대폰번호, 이메일, 회사주소도 가능하다. 이 값을 이용해서 임의의 데이터를 반복적으로 만들어주는게 Factory_boy다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| # 초기화
Faker = Factory.create
fake = Faker()
seed_value = random.randint(0, 10000)
fake.seed(seed_value)
# seed_value 가 같은 데이터는 같은 가짜데이터를 가진다. 예를 들어,
fake = Faker()
fake.seed(4321)
print(fake.name()) # '이상혁'
print(fake.nickname()) # 'GOAT'
fake.seed(4321)
print(fake.name()) # '이상혁'
print(fake.nickname()) # 'GOAT'
fake.seed(1111)
print(fake.name()) # '메시'
print(fake.nickname()) # '메갓'
|
아래는 샵테이블 모델링의 일부다.
1
2
3
4
5
6
7
8
9
10
11
| class Shop(Base):
__tablename__ = "shop"
id = Column(Integer, primary_key=True, index=True)
platform_id = Column(Integer, index=True)
company_name = Column(String(125), index=True, unique=True)
shop_url = Column(String(125), index=True, unique=True)
shop_name = Column(Text, index=True)
platform_shop_url = Column(String(125), index=True, unique=True)
owner_email = Column(Text)
currency = Column(String(3))
iana_timezone = Column(String(31))
|
샵팩토리는 가짜 shop 데이터를 만들어준다. 정의는 다음과 같다.
1
2
3
4
5
6
7
8
9
10
11
12
| class ShopFactory(factory.alchemy.SQLAlchemyModelFactory):
class Meta:
model = Shop
sqlalchemy_session = Session
shop_name = factory.Faker("domain_word")
company_name = factory.Faker("company")
shop_url = factory.Faker("url")
owner_email = factory.Faker("safe_email")
currency = factory.Faker("currency_symbol")
country_code = fake.country_code()
iana_timezone = factory.Faker("timezone")
|
다양한 형식에 맞게 가짜 데이터를 만들어준다. 사용방법은
1
2
3
4
5
6
7
8
9
10
11
12
13
| def set_shop_factory_session(db: Session):
ShopFactory._meta.sqlalchemy_session = db
class TestShop:
@pytest.mark.asyncio
def test_get_shop(self):
with TestingSessionWriteLocal() as db:
set_shop_factory_session(db)
shop = ShopFactory.create()
db.add(shop)
db.commit()
db.refresh(shop)
assert shop.id
|
이런식으로 처리하면 된다. 나는 Shop 테이블에 딸려있는 shop_detail 데이터를 추가로 만들었다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| class ShopDetailFactory(factory.alchemy.SQLAlchemyModelFactory):
class Meta:
model = ShopDetail
sqlalchemy_session = Session
shop = factory.SubFactory(ShopFactory)
store_name = factory.Faker("domain_word")
company_name = factory.Faker("company")
industry = factory.Faker("company")
store_name = factory.Faker("company")
store_url = factory.Faker("url")
shop_owner = factory.Faker("name")
email = factory.Faker("safe_email")
phone = factory.Faker("phone_number")
address = factory.Faker("address")
customer_email = factory.Faker("safe_email")
store_logo = factory.Faker("image_url")
is_dropshipper = factory.Faker("pybool")
apps_log = {}
|
저 shop = factory.SubFactory(ShopFactory) 가 이 shop_detail과 관련된 shop을 만들어주는 부분이다. 이때 관계된 shop 데이터도 자동으로 만들어진다. 따라서
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| class TestShop:
@pytest.mark.asyncio
def test_get_shop(self):
with TestingSessionWriteLocal() as db:
set_shop_factory_session(db)
shop = ShopFactory.create()
shop_detail = ShopDetailFactory.create()
db.add(shop)
db.add(shop_detail
db.commit()
stmt = select(models.Shop).where(models.Shop.id > 0)
shops = db.execute(stmt).scalars().all()
assert len(shops) == 1
|
라고 작성하면 테스트가 실패한다. shop을 만들면서 1개, shop_detail을 만들면서 1개 총 2개가 만들어지기 떄문이다.
별도로 테이블 정의하지 않는 방법, 결과를 개발자가 아니더라도 확인하는 방법에 대해 고민 필요..