Attention! If you're having trouble following the previous step, you can visit relevant lesson , download the archive of the previous step, install it, and start this lesson exactly where the previous one ended!
In this course, we will go through all the stages of creating a new project together:
Most programmers don't like to write tests.
It is understandable - few people like to do double work: first write and debug code, and then spend the same amount of time (if not more!) on writing tests.
Well, if the project will be supported further. Then the time spent on writing tests is guaranteed to pay off: it will be possible to make sure that creating a new one did not break the old one in seconds.
However, if the project is closed, then the cost of tests will be added to the loss of time for writing the main code.
So writing tests is a bit like investing - in principle, a good thing, but risky. Therefore, to write tests or not to write - everyone chooses for himself, depending on each specific situation.
Nevertheless, there is an approach in programming that allows you, if not to win, then at least almost not to lose when writing tests. And this approach is called "Test Driven Development" (TDD) ("Development through testing"). The idea is very simple: we FIRST write tests, and only then we write the code itself, which must pass these tests.
And what is the advantage here? And the plus is that in this case, the time spent on checking the code is completely eliminated. After all, even the very first run of a freshly written code will be done "not by hand", but by tests!
And an additional bonus of this approach is that the check itself becomes more systematic, and therefore more reliable. Because what and how to test is thought out in advance, and not done impromptu. Which drastically reduces the chances of missing something important.
Actually, this is why Django has a well-developed code testing toolkit. And that is why already in the first training example on the website of this framework, a whole lesson out of eight is devoted to the consideration of an example of the implementation of this principle: "Development through testing" or "Test Driven Development" (TDD).
In our training mini-course, we will consider the test-driven development approach when creating a database, since the main computational load or the main business logic will be located in the shop/models.py module, which is responsible for the interaction between database tables data.
So, our tasks at this stage:
To simplify the task (after all, this is a mini-course!) we will stipulate three assumptions:
This block diagram satisfies all the above requirements:
Therefore, in the shop/models.py module, we create the following structure:
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}'
The final step in creating a database is to create migrations and apply them:
python manage.py makemigrations
python manage.py migrate
And the final touch is to add control and display of the database to the admin panel:
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)
As you can see, the list of checks is quite impressive. Even a single manual execution of all the listed tests is "very hard work", which can take 15 minutes, no less.
But we know well from our own experience that a rare code starts working without errors the first time. Consequently, manual testing would take hours of our time. And each new refactoring of the code is again hours for verification. Not to mention the fact that during manual testing, you can forget to check something, or forget to delete test values from the database.
In general, as we will see for ourselves further, writing tests will take less time from the first time, which means that it will pay off its costs already on the first test run!
When writing tests, keep the following in mind:
fixtures = [
"shop/fixtures/data.json"
]
An example of the contents of the test file shop/tests.py is shown below:
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'))
An example of a code snippet used in the lecture is shown below:
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)
You can learn more about all the details of this stage from this video (RU voice):