# Τεκμηρίωση του Django - 3.2.11.dev

# Γράφοντας το πρώτο σας Django app, μέρος 5

Ο οδηγός αυτός ξεκινά εκεί που τελειώνει ο οδηγός 4. Έχουμε φτιάξει μια εφαρμογή ψηφοφορίας (Web-poll application) και τώρα θα δημιουργήσουμε μερικά αυτοματοποιημένα τεστ για αυτήν.

Που να ψάξετε για βοήθεια

If you’re having trouble going through this tutorial, please head over to the Getting Help section of the FAQ.

## Εισαγωγή στα αυτοματοποιημένα τεστ

### Τι είναι τα αυτοματοποιημένα τεστ;

Tests are routines that check the operation of your code.

Το τεστ δραστηριοποιείται σε διαφορετικά επίπεδα. Μερικά τεστ μπορεί να εφαρμοστούν σε μια μικρή λεπτομέρεια (επιστρέφει μια συγκεκριμένη μέθοδος ενός μοντέλου τις αναμενόμενες τιμές;) ενώ άλλα εξετάζουν την συνολική εικόνα λειτουργίας του λογισμικού (παράγει το επιθυμητό αποτέλεσμα η σειρά των inputs ενός χρήστη στο site;). Αυτό δεν είναι κάτι διαφορετικό από το τεστ που κάνατε νωρίτερα στον Οδηγό 2, χρησιμοποιώντας την εντολή `shell` για να εξετάσετε τη συμπεριφορά των μεθόδων ή να τρέξετε την εφαρμογή και κατά την αλληλεπίδραση μαζί της, να δείτε πως συμπεριφέρεται.

Αυτό που διαφέρει στα αυτοματοποιημένα τεστ είναι ότι η δουλειά των τεστ γίνεται για σας από το σύστημα. Δημιουργείτε μια φορά ένα σετ από τεστ και μετά καθώς η εφαρμογή σας αλλάζει, μπορείτε να ελέγξετε ότι ο κώδικας σας δουλεύει όπως αρχικά είχατε σχεδιάσει χωρίς να χρονοτριβείτε με χειροκίνητα τεστ.

### Γιατί χρειάζεται να δημιουργείτε τεστ

Γιατί, λοιπόν, δημιουργούμε τεστ και γιατί τώρα;

Ίσως να αισθάνεστε ότι έχετε αρκετά στο κεφάλι σας ήδη (μαθαίνετε Python, μαθαίνετε Django) για να σας προστεθεί άλλη μια γνώση η οποία, σκέφτεστε, ίσως σας φανεί περιττή ή ακόμα και άχρηστη. Παρ’ όλ’ αυτά η εφαρμογή μας λειτουργεί περίφημα. Γιατί να μπούμε στον κόπο να φτιάξουμε αυτοματοποιημένα τεστ; Δεν θα κάνει την εφαρμογή μας καλύτερη, έτσι δεν είναι; Αν είναι να φτιάξετε μόνο αυτή την εφαρμογή με το Django και μετά δεν ασχοληθείτε άλλο, τότε ναι, δεν χρειάζεται να γνωρίζετε πως να φτιάξετε αυτοματοποιημένα τεστ. Αλλά αν δεν είναι έτσι, τώρα είναι μια καλή στιγμή να μάθετε πως.

#### Τα τεστ θα σας γλιτώσουν πολύτιμο χρόνο

Μέχρι ένα σημείο η λογική του “τεστάρω για να δω αν λειτουργεί” θα δουλεύει μια χαρά. Σε πιο εκλεπτυσμένες-περίπλοκες εφαρμογές μπορεί να έχετε δεκάδες, αν όχι εκατοντάδες, αλληλεπιδράσεις μεταξύ των οντοτήτων σας (models, views κλπ) και η παραπάνω λογική να μην μπορεί πλέον να σας φανεί χρήσιμη.

A change in any of those components could have unexpected consequences on the application’s behavior. Checking that it still “seems to work” could mean running through your code’s functionality with twenty different variations of your test data to make sure you haven’t broken something - not a good use of your time.

Αυτό είναι αλήθεια όταν τα αυτοματοποιημένα τεστ μπορούν να κάνουν την παραπάνω δουλειά για εσάς, σε δευτερόλεπτα. Αν κάτι πάει στραβά, τα τεστ θα σας βοηθήσουν στην αναγνώριση του κώδικα που δεν είχε την αναμενόμενη συμπεριφορά (δεν έτρεξε, δηλαδή, όπως του είχατε υπαγορεύσει).

Καμιά φορά ίσως σας φανεί ως αγγαρεία να ξεφύγετε από το παραγωγικό και δημιουργικό κομμάτι της δουλειάς σας μόνο και μόνο για να μπείτε στη διαδικασία γραφής των ανιαρών τεστ, ειδικά όταν γνωρίζετε ότι η εφαρμογή σας λειτουργεί ορθά.

Από την άλλη, το κομμάτι της συγγραφής των τεστ θα προσθέσει μεγαλύτερη αξιοπιστία στον κώδικα σας από το να τεστάρετε τον κώδικα χειροκίνητα ή να προσπαθείτε να αναγνωρίσετε τη πηγή ενός νεοεισερχόμενου προβλήματος.

#### Τα τεστ όχι μόνο αναγνωρίζουν τα προβλήματα αλλά τα εμποδίζουν επίσης

Είναι λάθος να νομίζετε ότι τα τεστ συμβάλλουν αρνητικά στην ανάπτυξη των εφαρμογών.

Φανταστείτε τους προγραμματιστές να μην είχαν στα εργαλεία τους τα τεστ. Χωρίς αυτά, ο στόχος ή η αναμενόμενη συμπεριφορά μιας εφαρμογής θα ήταν θολή-σκοτεινή. Ακόμα και αν είναι δικός σας κώδικας, θα χρειαστεί, μερικές φορές, να ψάξετε μέσα στον κώδικα για να διαπιστώσετε τι κάνει.

Τα τεστ τα αλλάζουν όλα αυτά. Φωτίζουν τον κώδικα σας από μέσα και όταν κάτι πάει στραβά, ξέρετε ακριβώς ποιο ήταν αυτό το κομμάτι, ακόμη και αν δεν είχατε συνειδητοποιήσει ότι αυτό το κομμάτι πήγε στραβά.

#### Τα τεστ κάνουν τον κώδικα σας πιο προσιτό

You might have created a brilliant piece of software, but you will find that many other developers will refuse to look at it because it lacks tests; without tests, they won’t trust it. Jacob Kaplan-Moss, one of Django’s original developers, says «Code without tests is broken by design.»

Προκειμένου άλλοι προγραμματιστές (developers) να δουν σοβαρά το δικό σας software, θα πρέπει να έχετε υλοποιήσει (γράψει) τα απαραίτητα τεστ.

#### Τα τεστ βοηθούν τις ομάδες να συνεργαστούν

Οι προηγούμενες παράγραφοι γράφτηκαν από την σκοπιά ενός και μόνου developer ο οποίος διατηρεί ένα application. Οι περίπλοκες εφαρμογγές, όμως, θα διατηρούνται από ομάδες. Τα τεστ, εγγυώνται ότι οι συμμετέχοντες στην ομάδα δεν θα χαλάσουν (break) τον κώδικα σας ακούσια αλλά ούτε και εσείς θα χαλάσετε τον δικό του, ακούσια πάλι. Αν θέλετε να βγάλετε τα προς το ζην όντας ένας Django programmer, θα πρέπει να είστε καλός στη συγγραφή των τεστ!

## Βασικές στρατηγικές τεστ

Υπάρχουν πολλοί τρόποι για να προσεγγίσει κανείς τη συγγραφή των τεστ.

Some programmers follow a discipline called «test-driven development»; they actually write their tests before they write their code. This might seem counter-intuitive, but in fact it’s similar to what most people will often do anyway: they describe a problem, then create some code to solve it. Test-driven development formalizes the problem in a Python test case.

Πιο συχνά, όσοι είναι νέοι στο χώρο των τεστ θα γράψουν πρώτα τον κώδικα και μετά θα αποφασίσουν ότι θα έπρεπε να είχαν γράψει μερικά τεστ. Ίσως θα ήταν καλύτερα να είχαν γραφτεί τα τεστ νωρίτερα, αλλά ποτέ δεν είναι αργά για να ξεκινήσει κανείς.

Μερικές φορές δεν ξέρεις πως να ξεκινήσεις να γράφεις τεστ. Αν έχετε γράψει μερικές χιλιάδες γραμμές Python κώδικα, είναι δύσκολο μετά να επιλέξεις κάτι για να γράψεις τεστ πάνω σε αυτό. Σε τέτοιες περιπτώσεις είναι πιο παραγωγικό να γράψετε το πρώτο σας τεστ κάθε φορά που κάνετε μια αλλαγή, είτε προσθέτετε κάποιο καινούργιο feature είτε διορθώνετε ένα bug.

Ας ξεκινήσουμε λοιπόν, αμέσως τώρα.

## Γράφοντας το πρώτο σας τεστ

### Βρίσκουμε ένα bug

Ευτυχώς, υπάρχει ένα μικρό bug στην εφαρμογή μας (`polls`) και αξίζει να το διορθώσουμε ευθύς αμέσως: η μέθοδος `Question.was_published_recently()` επιστρέφει `True` αν η ερώτηση (`Question`) εκδόθηκε εντός εικοσιτεσσάρων ωρών (το οποίο είναι σωστό). Η ίδια μέθοδος επιστρέφει πάλι `True` αν το `pub_date` τοποθετείται στο μέλλον (κάτι το οποίο δεν μπορεί να ισχύει).

Confirm the bug by using the `shell` to check the method on a question whose date lies in the future:

/ 

    $ python manage.py shell

    ...\> py manage.py shell

    >>> import datetime
    >>> from django.utils import timezone
    >>> from polls.models import Question
    >>> # create a Question instance with pub_date 30 days in the future
    >>> future_question = Question(pub_date=timezone.now() + datetime.timedelta(days=30))
    >>> # was it published recently?
    >>> future_question.was_published_recently()
    True

Εφόσον τα πράγματα στο μέλλον δε θεωρούνται “πρόσφατα”, τότε αυτό είναι ξεκάθαρα λάθος.

### Δημιουργία ενός τεστ για την ανάδειξη του bug

Ότι ακριβώς κάναμε στο `shell` για να τεστάρουμε το πρόβλημα, θα το κάνουμε στο αυτοματοποιημένο τεστ που θα γράψουμε αμέσως τώρα.

Ένα βολικό μέρος για να κρατάτε όλα τα τεστ για την εφαρμογή σας είναι σε ένα ξεχωριστό αρχείο με το όνομα `tests.py`. Το σύστημα (testing system) θα βρει αυτόματα όλα τα τεστ, αρκεί καθένα από αυτά να ξεκινούν με τη λέξη `test`.

Γράψτε τον ακόλουθο κώδικα στο αρχείο `tests.py` μέσα στην εφαρμογή `polls`:

polls/tests.py

    import datetime

    from django.test import TestCase
    from django.utils import timezone

    from .models import Question

    class QuestionModelTests(TestCase):

        def test_was_published_recently_with_future_question(self):
            """
            was_published_recently() returns False for questions whose pub_date
            is in the future.
            """
            time = timezone.now() + datetime.timedelta(days=30)
            future_question = Question(pub_date=time)
            self.assertIs(future_question.was_published_recently(), False)

Here we have created a `django.test.TestCase` subclass with a method that creates a `Question` instance with a `pub_date` in the future. We then check the output of `was_published_recently()` - which ought to be False.

### Τρέχοντας τα τεστ

In the terminal, we can run our test:

/ 

    $ python manage.py test polls

    ...\> py manage.py test polls

και θα δείτε κάτι σαν αυτό:

    Creating test database for alias 'default'...
    System check identified no issues (0 silenced).
    F
    ======================================================================
    FAIL: test_was_published_recently_with_future_question (polls.tests.QuestionModelTests)
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "/path/to/mysite/polls/tests.py", line 16, in test_was_published_recently_with_future_question
        self.assertIs(future_question.was_published_recently(), False)
    AssertionError: True is not False

    ----------------------------------------------------------------------
    Ran 1 test in 0.001s

    FAILED (failures=1)
    Destroying test database for alias 'default'...

Different error?

If instead you’re getting a `NameError` here, you may have missed a step in Part 2 where we added imports of `datetime` and `timezone` to `polls/models.py`. Copy the imports from that section, and try running your tests again.

Αυτά που συνέβησαν είναι τα εξής:

- `manage.py test polls` looked for tests in the `polls` application
- βρήκε μια subclass της κλάσης `django.test.TestCase`
- δημιούργησε μια ειδική database για τους σκοπούς του τεστ
- έψαξε για τεστ μεθόδους - εκείνες των οποίων το όνομα ξεκινά με τη λέξη `test`
- μέσα στην μέθοδο `test_was_published_recently_with_future_question` δημιούργησε ένα `Question` instance του οποίου το `pub_date` field είναι 30 μέρες στο μέλλον
- … και χρησιμοποιώντας τη μέθοδο `assertIs()`, διαπίστωσε ότι η μέθοδος `was_published_recently()` επιστρέφει `True`, αντί για `False`

Το τεστ, μας ενημερώνει ποιο τεστ απέτυχε (τώρα έτυχε να έχουμε μονάχα ένα αλλά στο μέλλον δεν θα έχετε μόνο ένα) και ακόμη και την γραμμή του κώδικα στο τεστ όπου προέκυψε το failure.

### Διορθώνοντας το bug

Γνωρίζουμε ήδη ποιο και που είναι το πρόβλημα: η μέθοδος `Question.was_published_recently()` θα πρέπει να επιστρέψει `False` αν το `pub_date` τοποθετηθεί στο μέλλον. Βελτιώστε τη μέθοδο μέσα στο αρχείο `models.py`, ούτως ώστε να επιστρέφει `True` μόνο για παρελθοντικές ημερομηνίες:

polls/models.py

    def was_published_recently(self):
        now = timezone.now()
        return now - datetime.timedelta(days=1) <= self.pub_date <= now

και τρέξτε τα τεστ ξανά:

    Creating test database for alias 'default'...
    System check identified no issues (0 silenced).
    .
    ----------------------------------------------------------------------
    Ran 1 test in 0.001s

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

Αφού βρήκαμε ένα bug, γράψαμε ένα τεστ το οποίο το αναδεικνύει και τέλος διορθώσαμε το bug στο κώδικα ούτως ώστε το τεστ να θεωρηθεί επιτυχές.

Many other things might go wrong with our application in the future, but we can be sure that we won’t inadvertently reintroduce this bug, because running the test will warn us immediately. We can consider this little portion of the application pinned down safely forever.

### Περισσότερα περιεκτικά τεστ

Όσο είμαστε εδώ μπορούμε να βελτιστοποιήσουμε την μέθοδο `was_published_recently()` λιγάκι περισσότερο. Θα ήταν ντροπιαστικό, αν προσπαθώντας να διορθώσουμε ένα bug να έχουμε δημιουργήσει κάπου αλλού ένα άλλο (άθελα μας).

Προσθέστε δύο ακόμη μεθόδους τεστ στην ίδια κλάση, για να τεστάρουμε την συμπεριφορά της μεθόδου πιο περιεκτικά:

polls/tests.py

    def test_was_published_recently_with_old_question(self):
        """
        was_published_recently() returns False for questions whose pub_date
        is older than 1 day.
        """
        time = timezone.now() - datetime.timedelta(days=1, seconds=1)
        old_question = Question(pub_date=time)
        self.assertIs(old_question.was_published_recently(), False)

    def test_was_published_recently_with_recent_question(self):
        """
        was_published_recently() returns True for questions whose pub_date
        is within the last day.
        """
        time = timezone.now() - datetime.timedelta(hours=23, minutes=59, seconds=59)
        recent_question = Question(pub_date=time)
        self.assertIs(recent_question.was_published_recently(), True)

Τώρα έχουμε τρία τεστ, τα οποία επιβεβαιώνουν ότι η μέθοδος `Question.was_published_recently()` επιστρέφει λογικές τιμές για παρελθοντικές, παρούσες και μελλοντικές τιμές του `pub_date`.

Again, `polls` is a minimal application, but however complex it grows in the future and whatever other code it interacts with, we now have some guarantee that the method we have written tests for will behave in expected ways.

## Τεστ ένα view

Η εφαρμογή μας, `polls`, δεν κάνει διακρίσεις! Θα κάνει publish οποιαδήποτε ερώτηση, συμπεριλαμβανομένων και εκείνων που το `pub_date` field βρίσκεται στο μέλλον. Θα πρέπει να το διορθώσουμε αυτό. Θέτοντας το `pub_date` στο μέλλον θα σημαίνει ότι η ερώτηση θα πρέπει να γίνει publish εκείνη την ημερομηνία αλλά θα είναι αόρατη μέχρι τότε.

### Ένα τεστ για τη view

When we fixed the bug above, we wrote the test first and then the code to fix it. In fact that was an example of test-driven development, but it doesn’t really matter in which order we do the work.

Στο πρώτο μας τεστ επικεντρωθήκαμε στην εσωτερική συμπεριφορά του κώδικα μας (στη μέθοδο του μοντέλου). Για αυτό το τεστ, θέλουμε να ελέγξουμε την συμπεριφορά του όπως θα τη βίωνε ο χρήστης μέσα από τον browser του.

Προτού προσπαθήσουμε να φτιάξουμε το οτιδήποτε, ας ρίξουμε μια ματιά στα εργαλεία που έχουμε διαθέσιμα.

### Τα τεστ του Django από την πλευρά του client

Το Django παρέχει το τεστ `Client` για να εξομοιώνει τον χρήστη που αλληλεπιδρά με τον κώδικα σε επίπεδο view. Μπορούμε να το χρησιμοποιήσουμε στο αρχείο `tests.py` ή ακόμη και στο `shell`.

We will start again with the `shell`, where we need to do a couple of things that won’t be necessary in `tests.py`. The first is to set up the test environment in the `shell`:

/ 

    $ python manage.py shell

    ...\> py manage.py shell

    >>> from django.test.utils import setup_test_environment
    >>> setup_test_environment()

Η μέθοδος `setup_test_environment()` εγκαθιστά έναν template renderer ο οποίος μας επιτρέπει να ερευνήσουμε μερικά πρόσθετα attributes στα responses όπως το `response.context` το οποίο δεν θα ήταν διαθέσιμο αλλιώς. Σημειώστε ότι αυτή η μέθοδος δεν φτιάχνει καμία βάση δεδομένων για τεστ, κάτι το οποίο σημαίνει ότι θα τρέξει χρησιμοποιώντας την ήδη υπάρχουσα database. Επίσης σημειώστε ότι η έξοδος ίσως να είναι διαφορετική για εσάς ανάλογα τις ερωτήσεις που έχετε δημιουργήσει. Ίσως να λάβετε μη αναμενόμενα αποτελέσματα αν η ρύθμιση `TIME_ZONE`, μέσα στο γενικό αρχείο ρυθμίσεων `settings.py`, δεν είναι σωστή. Αν δεν την έχετε ρυθμίσει, κάντε το προτού προχωρήσετε.

Επόμενο βήμα είναι να κάνουμε import την τεστ κλάση για τον client (αργότερα στο αρχείο `tests.py` θα χρησιμοποιήσουμε την κλάση `django.test.TestCase`, η οποία έρχεται με δικό της client, οπότε αυτό δεν θα είναι απαραίτητο):

    >>> from django.test import Client
    >>> # create an instance of the client for our use
    >>> client = Client()

Με αυτό έτοιμο, μπορούμε να βάλουμε τον client να εργαστεί για εμάς:

    >>> # get a response from '/'
    >>> response = client.get('/')
    Not Found: /
    >>> # we should expect a 404 from that address; if you instead see an
    >>> # "Invalid HTTP_HOST header" error and a 400 response, you probably
    >>> # omitted the setup_test_environment() call described earlier.
    >>> response.status_code
    404
    >>> # on the other hand we should expect to find something at '/polls/'
    >>> # we'll use 'reverse()' rather than a hardcoded URL
    >>> from django.urls import reverse
    >>> response = client.get(reverse('polls:index'))
    >>> response.status_code
    200
    >>> response.content
    b'\n    <ul>\n    \n        <li><a href="/polls/1/">What&#x27;s up?</a></li>\n    \n    </ul>\n\n'
    >>> response.context['latest_question_list']
    <QuerySet [<Question: What's up?>]>

### Βελτιώνοντας το view

Η λίστα με τις ψηφοφορίες εμφανίζει ψηφοφορίες οι οποίες δεν έχουν γίνει ακόμη published (δηλαδή αυτές που το `pub_date` έχει μελλοντική ημερομηνία). Ας το διορθώσουμε.

Στον Οδηγό 4 εισάγαμε την έννοια του class-based view, βασισμένο στη `ListView`:

polls/views.py

    class IndexView(generic.ListView):
        template_name = 'polls/index.html'
        context_object_name = 'latest_question_list'

        def get_queryset(self):
            """Return the last five published questions."""
            return Question.objects.order_by('-pub_date')[:5]

Θα χρειαστεί να βελτιώσουμε τη μέθοδο `get_queryset()` και να την αλλάξουμε ούτως ώστε να ελέγχει και την ημερομηνία συγκρίνοντας τη με το `timezone.now()`. Πρώτα θα χρειαστεί να προσθέσουμε ένα import:

polls/views.py

    from django.utils import timezone

και μετά πρέπει να βελτιώσουμε την μέθοδο `get_queryset` ως εξής:

polls/views.py

    def get_queryset(self):
        """
        Return the last five published questions (not including those set to be
        published in the future).
        """
        return Question.objects.filter(
            pub_date__lte=timezone.now()
        ).order_by('-pub_date')[:5]

Η μέθοδος `Question.objects.filter(pub_date__lte=timezone.now())` επιστρέφει ένα queryset (συμπεριφέρεται ως μια Python λίστα) το οποίο περιέχει `Question`s των οποίων το `pub_date` είναι μικρότερο ή ίσο (επειδή στην προκειμένη επρόκειτο για ημερομηνίες, ίσως είναι καλύτερα να πούμε νωρίτερο ή ίσο) από το `timezone.now` (από το τώρα).

### Τεστάροντας το καινούργιο view

Now you can satisfy yourself that this behaves as expected by firing up `runserver`, loading the site in your browser, creating `Questions` with dates in the past and future, and checking that only those that have been published are listed. You don’t want to have to do that every single time you make any change that might affect this - so let’s also create a test, based on our `shell` session above.

Προσθέστε τα ακόλουθα στο αρχείο `polls/tests.py`:

polls/tests.py

    from django.urls import reverse

Θα δημιουργήσουμε μια συνάρτηση συντόμευσης για να φτιάχνουμε ερωτήσεις και θα δημιουργήσουμε μια νέα test class:

polls/tests.py

    def create_question(question_text, days):
        """
        Create a question with the given `question_text` and published the
        given number of `days` offset to now (negative for questions published
        in the past, positive for questions that have yet to be published).
        """
        time = timezone.now() + datetime.timedelta(days=days)
        return Question.objects.create(question_text=question_text, pub_date=time)

    class QuestionIndexViewTests(TestCase):
        def test_no_questions(self):
            """
            If no questions exist, an appropriate message is displayed.
            """
            response = self.client.get(reverse('polls:index'))
            self.assertEqual(response.status_code, 200)
            self.assertContains(response, "No polls are available.")
            self.assertQuerysetEqual(response.context['latest_question_list'], [])

        def test_past_question(self):
            """
            Questions with a pub_date in the past are displayed on the
            index page.
            """
            question = create_question(question_text="Past question.", days=-30)
            response = self.client.get(reverse('polls:index'))
            self.assertQuerysetEqual(
                response.context['latest_question_list'],
                [question],
            )

        def test_future_question(self):
            """
            Questions with a pub_date in the future aren't displayed on
            the index page.
            """
            create_question(question_text="Future question.", days=30)
            response = self.client.get(reverse('polls:index'))
            self.assertContains(response, "No polls are available.")
            self.assertQuerysetEqual(response.context['latest_question_list'], [])

        def test_future_question_and_past_question(self):
            """
            Even if both past and future questions exist, only past questions
            are displayed.
            """
            question = create_question(question_text="Past question.", days=-30)
            create_question(question_text="Future question.", days=30)
            response = self.client.get(reverse('polls:index'))
            self.assertQuerysetEqual(
                response.context['latest_question_list'],
                [question],
            )

        def test_two_past_questions(self):
            """
            The questions index page may display multiple questions.
            """
            question1 = create_question(question_text="Past question 1.", days=-30)
            question2 = create_question(question_text="Past question 2.", days=-5)
            response = self.client.get(reverse('polls:index'))
            self.assertQuerysetEqual(
                response.context['latest_question_list'],
                [question2, question1],
            )

Ας κοιτάξουμε μερικά από τα παραπάνω τεστ πιο προσεκτικά.

Πρώτα, φτιάξαμε μια συνάρτηση (`create_question`) την οποία τη χρησιμοποιούμε ως συντόμευση για να δημιουργούμε ερωτήσεις.

`test_no_questions` doesn’t create any questions, but checks the message: «No polls are available.» and verifies the `latest_question_list` is empty. Note that the `django.test.TestCase` class provides some additional assertion methods. In these examples, we use `assertContains()` and `assertQuerysetEqual()`.

In `test_past_question`, we create a question and verify that it appears in the list.

In `test_future_question`, we create a question with a `pub_date` in the future. The database is reset for each test method, so the first question is no longer there, and so again the index shouldn’t have any questions in it.

Και τα τεστ συνεχίζονται. Στην πραγματικότητα χρησιμοποιούμε τα τεστ για να εξομοιώσουμε τη συμπεριφορά του admin interface καθώς και των υπόλοιπων χρηστών που αλληλεπιδρούν με το site μέσω του browser. Κάθε φορά που αλλάζει το state του συστήματος μας (της εφαρμογής μας αν θέλετε) εξετάζουμε την έξοδο του συστήματος και αν αυτή ανταποκρίνεται στα αποτελέσματα που αναμένουμε.

### Τεστάροντας το `DetailView`

Ότι έχουμε μέχρι τώρα λειτουργεί όπως θα θέλαμε να λειτουργεί. Ωστόσο, παρόλο που οι μελλοντικές ερωτήσεις δεν φαίνονται στην αρχική σελίδα της εφαρμογής μας (index), αν οι χρήστες μπορέσουν να μαντέψουν το σωστό URL που οδηγεί σε μια τέτοια ερώτηση (και δεν είναι κάτι δύσκολο αφού χρησιμοποιούμε IDs), τότε θα μπορέσουν να τη δουν. Άρα θα πρέπει να προσθέσουμε παρόμοιους περιορισμούς στο `DetailView`:

polls/views.py

    class DetailView(generic.DetailView):
        ...
        def get_queryset(self):
            """
            Excludes any questions that aren't published yet.
            """
            return Question.objects.filter(pub_date__lte=timezone.now())

We should then add some tests, to check that a `Question` whose `pub_date` is in the past can be displayed, and that one with a `pub_date` in the future is not:

polls/tests.py

    class QuestionDetailViewTests(TestCase):
        def test_future_question(self):
            """
            The detail view of a question with a pub_date in the future
            returns a 404 not found.
            """
            future_question = create_question(question_text='Future question.', days=5)
            url = reverse('polls:detail', args=(future_question.id,))
            response = self.client.get(url)
            self.assertEqual(response.status_code, 404)

        def test_past_question(self):
            """
            The detail view of a question with a pub_date in the past
            displays the question's text.
            """
            past_question = create_question(question_text='Past Question.', days=-5)
            url = reverse('polls:detail', args=(past_question.id,))
            response = self.client.get(url)
            self.assertContains(response, past_question.question_text)

### Ιδέες για περισσότερα τεστ

Χρωστάμε να προσθέσουμε μια παρόμοια μέθοδο `get_queryset` στο `ResultsView` και να δημιουργήσουμε μια νέα test class για αυτό το view. Θα είναι παρόμοια με αυτή που μόλις δημιουργήσαμε (`DetailView`). Στη πραγματικότητα θα είναι μια επανάληψη.

Θα μπορούσαμε επίσης να βελτιώσουμε την εφαρμογή μας και με άλλους τρόπους προσθέτοντας τεστ καθώς προχωράμε. Για παράδειγμα είναι λιγάκι ανόητο να μπορούν να γίνονται published οι ερωτήσεις (`Questions`) δίχως κάποιες απαντήσεις (`Choices`). Οπότε, τα views θα μπορούσα να ελέγξουν γι’ αυτό και να εξαιρέσουν (exclude) τέτοιες ερωτήσεις. Τα τεστ μας θα δημιουργούσαν μια ερώτηση χωρίς `Choices` και μετά θα έλεγχαν ότι αυτή η ερώτηση δεν θα γινόταν published. Ομοίως θα δημιουργούσαμε μια ερώτηση (σε άλλο τεστ) με `Choices` αυτή τη φορά και ελέγχαμε ότι αυτή η ερώτηση θα γινόταν published.

Ίσως οι logged-in admin χρήστες να επιτρεπόταν να δουν τις unpublished ερωτήσεις, αλλά όχι οι συνηθισμένοι επισκέπτες. Για ακόμη μια φορά: οτιδήποτε χρειάζεται να προστεθεί στο software για να επιτευχθεί ο στόχος σας θα πρέπει να συνοδεύεται από ένα τεστ, είτε γράψετε το test πρώτο και μετά τον κώδικα για να κάνετε το τεστ να επιτύχει είτε γράψετε τον κώδικα πρώτα και μετά το τεστ για να αποδείξετε ότι ο κώδικας σας λειτουργεί.

Θα φτάσετε σε ένα σημείο όπου θα κοιτάζετε τα τεστ σας και θα αναρωτιέστε αν ο κώδικας σας υποφέρει από υπερβολικό αριθμό τεστ, κάτι το οποίο μας φέρνει στο εξής:

## Όταν τεστάρετε, το περισσότερο είναι καλύτερο

Ίσως να φαίνεται ότι τα τεστ αρχίζουν και γίνονται τεράστια καθώς και μη ελέγξιμα. Σύντομα θα διαπιστώσετε ότι υπάρχει περισσότερος κώδικας στα τεστ παρά στην εφαρμογή την ίδια και ότι η επανάληψη είναι αντιαισθητική εν συγκρίσει με την κομψή περιεκτικότητα του υπόλοιπου κώδικα σας.

Δεν πειράζει. Αφήστε το να μεγαλώσει. Τις πιο πολλές φορές θα γράψετε ένα τεστ και μετά θα το ξεχάσετε. Εκείνο όμως θα κάνει τη δουλειά του όσο εσείς συνεχίζετε με την ανάπτυξη της εφαρμογής σας (και γενικά όλου του project σας).

Μερικές φορές τα τεστ θα χρειαστεί να αναβαθμιστούν. Υποθέστε ότι βελτιώνουμε τα views ούτως ώστε μόνο οι ερωτήσεις (`Questions`) που έχουν απαντήσεις (`Choices`) θα γίνουν published. Σε αυτή την περίπτωση πολλά από τα τεστ μας θα αποτύχουν - λέγοντας μας ακριβώς ποια τεστ θα χρειαστεί να βελτιώσουμε για να είναι συμβατά με την εφαρμογή μας. Οπότε όσον αφορά την ανανέωση των τεστ, μην ανησυχείτε, τα τεστ φροντίζουν μόνα τους για τους εαυτούς τους.

Στη χειρότερη, καθώς αναπτύσσετε την εφαρμογή σας, ίσως βρεθείτε σε μια θέση όπου μερικά τεστ είναι πλέον περιττά. Και πάλι μην ανησυχείτε. Στον κόσμο του τεστ ο πλεονασμός (redundancy) είναι καλό στοιχείο.

Όσο τα τεστ σας είναι λογικά οργανωμένα δεν θα γίνουν αδιαχείριστα. Μερικοί κανόνες όσον αφορά τα τεστ είναι:

- μια ξεχωριστή `TestClass` για κάθε μοντέλο ή view
- μια ξεχωριστή μέθοδο τεστ για κάθε κατάσταση που θέλετε να τεστάρετε
- να ονομάζετε τις μεθόδους τεστ όσο πιο περιεκτικά γίνεται σχετικά με αυτό που προσπαθούν να τεστάρουν

## Επιεπλέον τεστ

Αυτό ο οδηγός έκανε μια εισαγωγή στον κόσμο των τεστ. Υπάρχουν πολλά περισσότερα που μπορείτε να κάνετε όπως επίσης και περισσότερα εργαλεία που μπορείτε να χρησιμοποιήσετε για να φτιάξετε πολύ έξυπνα τεστ.

Για παράδειγμα, παρόλο που τα τεστ σε αυτό τον οδηγό κάλυψαν ένα κομμάτι από την εσωτερική λειτουργία του κώδικα μας (μέθοδος μοντέλου) και άλλο ένα από τον τρόπο που τα views κάνουν publish τις ερωτήσεις μας, μπορείτε να χρησιμοποιήσετε ένα «in-browser» framework όπως το Selenium για να τεστάρετε τον τρόπο με τον οποίο η HTML γίνεται render στον browser. Αυτά τα εργαλεία δεν σας επιτρέπουν να ελέγξετε μόνο τη συμπεριφορά του Django κώδικα σας αλλά και του κώδικα π.χ της Javascript (που πολύ πιθανόν να έχετε). Είναι αρκετά συναρπαστικό να μπορείτε να βλέπετε τα τεστ σας να ανοίγουν τον browser σας και να αλληλεπιδρούν με το site ακριβώς όπως ένας άνθρωπος θα το έκανε! Το Django περιλαμβάνει την κλάση `LiveServerTestCase` για να διευκολύνει την ενσωμάτωση (συνεργασία) με εργαλεία όπως το Selenium.

Αν έχετε μια περίπλοκη εφαρμογή ίσως να θέλετε να τρέχουν τα τεστ, αυτόματα, κάθε φορά που κάνετε commit για λόγους του continuous integration, ούτως ώστε το ίδιο το quality control (έλεγχος ποιότητας) - να είναι τουλάχιστον μερικώς - αυτοματοποιημένο.

Ένας καλός τρόπος για να εντοπίσετε τα μέρη της εφαρμογής σας που δεν έχουν γραφτεί τεστ για αυτά, είναι να ελέγξετε το κατά πόσο είναι καλυμμένος ο κώδικας σας με τεστ. Αυτό επίσης σας βοηθά στο να αναγνωρίσετε αδύναμο ή ακόμη και αχρησιμοποίητο-νεκρό κώδικα. Αν δεν μπορείτε να τεστάρετε ένα κομμάτι κώδικα, τότε αυτό ίσως σημαίνει ότι ο κώδικας θα πρέπει να ξαναγραφτεί ή να αφαιρεθεί τελείως. Το Coverage θα σας βοηθήσει (και είναι πολύ σημαντικό αυτό) να αναγνωρίσετε τον νεκρό κώδικα. Δείτε στην αναφορά Integration with coverage.py για περισσότερες λεπτομέρειες.

Επίσης το άρθρο Testing με το Django περιέχει περισσότερες πληροφορίες σχετικά με το testing.

## Επόμενα βήματα

Για περισσότερες πληροφορίες σχετικά με τα τεστ δείτε στο άρθρο Testing με το Django.

Όταν είστε εξοικειωμένοι με το concept και τη λειτουργία των τεστ στο Django, διαβάστε το έκτο μέρος αυτού του οδηγού για να μάθετε περισσότερα σχετικά με τη διαχείριση των στατικών αρχείων (static files).