• Najnowsze pytania
  • Bez odpowiedzi
  • Zadaj pytanie
  • Kategorie
  • Tagi
  • Zdobyte punkty
  • Ekipa ninja
  • IRC
  • FAQ
  • Regulamin
  • Książki warte uwagi

Jak pisać testy jednostkowe Python Django?

Object Storage Arubacloud
0 głosów
3,114 wizyt
pytanie zadane 28 października 2018 w Python przez Eliro Stary wyjadacz (12,160 p.)
Cześć!

W Django jestem żółtodziobem, więc wybaczcie mi moją niewiedzę. Zrobiłem sobie mały projekcjik forum internetowego w Django, żeby mieć co pokazać na CV, jednak jest coś, czego mu brakuje. Mianowicie - testów. Dowiedziałem się, że wymagane są w każdej aplikacji. Zanim więc zabiorę się za pisanie jakiegoś kolejnego projektu, chciałbym się was zapytać - w jaki sposób tworzyć te testy? Szukałem po internecie jakichś poradników, ale jakoś to do mnie nie dociera. Moglibyście mi polecić jakiś materiał do nauki?

https://github.com/Incybro/Forum
komentarz 28 października 2018 przez izonik Stary wyjadacz (12,560 p.)
Problem jest z testami ogólnie, czy nie wiesz jak napisać je w Django ?
komentarz 28 października 2018 przez Eliro Stary wyjadacz (12,160 p.)
W ogóle takie coś jak testy jest dla mnie nowe i nie wiem jak się za to zabrać. Głównie chciałbym uczyć się pod Django.

2 odpowiedzi

+4 głosów
odpowiedź 28 października 2018 przez izonik Stary wyjadacz (12,560 p.)
wybrane 30 października 2018 przez Eliro
 
Najlepsza

Witaj, 

Testy nie są trudne, ale IMO lepiej je zrozumiesz na czystym języku.

Spróbuje ci to wytłumaczyć.

 

Dla przykładu stwórzmy sobie funkcję liczącą średnią.

def arithmetic_mean(*args):
	return sum(args) / len(args)

Myślę że jej działanie jest oczywiste. Teraz chcemy sprawdzić czy aby na pewno działa. Więc tak na początku sprawdźmy sobie czy zadziała w standardowym przypadku - czyli z liczbami naturalnymi. Aby to zrobić użyjmy konsoli Pythona.

>>> arithmetic_mean(1, 2, 3, 4)
>>> 2.5

Ok działa mamy to co chcieliśmy. Teraz spróbujmy z liczbami ujemnymi.

>>> arithmetic_mean(-8, -1, -19, -8)
>>> -9.0

Też działa, no to teraz sprawdźmy mieszanymi liczbami.

>>> arithmetic_mean(-19, 10, 54, 21)
>>> 16.5

Ok, a co się stanie kiedy podamy tylko jedną liczbę ?

>>> arithmetic_mean(32)
>>> 32.0

Ponownie zadziałało. 

Teraz wyobraźmy sobie że zamiast naszej małej funkcji mamy skomplikowaną funkcję liczącą np. 100 lini, wykorzystującą 10 innych funkcji. Jasne możemy sprawdzać czy działa ręcznie, ale wraz z rozrastaniem się projektu będzie to coraz dłużej trwało i trwało. Zmiana działania jednej funkcji, może rzutować na resztę kodu. Bez testów szukanie przyczyny może być trudne, bądź możemy nie dostrzec błędu. Dzięki testom, nie musimy robić tego za każdym razem - po prostu odpalamy testy i widzimy czy wszystko działa. (pod warunkiem że testy zostały napisane dobrze)

Inne zastosowanie testów. Kod tego forum jest dostępny publicznie i każdy może wnieść swoją poprawkę itp. Gdyby nie zawierał testów, to dodając nową funkcjonalność mógłbym zepsuć coś co już działa. Kiedy testy istnieją to widzę, czy mój wkład nie zepsuje czegoś co wcześniej działało. Tak samo ważne jest aby mój kod posiadał testy. Przecież ktoś inny też może coś dodać, bez testów nie będzie zagwarantowane, że moja funkcjonalność będzie działać po modyfikacji.

Ok teraz czas na stworzenie testów.

import unittest


# Tworzymy klase do testowania naszej funkcji
class ArithmeticMeanTests(unittest.TestCase):
	# Testujemy czy zadziała z liczbami naturalnymi
	def test_positive_numbers(self):
		numbers = [1, 3, 4, 5]
		# assertEqual służy do sprawdzenia czy wartości są równe
		# jeśli tak to test się powiedzie, w przeciwnym razie nie
		self.assertEqual(
			arithmetic_mean(*numbers),
			3.25
			)

	# Restujemy ale dla liczb ujemnych
	def test_negative_numbers(self):
		numbers = [-1, -5, -19, -21]
		self.assertEqual(
			arithmetic_mean(*numbers),
			-11.5
			)

	# Tutaj dla liczb całkowitych
	def test_mixed_numbers(self):
		numbers = [39, -21, 1, 0, 5]
		self.assertEqual(
			arithmetic_mean(*numbers),
			4.8
			)

	# Tutaj dla tylko jednej liczby
	def test_single_number(self):
		self.assertEqual(
			arithmetic_mean(129),
			129
			)


# Uruchamiamy testy
unittest.main()

Ćwiczenia:

-> dopisz testy testujące kod w przypadku ułamków

*-> napisz funkcję zamieniającą liczby z systemu dziesiętnego na szesnastkowy, nie zapomnij o testach

 

Spójrz jeszcze tu. Jeśli chodzi o Django to po zrozumieniu idei testowania, nie będziesz miał problemów. Poczytaj MDN i Oficjalny Tutorial.

komentarz 28 października 2018 przez Eliro Stary wyjadacz (12,160 p.)
edycja 28 października 2018 przez Eliro
    def test_fraction_numbers(self):
        numbers = [1.5, 3.5, 4.5, 5.5]
        self.assertEqual(arithmetic_mean(*numbers), 3.75)

No dobrze, testy takich aplikacji w miarę już rozumiem. Ale co z Django, gdzie mam np. model Post, Category oraz Answer? Co ja mam niby tu przetestować?

class Category(models.Model):
    title = models.CharField(max_length=100)
    description = models.TextField()

    def __str__(self): #Definiujemy funkcje, która zwraca tytuł wpisu
        return self.title

class Post(models.Model):
    author = models.ForeignKey(User, on_delete=models.CASCADE)
    title = models.CharField(max_length=100)
    description = models.TextField()
    category = models.ForeignKey(Category, on_delete=models.CASCADE)
    published_date = models.DateTimeField(default=timezone.now)
    updated = models.DateTimeField(blank=True, null=True)
    views = models.IntegerField(default=0)

    def __str__(self): #Definiujemy funkcje, która zwraca tytuł wpisu
        return self.title

class Answer(models.Model):
    post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='answers')
    author = models.ForeignKey(User, on_delete=models.CASCADE)
    text = models.TextField()
    published_date = models.DateTimeField(default=timezone.now)

    def __str__(self):
        return self.text

 

Testy nie są trudne

Więc ja chyba jestem wybitnym beztalenciem, bo nie wiem jak stworzyć testy do moich aplikacji.

komentarz 28 października 2018 przez kubaapk Nałogowiec (44,270 p.)
Poczytaj o: mock, stub, spy.
+1 głos
odpowiedź 28 października 2018 przez adrian17 Ekspert (344,860 p.)
edycja 28 października 2018 przez adrian17

Oficjalna dokumentacja jest dość kompletna, choć nie ma wielu przykładów: https://docs.djangoproject.com/en/2.1/topics/testing/

 

Ogólnie najlepiej jeśli większość logiki aplikacji jest niezależna od widoków - wtedy testowanie jest proste, bo sprowadza się do przygotowaniu stanu bazy danych, wywołaniu kodu z logiką i sprawdzenie wyniku; na przykład coś w stylu

from django.test import TestCase
from aplikacja import archive_old_posts

class MyTests(TestCase):
    def test_something(self):
        user = User.objects.create(...)
        Post.objects.create(user=user, ...)

        archive_old_posts()

        self.assertTrue(Post.objects.get(...).archived)

(nowszy styl pisania widoków, na klasach, trochę w tym pomaga; nie musisz wtedy pisać wszędzie tego boilerplate `if request.method == "POST"` etc)

Kiedy chcesz testować samą warstwę widoków, używana jest klasa Client. Na przykład najprostszy test:

import unittest
from django.test import Client

class PostTest(unittest.TestCase):
    def test_view(self):
        client = Client()
        Post.objects.create(id=1)
        response = client.get('/post/1')
        self.assertEqual(response.status_code, 200)
        # mozesz tez przetestowac sam wynikowy HTML (JSON jesli to API)

    def test_post_not_found(self):
        client = Client()
        response = client.get('/post/1') # post 1 doesn't exist
        self.assertEqual(response.status_code, 404)

Albo na przykład przetestować formularz:

    def test_create_view(self):
        client = Client()
        response = client.post('/post/create', {'title': 'moj pierwszy post'})
        self.assertEqual(Post.objects.get().title, 'moj pierwszy post')

Podobny przykład z innym popularnym frameworkiem do testowania, py.test:

testy funkcji: https://github.com/mirumee/saleor/blob/master/tests/test_cart.py#L59

widoków: https://github.com/mirumee/saleor/blob/master/tests/test_cart.py#L434

komentarz 28 października 2018 przez adrian17 Ekspert (344,860 p.)
Na moje oko to nie ma związku z testami, tylko z tym, że stan "bazy" nie zgadza się z modelem; na przykład żadna migracja nie tworzy klasy Answer. Robiłeś ostatnio `makemigrations`?
komentarz 28 października 2018 przez Eliro Stary wyjadacz (12,160 p.)

Rzeczywiście. Choć to dziwne, bo przecież normalnie dodawałem komentarze(Answer) po uruchomieniu przez runserver. Pamiętam również, że robiłem makemigrations.

C:\Users\Admin\Desktop\Forum>python manage.py test
Creating test database for alias 'default'...
Got an error creating the test database: (1007, "Can't create database 'test_for
um'; database exists")
Type 'yes' if you would like to try deleting the test database 'test_forum', or
'no' to cancel: yes
Destroying old test database for alias 'default'...
System check identified no issues (0 silenced).
EE
======================================================================
ERROR: test_post_not_found (Homepage.tests.PostTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\Users\Admin\Desktop\Forum\Homepage\tests.py", line 13, in test_post_n
ot_found
    response = self.client.get('/post/1') # post 1 doesn't exist
AttributeError: 'PostTest' object has no attribute 'client'

======================================================================
ERROR: test_view (Homepage.tests.PostTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\Users\Admin\AppData\Local\Programs\Python\Python36-32\lib\site-packag
es\django\db\backends\mysql\base.py", line 71, in execute
    return self.cursor.execute(query, args)
  File "C:\Users\Admin\AppData\Local\Programs\Python\Python36-32\lib\site-packag
es\MySQLdb\cursors.py", line 250, in execute
    self.errorhandler(self, exc, value)
  File "C:\Users\Admin\AppData\Local\Programs\Python\Python36-32\lib\site-packag
es\MySQLdb\connections.py", line 50, in defaulterrorhandler
    raise errorvalue
  File "C:\Users\Admin\AppData\Local\Programs\Python\Python36-32\lib\site-packag
es\MySQLdb\cursors.py", line 247, in execute
    res = self._query(query)
  File "C:\Users\Admin\AppData\Local\Programs\Python\Python36-32\lib\site-packag
es\MySQLdb\cursors.py", line 411, in _query
    rowcount = self._do_query(q)
  File "C:\Users\Admin\AppData\Local\Programs\Python\Python36-32\lib\site-packag
es\MySQLdb\cursors.py", line 374, in _do_query
    db.query(q)
  File "C:\Users\Admin\AppData\Local\Programs\Python\Python36-32\lib\site-packag
es\MySQLdb\connections.py", line 277, in query
    _mysql.connection.query(self, query)
_mysql_exceptions.OperationalError: (1048, "Column 'author_id' cannot be null")

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:\Users\Admin\Desktop\Forum\Homepage\tests.py", line 7, in test_view
    Post.objects.create(id=1)
  File "C:\Users\Admin\AppData\Local\Programs\Python\Python36-32\lib\site-packag
es\django\db\models\manager.py", line 82, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "C:\Users\Admin\AppData\Local\Programs\Python\Python36-32\lib\site-packag
es\django\db\models\query.py", line 413, in create
    obj.save(force_insert=True, using=self.db)
  File "C:\Users\Admin\AppData\Local\Programs\Python\Python36-32\lib\site-packag
es\django\db\models\base.py", line 718, in save
    force_update=force_update, update_fields=update_fields)
  File "C:\Users\Admin\AppData\Local\Programs\Python\Python36-32\lib\site-packag
es\django\db\models\base.py", line 748, in save_base
    updated = self._save_table(raw, cls, force_insert, force_update, using, upda
te_fields)
  File "C:\Users\Admin\AppData\Local\Programs\Python\Python36-32\lib\site-packag
es\django\db\models\base.py", line 831, in _save_table
    result = self._do_insert(cls._base_manager, using, fields, update_pk, raw)
  File "C:\Users\Admin\AppData\Local\Programs\Python\Python36-32\lib\site-packag
es\django\db\models\base.py", line 869, in _do_insert
    using=using, raw=raw)
  File "C:\Users\Admin\AppData\Local\Programs\Python\Python36-32\lib\site-packag
es\django\db\models\manager.py", line 82, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "C:\Users\Admin\AppData\Local\Programs\Python\Python36-32\lib\site-packag
es\django\db\models\query.py", line 1136, in _insert
    return query.get_compiler(using=using).execute_sql(return_id)
  File "C:\Users\Admin\AppData\Local\Programs\Python\Python36-32\lib\site-packag
es\django\db\models\sql\compiler.py", line 1289, in execute_sql
    cursor.execute(sql, params)
  File "C:\Users\Admin\AppData\Local\Programs\Python\Python36-32\lib\site-packag
es\django\db\backends\utils.py", line 68, in execute
    return self._execute_with_wrappers(sql, params, many=False, executor=self._e
xecute)
  File "C:\Users\Admin\AppData\Local\Programs\Python\Python36-32\lib\site-packag
es\django\db\backends\utils.py", line 77, in _execute_with_wrappers
    return executor(sql, params, many, context)
  File "C:\Users\Admin\AppData\Local\Programs\Python\Python36-32\lib\site-packag
es\django\db\backends\utils.py", line 85, in _execute
    return self.cursor.execute(sql, params)
  File "C:\Users\Admin\AppData\Local\Programs\Python\Python36-32\lib\site-packag
es\django\db\backends\mysql\base.py", line 76, in execute
    raise utils.IntegrityError(*tuple(e.args))
django.db.utils.IntegrityError: (1048, "Column 'author_id' cannot be null")

----------------------------------------------------------------------
Ran 2 tests in 0.031s

FAILED (errors=2)
Destroying test database for alias 'default'...

Nie wiem czy tak to miało działać

komentarz 28 października 2018 przez adrian17 Ekspert (344,860 p.)
Co do `client`, zrobiłem drobne błędy w moim przykładzie :) Spójrz teraz.

Co do reszty i Post, to był kompletnie przykładowy kod bez związku z Twoim modelem - miał tylko pokazać jak to wygląda, a nie żebyś go bezpośrednio przekopiował do siebie ;)
komentarz 28 października 2018 przez Eliro Stary wyjadacz (12,160 p.)
from django.test import TestCase
from django.test import Client
from .models import *

class TestSomething(TestCase):
    def setUp(self):
        self.client = Client()
        self.user = User.objects.create(
            first_name='Incybro',
            last_name='S',
            email='incybro@gmail.com',
            username='Incybro',
            is_superuser=True
        )
        self.password = 'secret'
        self.user.set_password(self.password)
        self.user.save()
    def test_user_login(self):
        login = self.client.login(username=self.user.username, password=self.password)
        self.assertTrue(login)

Wynik:

C:\Users\Admin\Desktop\Forum>python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------
Ran 1 test in 1.747s

OK
Destroying test database for alias 'default'...

C:\Users\Admin\Desktop\Forum>

Chyba tak jakoś mam to robić?

komentarz 28 października 2018 przez adrian17 Ekspert (344,860 p.)
Tak :)

Podobne pytania

0 głosów
1 odpowiedź 255 wizyt
pytanie zadane 12 listopada 2018 w Python przez Eliro Stary wyjadacz (12,160 p.)
0 głosów
0 odpowiedzi 229 wizyt
+1 głos
2 odpowiedzi 323 wizyt
pytanie zadane 7 listopada 2019 w Python przez Kamil Początkujący (430 p.)

92,568 zapytań

141,420 odpowiedzi

319,620 komentarzy

61,954 pasjonatów

Motyw:

Akcja Pajacyk

Pajacyk od wielu lat dożywia dzieci. Pomóż klikając w zielony brzuszek na stronie. Dziękujemy! ♡

Oto polecana książka warta uwagi.
Pełną listę książek znajdziesz tutaj.

Akademia Sekuraka

Kolejna edycja największej imprezy hakerskiej w Polsce, czyli Mega Sekurak Hacking Party odbędzie się już 20 maja 2024r. Z tej okazji mamy dla Was kod: pasjamshp - jeżeli wpiszecie go w koszyku, to wówczas otrzymacie 40% zniżki na bilet w wersji standard!

Więcej informacji na temat imprezy znajdziecie tutaj. Dziękujemy ekipie Sekuraka za taką fajną zniżkę dla wszystkich Pasjonatów!

Akademia Sekuraka

Niedawno wystartował dodruk tej świetnej, rozchwytywanej książki (około 940 stron). Mamy dla Was kod: pasja (wpiszcie go w koszyku), dzięki któremu otrzymujemy 10% zniżki - dziękujemy zaprzyjaźnionej ekipie Sekuraka za taki bonus dla Pasjonatów! Książka to pierwszy tom z serii o ITsec, który łagodnie wprowadzi w świat bezpieczeństwa IT każdą osobę - warto, polecamy!

...