Внимание! Если у вас возникли проблемы с выполнением предыдущего этапа, то вы сможете зайти в соответствующий урок, скачать архив предыдущего этапа, инсталлировать его, и начать этот урок именно с того самого места, где закончился предыдущий!
Содержание курса:
Большинство программистов не любит писать тесты.
Оно и понятно - мало кто любит делать двойную работу: сначала написать и отладить код, а потом ещё столько же времени (если не больше!) затратить на написание тестов.
Хорошо, если проект будет поддерживаться и дальше. Тогда время, затраченное на написание тестов, гарантированно окупится: убедиться в том, что создавая новое мы не сломали старое, можно будет за секунды.
Однако, если проект будет закрыт, то к потерям времени на написание основного кода добавятся ещё и затраты на тесты.
Так что написание тестов чем-то похоже на инвестирование - дело, в принципе, хорошее, но рискованное. Поэтому писать тесты или не писать - каждый выбирает для себя сам в зависимости от каждой конкретной ситуации.
Тем не менее, есть подход в программировании, который позволяет если не выиграть, то, как минимум, почти не проиграть при написании тестов. И называется это подход "Test Driven Development" (TDD) ("Разработка через тестирование"). Идея очень проста: мы СНАЧАЛА пишем тесты, и только потом пишем сам код, который должен пройти эти тесты.
И в чём же здесь плюс? А плюс в том, что в этом случае полностью исключается затраты времени на проверку кода. Ведь даже самый первый прогон свеже-написанного кода будет делаться уже "не руками", а тестами!
И дополнительный бонус этого подхода - сама проверка становится более системной, а значит более надёжной. Поскольку, что и как тестировать - обдумывается заранее, а не делается экспромтом. Что резко уменьшает шансы упустить из виду что-то важное.
Собственно, именно поэтому в Django достаточно хорошо разработан инструментарий для тестирования кода. И именно поэтому уже в первом учебном примере на сайте этого фреймворка целый урок из восьми отводится на рассмотрение примера реализации этого принципа: "Разработка через тестирование" или "Test Driven Development" (TDD).
В нашем учебном мини-курсе мы рассмотрим подход разработки через тестирование при создании базы данных, поскольку основная вычислительная нагрузка или основная бизнес-логика будет находится именно в модуле shop/models.py, отвечающем за взаимодействие между таблицами базы данных.
Итак, наши задачи на этом этапе:
Для упрощения задачи (всё-таки, это мини-курс!) оговорим три допущения:
Таким образом, структура данных будет состоять из 4-х таблиц:
Ещё нам потребуется таблица Клиентов User, но на наше счастье она уже присутствует в Django по умолчанию, хотя и в самой простой базовой версии. Однако, на этапе учебного проекта этого будет более, чем достаточно.
Всем вышеперечисленным требованиям вполне удовлетворяет эта блок-схема:
Поэтому в модуле shop/models.py создаём следующую структуру:
class Product(models.Model):
name = models.CharField(max_length=255, verbose_name='product_name')
code = models.CharField(max_length=255, verbose_name='product_code')
price = models.DecimalField(max_digits=20, decimal_places=2)
unit = models.CharField(max_length=255, blank=True, null=True)
image_url = models.URLField(blank=True, null=True)
note = models.TextField(blank=True, null=True)
class Payment(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
amount = models.DecimalField(max_digits=20, decimal_places=2, blank=True, null=True)
time = models.DateTimeField(auto_now_add=True)
comment = models.TextField(blank=True, null=True)
class Order(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
# items = models.ManyToManyField(OrderItem, related_name='orders')
status = models.CharField(max_length=32, choices=STATUS_CHOICES, default=STATUS_CART)
amount = models.DecimalField(max_digits=20, decimal_places=2, blank=True, null=True)
creation_time = models.DateTimeField(auto_now_add=True)
payment = models.ForeignKey(Payment, on_delete=models.PROTECT, blank=True, null=True)
comment = models.TextField(blank=True, null=True)
class OrderItem(models.Model):
order = models.ForeignKey(Order, on_delete=models.CASCADE)
product = models.ForeignKey(Product, on_delete=models.PROTECT)
quantity = models.PositiveIntegerField(default=1)
price = models.DecimalField(max_digits=20, decimal_places=2)
discount = models.DecimalField(max_digits=20, decimal_places=2, default=0)
Чтобы объекты этих классов понятно отображались при отладке и в админке, полезно будет добавить в каждый класс методы __str__, а также упорядочить все записи в таблицах по порядку, например:
class Meta:
ordering = ['pk']
def __str__(self):
return f'{self.user} --- {self.time.ctime()} --- {self.amount}'
Завершающим шагом создания БД будет создание миграция и их применение:
python manage.py makemigrations
python manage.py migrate
И последний штрих - добавить управление и отображение БД в админку:
from django.contrib import admin
from shop.models import Product, Payment, OrderItem, Order
admin.site.register(Product)
admin.site.register(Payment)
admin.site.register(OrderItem)
admin.site.register(Order)
Как видим, список проверок достаточно внушительный. Даже однократное ручное выполнение всех перечисленных тестов - “та ещё работёнка”, которая может занять минут 15, не меньше.
Но мы-то хорошо знаем из собственного опыта, что редкий код начинает работать без ошибок с первого раза. Следовательно, ручное тестирование заняло бы часы нашего времени. А каждый новый рефакторинг кода - это снова часы на проверку. Не говоря уже о том, что при ручном тестирование можно что-то забыть проверить, или забыть удалить тестовые значения из БД.
В общем, как мы сами убедимся далее, написание тестов займёт меньше времени уже с первого раза, а значит окупит свои затраты уже на первом тестовом прогоне!
При написании тестов необходимо помнить следующее:
fixtures = [
"shop/fixtures/data.json"
]
Пример содержимого файла тестов shop/tests.py представлен ниже:
from django.test import TestCase,
from shop.models import *
class TestDataBase(TestCase):
fixtures = [
"shop/fixtures/data.json"
]
def setUp(self):
self.user = User.objects.get(username='root')
def test_user_exists(self):
users = User.objects.all()
users_number = users.count()
user = users.first()
self.assertEqual(users_number, 1)
self.assertEqual(user.username, 'root')
self.assertTrue(user.is_superuser)
def test_user_check_password(self):
self.assertTrue(self.user.check_password('123'))
Пример фрагмента кода, используемого в лекции, представлен ниже:
def change_status_after_payment(payment: Payment):
"""
Calling the method after creating a payment and before saving it.
First need to find total amount from all payments (previous and current) for the user.
If total amount >= the order amount, we change status and create negative payment.
"""
user = payment.user
while True:
order = Order.objects.filter(status=Order.STATUS_WAITING_FOR_PAYMENT, user=user) \
.order_by('creation_time') \
.first()
if not order:
break
total_payments_amount = get_balance(payment.user)
if order.amount > total_payments_amount:
break
order.payment = payment
order.status = Order.STATUS_PAID
order.save()
Payment.objects.create(user=user, amount=-order.amount)
@receiver(post_save, sender=Payment)
def on_payment_save(sender, instance, **kwargs):
if instance.amount > 0:
# pay_for_waiting_orders(instance)
change_status_after_payment(instance)
@receiver(post_save, sender=Order)
def on_order_save(sender, instance, **kwargs):
if Order.objects.filter(status='2_waiting_for_payment'):
pay_for_waiting_orders(instance)
Более подробно с со всеми деталями этого этапа вы сможете познакомиться из этого видео: