Introduction
안녕하세요. 1편과 2편에 이어서 마지막 편으로 Django에서 DB Transaction과 관련된 코드들의 테스트 코드를 작성하는 방법에 대해서 포스팅 해보려고 합니다.
아직 1편과 2편을 읽지 않으신 분이 계신다면 먼저 읽고 오시는 것을 추천드립니다. Django에서 DB Transaction을 어떻게 사용하는지 그리고 실제 DB에서 어떤 동작이 일어나는지를 이해하시고 이번 글을 읽으시면 이해하기 쉽고 더 도움이 될 거라고 생각합니다.
지난 글
Django DB Transaction(트랜잭션) 1편 - Request와 DB Transaction 묶기(Feat. ATOMIC_REQUESTS)
Django DB Transaction 2편 - 명시적으로 transaction 활용하기. (feat. savepoint)
그럼 본격적으로 1,2편에서 살펴본 DB Transaction과 관련된 기능들을 어떻게 테스트할지에 대한 이야기를 시작해보도록 하겠습니다.
*주의 이 글은 Django4.0 및 MySQL5.7 기준의 글입니다.*
Django's TestCase
Django에서 지원하는 TestCase는 아래와 같은 Hierarchy를 가지고 있습니다.
우리가 집중적으로 볼 부분은 Transaction과 관련된 부분입니다. 그렇기 때문에 Database에 관련된 기능을 구현한 TransactionTestCase와 TransactionTestCase를 상속받은 TestCase가 transaction과 관련해서 어떻게 동작하는지 살펴볼 것 입니다. TransactionTestCase와 TestCase는 테스트 간의 데이터를 reset하는 방법을 제외하고는 동일합니다.
TransactionTestCase
- 테스트 수행 후에 모든 테이블을 truncate 합니다.
- 테스트에서 Transaction의 commit이나 rollback을 관찰 할 수 있다.
- 단점은 각 테스트 간의 모든 테이블 지워야 하므로 테스트 시간이 오래 걸린다.
TestCase
- 테스트 수행 후에 모든 테이블을 truncate하지 않고, 각 test를 transaction으로 처리하여 test 수행 후에 transaction을 rollback합니다. 우리가 ATOMIC_REQUESTS를 사용하여 request handler의 시작 전에 transaction을 시작하고, response를 반환 후에 commit or rollback을 하던 것 과 같은 전략입니다. 즉, TestCase안에서는 tranaction.atomic block이나 atomic_request 설정이 savepoint로 동작하게 됩니다. 이 방법의 장점은 성능입니다. (truncate보다 transaction rollback이 빠릅니다.)
-> 하지만, savepoint가 있기 때문에 같은 DB connection의 transaction 안에서는 부분별로 transaction이 잘 동작하는지 확인할 수도 있습니다. 아래 예제 코드를 통해서 살펴볼 예정입니다. 하지만, 어쨌든 실제 Production과는 다르게 동작하므로 이 부분은 주의해야 될 것 같습니다.
예제 코드로 실제 동작 살펴보기
1,2 편에서 사용했던 코드를 계속 사용하겠습니다.
먼저 models.py, serializers.py, views.py 코드입니다. 아래 코드는 안 보셔도 되고 다만 아래와 같은 코드를 테스트하는 것이 때문에 참고만 하시면 됩니다.
# models.py
from django.db import models
from django.contrib.auth.models import User
class Company(models.Model):
ticker = models.CharField(max_length=16)
name = models.CharField(max_length=64)
class Account(models.Model):
owner = models.ForeignKey(User, on_delete=models.PROTECT, db_constraint=False)
created_dt = models.DateTimeField(auto_now_add=True)
updated_dt = models.DateTimeField(auto_now=True)
def __repr__(self):
return f"{self.owner} Account"
class Shares(models.Model):
account = models.ForeignKey(Account, on_delete=models.PROTECT, db_constraint=False)
company = models.ForeignKey(Company, on_delete=models.CASCADE, db_constraint=False)
amount = models.PositiveBigIntegerField()
created_dt = models.DateTimeField(auto_now_add=True)
updated_dt = models.DateTimeField(auto_now=True)
class Meta:
unique_together = [['account', 'company']]
class SharesTransfer(models.Model):
account_from = models.ForeignKey(
Account, on_delete=models.PROTECT, db_constraint=False, related_name='account_from'
)
account_to = models.ForeignKey(
Account, on_delete=models.PROTECT, db_constraint=False, related_name='account_to'
)
company = models.ForeignKey(Company, on_delete=models.PROTECT, db_constraint=False)
amount = models.PositiveBigIntegerField()
created_dt = models.DateTimeField(auto_now_add=True)
updated_dt = models.DateTimeField(auto_now=True)
# serializers.py
from django.db.models import F
from rest_framework import serializers
from .models import Shares, SharesTransfer
class SharesTransferSerializer(serializers.Serializer):
# validation 은 생략
account_from = serializers.IntegerField()
account_to = serializers.IntegerField()
company_id = serializers.IntegerField()
amount = serializers.IntegerField()
def create(self, validated_data, *args, **kwargs):
# 보낸 사람의 주식을 줄인다.
Shares.objects.filter(account_id=validated_data.get('account_from'), company_id=validated_data.get('company_id'))\
.update(amount=F('amount') - validated_data.get('amount'))
# 받은 사람의 주식을 늘린다.
Shares.objects.filter(account_id=validated_data.get('account_to'), company_id=validated_data.get('company_id'))\
.update(amount=F('amount') + validated_data.get('amount'))
# 전송내역을 저장한다.
transfer = SharesTransfer.objects.create(
account_from_id=validated_data.get('account_from'),
account_to_id=validated_data.get('account_to'),
company_id=validated_data.get('company_id'),
amount=validated_data.get('amount')
)
return transfer
# views.py
from rest_framework import viewsets, permissions
from rest_framework.response import Response
from rest_framework import status
from .serializers import SharesTransferSerializer
def send_email():
print('send_email_for_testing')
class ShareTransferViewSet(viewsets.GenericViewSet):
permission_classes = [permissions.IsAuthenticated]
serializer_class = SharesTransferSerializer
lookup_field = 'id'
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
# for transaction test
send_email()
return Response('success', status=status.HTTP_201_CREATED)
위와 같은 API의 integration test를 작성해보겠습니다.
테스트 코드는 아래와 같습니다. 저희가 살펴 볼 테스트는 test_create_fail_with_exception_for_checking_transaction 입니다. 해당 테스트에서는 send_email에서 exception이 발생시켜서 request에 대한 response가 실패하도록 의도적으로 만들고 있습니다.
from unittest.mock import patch
from django.urls import reverse
from django.test import TestCase, TransactionTestCase
from rest_framework import status
from rest_framework.test import APITestCase, APIClient
from django.contrib.auth.models import User
from trading import views
from trading.models import Account, Company, Shares, SharesTransfer
class ShareTransferViewSetTests(TestCase):
@classmethod
def setUpTestData(cls):
super(ShareTransferViewSetTests, cls).setUpTestData()
cls.username1, cls.password1 = 'tester', 'tester1234'
cls.username2, cls.password2 = 'tester2', 'tester12342'
cls.user1 = User.objects.create_user(username=cls.username1, password=cls.password1)
cls.user2 = User.objects.create_user(username=cls.username2, password=cls.password2)
# fixture dataset
cls.company = Company.objects.create(ticker='AMZN', name='amazon')
cls.account1 = Account.objects.create(owner=cls.user1)
cls.account2 = Account.objects.create(owner=cls.user2)
cls.shares1 = Shares.objects.create(account=cls.account1, company=cls.company, amount=100)
cls.shares2 = Shares.objects.create(account=cls.account2, company=cls.company, amount=100)
def setUp(self) -> None:
# for session authenticating
self.client = APIClient()
self.client.login(username=self.username1, password=self.password1)
def test_create_success(self) -> None:
url = reverse('transfer-list')
amount = 10
data = {
"account_from": self.account1.id,
"account_to": self.account2.id,
"company_id": self.company.id,
"amount": amount
}
response = self.client.post(url, data=data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.json(), "success")
@patch.object(views, "send_email")
def test_create_fail_with_exception_for_checking_transaction(self, mock_send_email):
mock_send_email.side_effect = Exception("test error")
url = reverse('transfer-list')
amount = 10
data = {
"account_from": self.account1.id,
"account_to": self.account2.id,
"company_id": self.company.id,
"amount": amount
}
before_shares1_amount = self.shares1.amount
before_shares2_amount = self.shares2.amount
with self.assertRaises(Exception):
self.client.post(url, data=data, format='json')
# checking transaction working well (http response와 묶여서 DB에 반영이 안됨)
self.shares1.refresh_from_db()
self.shares2.refresh_from_db()
self.assertEqual(self.shares1.amount, before_shares1_amount)
self.assertEqual(self.shares2.amount, before_shares2_amount)
이를 통해서 아래와 같이 4가지 상황에 대해서 살펴볼 예정입니다.
- ATOMIC_REQUESTS가 True일 때와 False일 때.
- TestCase를 사용할 때 TransactionTestCase를 사용할 때.
1) TestCase상속, ATOMIC_REQUESTS=True 일 때
TestCase를 사용했기 때문에 test시작 부분에서 autocommit=0으로 transaction을 시작하고, 마지막에 rollback하는 것을 볼 수 있다.
테스트 결과: 성공
Request 자체가 Savepoint로 묶여서 exception이 발생했으므로 rollback되어서 해당 트랜잭션내에서 바뀌지 않은것으로 처리됨. 하지만 실제 코드에서는 Transaction으로 묶이기 때문에 테스트는 성공하지만 실제와 다르게 동작한다는 단점이 있음.
DB에 수행되는 SQL
# test시작되는 부분
151 Query SET autocommit=0
....
# response가 시작되는 부분
151 Query SAVEPOINT `s140327450969920_x6`
151 Query SELECT `auth_user`.`id`, `auth_user`.`password`, `auth_user`.`last_login`, `auth_user`.`is_superuser`, `auth_user`.`username`, `auth_user`.`first_name`, `auth_user`.`last_name`, `auth_user`.`email`, `auth_user`.`is_staff`, `auth_user`.`is_active`, `auth_user`.`date_joined` FROM `auth_user` WHERE `auth_user`.`username` = 'tester' LIMIT 21
151 Query SELECT (1) AS `a` FROM `django_session` WHERE `django_session`.`session_key` = 'wu2y79xbx12kvvfg4u4f2dnbfbzlivf4' LIMIT 1
151 Query SAVEPOINT `s140327450969920_x7`
151 Query INSERT INTO `django_session` (`session_key`, `session_data`, `expire_date`) VALUES ('wu2y79xbx12kvvfg4u4f2dnbfbzlivf4', 'e30:1nFA3t:0qG48fhGDWhOHPkGIEk5g04yU4mhmtwqLp-YIPEIHkk', '2022-02-16 07:28:17.729747')
151 Query RELEASE SAVEPOINT `s140327450969920_x7`
151 Query SELECT `django_session`.`session_key`, `django_session`.`session_data`, `django_session`.`expire_date` FROM `django_session` WHERE (`django_session`.`expire_date` > '2022-02-02 07:28:17.733107' AND `django_session`.`session_key` = 'wu2y79xbx12kvvfg4u4f2dnbfbzlivf4') LIMIT 21
151 Query SELECT (1) AS `a` FROM `django_session` WHERE `django_session`.`session_key` = '3m4sv2d4ib7gqzjq4wxvtltb4a8g9rfg' LIMIT 1
151 Query SAVEPOINT `s140327450969920_x8`
151 Query INSERT INTO `django_session` (`session_key`, `session_data`, `expire_date`) VALUES ('3m4sv2d4ib7gqzjq4wxvtltb4a8g9rfg', 'e30:1nFA3t:0qG48fhGDWhOHPkGIEk5g04yU4mhmtwqLp-YIPEIHkk', '2022-02-16 07:28:17.736853')
151 Query RELEASE SAVEPOINT `s140327450969920_x8`
151 Query SELECT `django_session`.`session_key`, `django_session`.`session_data`, `django_session`.`expire_date` FROM `django_session` WHERE `django_session`.`session_key` = 'wu2y79xbx12kvvfg4u4f2dnbfbzlivf4' LIMIT 21
151 Query DELETE FROM `django_session` WHERE `django_session`.`session_key` IN ('wu2y79xbx12kvvfg4u4f2dnbfbzlivf4')
151 Query UPDATE `auth_user` SET `last_login` = '2022-02-02 07:28:17.743379' WHERE `auth_user`.`id` = 1
151 Query SAVEPOINT `s140327450969920_x9`
151 Query UPDATE `django_session` SET `session_data` = '.eJxVjEEOwiAQRe_C2hARB4pL956BDDODVA0kpV0Z726bdKHb_977bxVxmUtcukxxZHVRRh1-t4T0lLoBfmC9N02tztOY9KbonXZ9ayyv6-7-HRTsZa3zOTAaT-SZA3kBy56TdSk7czJCAg7FBkAYgHII4ehoAOtgNckZoz5fB6c4IA:1nFA3t:xI_WUQANWnZCwZ3_TsPHVojj53iQkEkDCNplsUHeijQ', `expire_date` = '2022-02-16 07:28:17.745177' WHERE `django_session`.`session_key` = '3m4sv2d4ib7gqzjq4wxvtltb4a8g9rfg'
151 Query RELEASE SAVEPOINT `s140327450969920_x9`
151 Query SAVEPOINT `s140327450969920_x10`
151 Query SELECT `django_session`.`session_key`, `django_session`.`session_data`, `django_session`.`expire_date` FROM `django_session` WHERE (`django_session`.`expire_date` > '2022-02-02 07:28:17.751501' AND `django_session`.`session_key` = '3m4sv2d4ib7gqzjq4wxvtltb4a8g9rfg') LIMIT 21
151 Query SELECT `auth_user`.`id`, `auth_user`.`password`, `auth_user`.`last_login`, `auth_user`.`is_superuser`, `auth_user`.`username`, `auth_user`.`first_name`, `auth_user`.`last_name`, `auth_user`.`email`, `auth_user`.`is_staff`, `auth_user`.`is_active`, `auth_user`.`date_joined` FROM `auth_user` WHERE `auth_user`.`id` = 1 LIMIT 21
151 Query UPDATE `trading_shares` SET `amount` = (`trading_shares`.`amount` - 10) WHERE (`trading_shares`.`account_id` = 1 AND `trading_shares`.`company_id` = 1)
151 Query UPDATE `trading_shares` SET `amount` = (`trading_shares`.`amount` + 10) WHERE (`trading_shares`.`account_id` = 2 AND `trading_shares`.`company_id` = 1)
151 Query INSERT INTO `trading_sharestransfer` (`account_from_id`, `account_to_id`, `company_id`, `amount`, `created_dt`, `updated_dt`) VALUES (1, 2, 1, 10, '2022-02-02 07:28:17.760093', '2022-02-02 07:28:17.760164')
151 Query RELEASE SAVEPOINT `s140327450969920_x10`
151 Query ROLLBACK TO SAVEPOINT `s140327450969920_x6`
# response가 끝나는 부분
151 Query RELEASE SAVEPOINT `s140327450969920_x6`
....
# 테스트 끝나는 부분
151 ROLLBACK
151 SET autocommit=1
2) TestCase상속, ATOMIC_REQUESTS=False 일 때
테스트 결과: 실패
해당 트랜잭션내에서 select해서 phantom read하여 바뀐 것 처럼 보임. 하지만 다른 transaction에서 조회했을 때는 그래로이다.(isolation level별로 다르지만, 보통 우리는 READ UNCOMMITED는 사용하지 않기 때문에..)
TestCase class를 사용하면 이런 문제가 있다...(Transaction동작에 대해서 정확히 이해하기 힘듬.)
Error message
FAILED tests/trading/api/test_share_transfer_view_set.py::ShareTransferViewSetTests::test_create_fail_with_exception_for_checking_transaction - AssertionError: 90 != 100
DB에 실행되는 SQL
# Test 시작부분
160 Query SET autocommit=0
160 Query INSERT INTO `auth_user` (`password`, `last_login`, `is_superuser`, `username`, `first_name`, `last_name`, `email`, `is_staff`, `is_active`, `date_joined`) VALUES ('pbkdf2_sha256$260000$bxFgW9t5ASFHwLNKVpnSXZ$ZoX/UECeECQ/MXQ9zGuZhclxegf/17vH+MOK9as3UlY=', NULL, 0, 'tester', '', '', '', 0, 1, '2022-02-02 07:36:52.657499')
160 Query INSERT INTO `auth_user` (`password`, `last_login`, `is_superuser`, `username`, `first_name`, `last_name`, `email`, `is_staff`, `is_active`, `date_joined`) VALUES ('pbkdf2_sha256$260000$crb0qlMQEvlzA8i0y9wLRJ$DHm4UktINgeYHp9txkvTRI5wZ8nrFoXWLnvRypKZ6oc=', NULL, 0, 'tester2', '', '', '', 0, 1, '2022-02-02 07:36:52.898442')
160 Query INSERT INTO `trading_company` (`ticker`, `name`) VALUES ('AMZN', 'amazon')
160 Query INSERT INTO `trading_account` (`owner_id`, `created_dt`, `updated_dt`) VALUES (1, '2022-02-02 07:36:53.127459', '2022-02-02 07:36:53.127497')
160 Query INSERT INTO `trading_account` (`owner_id`, `created_dt`, `updated_dt`) VALUES (2, '2022-02-02 07:36:53.131104', '2022-02-02 07:36:53.131260')
160 Query INSERT INTO `trading_shares` (`account_id`, `company_id`, `amount`, `created_dt`, `updated_dt`) VALUES (1, 1, 100, '2022-02-02 07:36:53.134858', '2022-02-02 07:36:53.135043')
160 Query INSERT INTO `trading_shares` (`account_id`, `company_id`, `amount`, `created_dt`, `updated_dt`) VALUES (2, 1, 100, '2022-02-02 07:36:53.137689', '2022-02-02 07:36:53.137739')
160 Query SAVEPOINT `s139711800366912_x1`
160 Query SELECT `auth_user`.`id`, `auth_user`.`password`, `auth_user`.`last_login`, `auth_user`.`is_superuser`, `auth_user`.`username`, `auth_user`.`first_name`, `auth_user`.`last_name`, `auth_user`.`email`, `auth_user`.`is_staff`, `auth_user`.`is_active`, `auth_user`.`date_joined` FROM `auth_user` WHERE `auth_user`.`username` = 'tester' LIMIT 21
160 Query SELECT (1) AS `a` FROM `django_session` WHERE `django_session`.`session_key` = 'dqnu7uq5edpix55dckwu5qp07osbuxck' LIMIT 1
160 Query SAVEPOINT `s139711800366912_x2`
160 Query INSERT INTO `django_session` (`session_key`, `session_data`, `expire_date`) VALUES ('dqnu7uq5edpix55dckwu5qp07osbuxck', 'e30:1nFACD:_6rx3SAb8VF8GaVx0-0QGMk9tIU97SQ6Nr2F53h4Brw', '2022-02-16 07:36:53.345378')
160 Query RELEASE SAVEPOINT `s139711800366912_x2`
160 Query SELECT `django_session`.`session_key`, `django_session`.`session_data`, `django_session`.`expire_date` FROM `django_session` WHERE (`django_session`.`expire_date` > '2022-02-02 07:36:53.350622' AND `django_session`.`session_key` = 'dqnu7uq5edpix55dckwu5qp07osbuxck') LIMIT 21
160 Query SELECT (1) AS `a` FROM `django_session` WHERE `django_session`.`session_key` = '914zlc01k9aa40ci59gjs3pklewdxd0l' LIMIT 1
160 Query SAVEPOINT `s139711800366912_x3`
160 Query INSERT INTO `django_session` (`session_key`, `session_data`, `expire_date`) VALUES ('914zlc01k9aa40ci59gjs3pklewdxd0l', 'e30:1nFACD:_6rx3SAb8VF8GaVx0-0QGMk9tIU97SQ6Nr2F53h4Brw', '2022-02-16 07:36:53.358372')
160 Query RELEASE SAVEPOINT `s139711800366912_x3`
160 Query SELECT `django_session`.`session_key`, `django_session`.`session_data`, `django_session`.`expire_date` FROM `django_session` WHERE `django_session`.`session_key` = 'dqnu7uq5edpix55dckwu5qp07osbuxck' LIMIT 21
160 Query DELETE FROM `django_session` WHERE `django_session`.`session_key` IN ('dqnu7uq5edpix55dckwu5qp07osbuxck')
160 Query UPDATE `auth_user` SET `last_login` = '2022-02-02 07:36:53.366915' WHERE `auth_user`.`id` = 1
160 Query SAVEPOINT `s139711800366912_x4`
160 Query UPDATE `django_session` SET `session_data` = '.eJxVjDsOwjAQBe_iGlnrT2wvJT1nsNY_HEC2FCcV4u4QKQW0b2bei3na1uq3kRc_J3Zmgp1-t0DxkdsO0p3arfPY27rMge8KP-jg157y83K4fweVRv3W2hYKU0EALVRBJRyCic7lIG0wIVkqjjJhsiCMUpNGqdAUnQs40BLY-wPUyzcf:1nFACD:kJm3I5Tqi0pU0Bwx_P8Yld6sl77R54-h1th4sBmaEWQ', `expire_date` = '2022-02-16 07:36:53.369080' WHERE `django_session`.`session_key` = '914zlc01k9aa40ci59gjs3pklewdxd0l'
160 Query RELEASE SAVEPOINT `s139711800366912_x4`
160 Query SELECT `django_session`.`session_key`, `django_session`.`session_data`, `django_session`.`expire_date` FROM `django_session` WHERE (`django_session`.`expire_date` > '2022-02-02 07:36:53.793298' AND `django_session`.`session_key` = '914zlc01k9aa40ci59gjs3pklewdxd0l') LIMIT 21
160 Query SELECT `auth_user`.`id`, `auth_user`.`password`, `auth_user`.`last_login`, `auth_user`.`is_superuser`, `auth_user`.`username`, `auth_user`.`first_name`, `auth_user`.`last_name`, `auth_user`.`email`, `auth_user`.`is_staff`, `auth_user`.`is_active`, `auth_user`.`date_joined` FROM `auth_user` WHERE `auth_user`.`id` = 1 LIMIT 21
160 Query UPDATE `trading_shares` SET `amount` = (`trading_shares`.`amount` - 10) WHERE (`trading_shares`.`account_id` = 1 AND `trading_shares`.`company_id` = 1)
160 Query UPDATE `trading_shares` SET `amount` = (`trading_shares`.`amount` + 10) WHERE (`trading_shares`.`account_id` = 2 AND `trading_shares`.`company_id` = 1)
160 Query INSERT INTO `trading_sharestransfer` (`account_from_id`, `account_to_id`, `company_id`, `amount`, `created_dt`, `updated_dt`) VALUES (1, 2, 1, 10, '2022-02-02 07:36:53.804298', '2022-02-02 07:36:53.804350')
160 Query SELECT `trading_shares`.`id`, `trading_shares`.`account_id`, `trading_shares`.`company_id`, `trading_shares`.`amount`, `trading_shares`.`created_dt`, `trading_shares`.`updated_dt` FROM `trading_shares` WHERE `trading_shares`.`id` = 1 LIMIT 21
160 Query SELECT `trading_shares`.`id`, `trading_shares`.`account_id`, `trading_shares`.`company_id`, `trading_shares`.`amount`, `trading_shares`.`created_dt`, `trading_shares`.`updated_dt` FROM `trading_shares` WHERE `trading_shares`.`id` = 2 LIMIT 21
160 Query ROLLBACK TO SAVEPOINT `s139711800366912_x1`
160 Query RELEASE SAVEPOINT `s139711800366912_x1`
160 Query ROLLBACK
# test가 끝나면서 rollback 됨.
160 Query SET autocommit=1
160 Quit
3) TransactionTestCase상속, ATOMIC_REQUESTS=True 일 때
우선 TransactionTestCase를 사용하면 setUpTestData class method를 사용할 수 없다. 테스트 별로 truncate table이 수행되기 때문에 transaction단위로 reset을 하는 TestCase 클래스에서 사용하는 class method이기 때문이다.
그래서 아래와 같이 각 test 별로 실행하는 setUp 메서드에 초기 데이터를 생성하는 코드를 넣어야 한다.
def setUp(self) -> None:
self.username1, self.password1 = 'tester', 'tester1234'
self.username2, self.password2 = 'tester2', 'tester12342'
self.user1 = User.objects.create_user(username=self.username1, password=self.password1)
self.user2 = User.objects.create_user(username=self.username2, password=self.password2)
# fixture dataset
self.company = Company.objects.create(ticker='AMZN', name='amazon')
self.account1 = Account.objects.create(owner=self.user1)
self.account2 = Account.objects.create(owner=self.user2)
self.shares1 = Shares.objects.create(account=self.account1, company=self.company, amount=100)
self.shares2 = Shares.objects.create(account=self.account2, company=self.company, amount=100)
# for session authenticating
self.client = APIClient()
self.client.login(username=self.username1, password=self.password1)
테스트 결과: 성공
실제 우리의 production 환경에서 동작하는대로 response handler 시작 부분에서 transaction이 사작된다.
그리고 exception이 발생했기 때문에 마지막에 rollback까지 되서 예상대로 동작한 것을 확인할 수 있다.
또, 위에서 살펴본 것 처럼 테스트 후 모든 테이블을 지우는 것을 확인할 수 있다.
DB에 실행되는 SQL
# response 시작
173 Query SET autocommit=0
173 Query SELECT `django_session`.`session_key`, `django_session`.`session_data`, `django_session`.`expire_date` FROM `django_session` WHERE (`django_session`.`expire_date` > '2022-02-02 08:02:40.966071' AND `django_session`.`session_key` = 'pg91k1gjinhmlvk6682g51lkswhj3osk') LIMIT 21
173 Query SELECT `auth_user`.`id`, `auth_user`.`password`, `auth_user`.`last_login`, `auth_user`.`is_superuser`, `auth_user`.`username`, `auth_user`.`first_name`, `auth_user`.`last_name`, `auth_user`.`email`, `auth_user`.`is_staff`, `auth_user`.`is_active`, `auth_user`.`date_joined` FROM `auth_user` WHERE `auth_user`.`id` = 1 LIMIT 21
173 Query UPDATE `trading_shares` SET `amount` = (`trading_shares`.`amount` - 10) WHERE (`trading_shares`.`account_id` = 1 AND `trading_shares`.`company_id` = 1)
173 Query UPDATE `trading_shares` SET `amount` = (`trading_shares`.`amount` + 10) WHERE (`trading_shares`.`account_id` = 2 AND `trading_shares`.`company_id` = 1)
173 Query INSERT INTO `trading_sharestransfer` (`account_from_id`, `account_to_id`, `company_id`, `amount`, `created_dt`, `updated_dt`) VALUES (1, 2, 1, 10, '2022-02-02 08:02:40.980863', '2022-02-02 08:02:40.980912')
# response과정에서 exception이 발생해서 rollback
173 Query ROLLBACK
173 Query SET autocommit=1
173 Query SELECT `trading_shares`.`id`, `trading_shares`.`account_id`, `trading_shares`.`company_id`, `trading_shares`.`amount`, `trading_shares`.`created_dt`, `trading_shares`.`updated_dt` FROM `trading_shares` WHERE `trading_shares`.`id` = 1 LIMIT 21
173 Query SELECT `trading_shares`.`id`, `trading_shares`.`account_id`, `trading_shares`.`company_id`, `trading_shares`.`amount`, `trading_shares`.`created_dt`, `trading_shares`.`updated_dt` FROM `trading_shares` WHERE `trading_shares`.`id` = 2 LIMIT 21
173 Query SHOW FULL TABLES
173 Query SET autocommit=0
173 Query SET FOREIGN_KEY_CHECKS = 0
# test 수행 후 모든 테이블 truncate
173 Query DELETE FROM `django_admin_log`
173 Query DELETE FROM `auth_user_groups`
173 Query DELETE FROM `auth_user_user_permissions`
173 Query DELETE FROM `django_session`
173 Query DELETE FROM `event_analyticsevent`
173 Query DELETE FROM `trading_sharestransfer`
173 Query DELETE FROM `auth_group_permissions`
4) TransactionTestCase상속, ATOMIC_REQUESTS=False 일 때
테스트 결과: 실패
FAILED tests/trading/api/test_share_transfer_view_set.py::ShareTransferViewSetTests::test_create_fail_with_exception_for_checking_transaction - AssertionError: 90 != 100
동작하는 테스트 코드는 위와 같다. 크게 예상과는 다를 것이 없다. 트랜잭션으로 묶이는 부분이 없기 때문에 response에서 exception이 발생했지만 DB에는 잘 반영되는 것을 볼 수 있다. TestCase도 ATOMIC_REQUEST=False일 때 실패했지만 정확한 동작은 다르다. 이번에는 다른 transaction에서 조회하더라도 같은 결과를 볼 수 있다.
DB에 실행되는 SQL
# autocommit=1 이기 때문에 sql마다 바로 commit됨.
178 Query SET autocommit=1
178 Query SELECT `django_session`.`session_key`, `django_session`.`session_data`, `django_session`.`expire_date` FROM `django_session` WHERE (`django_session`.`expire_date` > '2022-02-02 08:05:15.733374' AND `django_session`.`session_key` = '04mlonkuwc55gaqugd0sere8drm9ytbu') LIMIT 21
178 Query SELECT `auth_user`.`id`, `auth_user`.`password`, `auth_user`.`last_login`, `auth_user`.`is_superuser`, `auth_user`.`username`, `auth_user`.`first_name`, `auth_user`.`last_name`, `auth_user`.`email`, `auth_user`.`is_staff`, `auth_user`.`is_active`, `auth_user`.`date_joined` FROM `auth_user` WHERE `auth_user`.`id` = 3 LIMIT 21
178 Query UPDATE `trading_shares` SET `amount` = (`trading_shares`.`amount` - 10) WHERE (`trading_shares`.`account_id` = 3 AND `trading_shares`.`company_id` = 2)
178 Query UPDATE `trading_shares` SET `amount` = (`trading_shares`.`amount` + 10) WHERE (`trading_shares`.`account_id` = 4 AND `trading_shares`.`company_id` = 2)
178 Query INSERT INTO `trading_sharestransfer` (`account_from_id`, `account_to_id`, `company_id`, `amount`, `created_dt`, `updated_dt`) VALUES (3, 4, 2, 10, '2022-02-02 08:05:15.750129', '2022-02-02 08:05:15.750191')
178 Query SHOW FULL TABLES
178 Query SET autocommit=0
178 Query SET FOREIGN_KEY_CHECKS = 0
# 각 테스트 수행 후 테이블 truncate
178 Query DELETE FROM `django_session`
178 Query DELETE FROM `blog_post`
178 Query DELETE FROM `django_admin_log`
....
마무리
이번 편에서는 Django에서 제공하는 TestClass들을 활용해서 Transaction과 관련된 테스트를 진행해 봤습니다. 정확한 Transaction의 동작을 테스트 하기 위해서 TransactionTestCase를 사용하는 것이 좋습니다. 하지만, 정확한 이해를 하고 있다는 것을 바탕으로는 TestCase class로도 어느 정도까지는 Savepoint를 활용하여 테스트가 가능합니다.
Django DB Transaction 포스팅 마무리 하면서..
이 번 편을 마지막으로 Django DB Transaction과 관련된 포스팅을 마치려고 합니다. 원래는 4편으로 나눠서 쓰려고 했는데 SAVEPOINT가 2편에 같이 포스팅하면서 3편으로 줄었습니다. Django에서 지원하는 DB Transaction과 관련된 부분을 잘 전달하려고 노력했는데 생각보다 글을 잘 작성하는 것도 어렵고, 정리하다보면 제가 모르는 부분도 많이 있어서 부족한 부분이 많은 것 같습니다. Django 공식 문서가 정말 정리가 잘 되어 있기 때문에 중간에 이해가 안 가시는 부분이 있으면 같이 참고하셔도 좋을 듯 합니다. 제가 조금 신경 쓴 부분은 실제 Database에서 어떻게 동작하는지를 최대한 같이 설명하려고 노력했습니다. 읽어 주셔서 감사하고, 혹시 글과 관련되서 궁금한 점이나 잘 못된 부분을 알려주시면 반영하도록 하겠습니다! 또, Django와 관련된 공유나 활동(스터디, 발표등)들이 많이 생기면 좋겠다는 생각을 하고 있는데 관심있는 분들은 알려주세요!
'Computer Engineering > Django' 카테고리의 다른 글
Django CORS 관련 설정하기 / django-cors-headers (0) | 2023.10.29 |
---|---|
Python/Django NewRelic 셋업 및 환경 분리하기. (0) | 2022.06.29 |
Django DB Transaction 2편 - 명시적으로 transaction 활용하기. (feat. savepoint) (2) | 2022.01.17 |
Django DB Transaction 1편 - Request와 DB Transaction 묶기(Feat. ATOMIC_REQUESTS) (0) | 2022.01.01 |
Django 오픈소스 contributing 하기! (3) | 2021.07.31 |