Introduction
Ce tutoriel commence là où **Traitement des formulaires et réduction de notre code** s'est arrêté. Nous avons construit une application de sondage web, et nous allons maintenant créer quelques tests automatisés pour elle.
Présentation des tests automatisés
Qu'est-ce qu'un test automatisé?
Les tests sont des routines qui vérifient le fonctionnement de votre code.
Les tests sont effectués à différents niveaux. Certains tests peuvent s'appliquer à un détail minuscule (une méthode de modèle particulière renvoie-t-elle les valeurs attendues?), tandis que d'autres examinent le fonctionnement global du logiciel (une séquence d'entrées d'utilisateur sur le site produit-elle le résultat souhaité?). Cela ne diffère en rien des tests que vous avez effectués plus tôt dans **Configurer la base de données**, en utilisant le shell pour examiner le comportement d'une méthode, ou en exécutant l'application et en saisissant des données pour vérifier son comportement.
Ce qui est différent dans les tests automatisés est que le travail de test est effectué pour vous par le système. Vous créez une série de tests une fois, puis, lorsque vous apportez des modifications à votre application, vous pouvez vérifier que votre code fonctionne toujours comme vous l'avez initialement prévu, sans avoir à effectuer de tests manuels fastidieux.
Pourquoi vous devez créer des tests
Alors pourquoi créer des tests, et pourquoi maintenant?
Vous pouvez penser que vous avez déjà assez à faire en apprenant Python/Django, et qu'avoir une autre chose à apprendre et à faire peut sembler écrasant et peut-être inutile. Après tout, notre application de sondages fonctionne parfaitement pour le moment; prendre la peine de créer des tests automatisés ne la fera pas fonctionner mieux. Si la création de l'application de sondages est la dernière partie de la programmation Django que vous allez jamais faire, alors, effectivement, vous n'avez pas besoin de savoir comment créer des tests automatisés. Mais, si ce n'est pas le cas, maintenant est un excellent moment d'apprendre.
Les tests vous économiseront du temps
Jusqu'à un certain point, "vérifier que cela semble fonctionner" sera un test satisfaisant. Dans une application plus sophistiquée, vous pouvez avoir des dizaines d'interactions complexes entre les composants.
Un changement dans l'un de ces composants pourrait avoir des conséquences inattendues sur le comportement de l'application. Vérifier qu'elle semble toujours fonctionner pourrait signifier parcourir la fonctionnalité de votre code avec vingt variations différentes de vos données de test pour vous assurer que vous n'avez pas cassé quelque chose - pas une bonne utilisation de votre temps.
Cela est particulièrement vrai lorsque des tests automatisés pourraient le faire pour vous en quelques secondes. Si quelque chose ne va pas, les tests vous aideront également à identifier le code qui cause le comportement inattendu.
Parfois, il peut sembler pénible de vous détourner de votre travail de programmation productive et créative pour affronter la tâche fastidieuse et non passionnante d'écrire des tests, en particulier lorsque vous savez que votre code fonctionne correctement.
Cependant, la tâche d'écrire des tests est bien plus gratifiante que de passer des heures à tester manuellement votre application ou à essayer d'identifier la cause d'un problème nouvellement introduit.
Les tests ne servent pas seulement à identifier les problèmes, ils les préviennent
Il est une erreur de considérer les tests seulement comme un aspect négatif du développement.
Sans tests, le but ou le comportement attendu d'une application peut être assez opaque. Même lorsque c'est votre propre code, vous allez parfois vous retrouver à fouiller dedans pour essayer de comprendre exactement ce qu'il fait.
Les tests changent cela; ils illuminent votre code de l'intérieur, et lorsqu'il y a un problème, ils mettent en lumière la partie qui a rencontré un problème - même si vous n'avez même pas réalisé qu'il y avait un problème.
Les tests rendent votre code plus attrayant
Vous pouvez avoir créé un excellent logiciel, mais vous constaterez que de nombreux autres développeurs refuseront de le regarder car il manque de tests; sans tests, ils ne le feront pas confiance. Jacob Kaplan-Moss, l'un des premiers développeurs de Django, dit : "Le code sans tests est cassé par conception."
Le fait que d'autres développeurs veuillent voir des tests dans votre logiciel avant de le prendre au sérieux est une autre raison pour laquelle vous devriez commencer à écrire des tests.
Les tests aident les équipes à travailler ensemble
Les points précédents sont écrits du point de vue d'un seul développeur maintenant une application. Les applications complexes seront maintenues par des équipes. Les tests garantissent que vos collègues ne cassent pas accidentellement votre code (et que vous ne le cassiez pas non plus sans le savoir). Si vous voulez gagner votre vie en tant que programmeur Django, vous devez être bon à écrire des tests!
Stratégies de test de base
Il existe de nombreuses façons d'aborder l'écriture de tests.
Certains programmeurs suivent une discipline appelée "développement guidé par les tests"; ils écrivent en fait leurs tests avant d'écrire leur code. Cela peut sembler contre-intuitif, mais en fait, c'est similaire à ce que la plupart des gens font souvent de toute façon: ils décrivent un problème, puis créent du code pour le résoudre. Le développement guidé par les tests formalise le problème dans un cas de test Python.
Plus souvent, un débutant dans les tests créera du code et décidera plus tard qu'il devrait avoir des tests. Peut-être aurait-il été mieux d'écrire quelques tests plus tôt, mais il n'est jamais trop tard pour commencer.
Parfois, il est difficile de savoir par où commencer à écrire des tests. Si vous avez écrit plusieurs milliers de lignes de Python, choisir quelque chose à tester peut ne pas être facile. Dans un tel cas, il est fructueux d'écrire votre premier test la prochaine fois que vous apportez une modification, que ce soit lorsqu vous ajoutez une nouvelle fonctionnalité ou que vous corrigez un bogue.
Alors commençons tout de suite.
Écrire notre premier test
Nous identifions un bogue
Heureusement, il y a un petit bogue dans l'application polls que nous pouvons corriger immédiatement : la méthode Question.was_published_recently() renvoie True si la Question a été publiée dans le dernier jour (ce qui est correct), mais également si le champ pub_date de la Question est dans l'avenir (ce qui n'est certainement pas le cas).
Confirmez le bogue en utilisant le shell pour vérifier la méthode sur une question dont la date est dans l'avenir :
cd ~/project/mysite
python manage.py shell
>>> import datetime
>>> from django.utils import timezone
>>> from polls.models import Question
>>> ## créez une instance de Question avec pub_date dans 30 jours
>>> future_question = Question(pub_date=timezone.now() + datetime.timedelta(days=30))
>>> ## a-t-elle été publiée récemment?
>>> future_question.was_published_recently()
True
Puisque les choses de l'avenir ne sont pas "récentes", c'est clairement faux.
Créer un test pour démasquer le bogue
Ce que nous venons de faire dans le shell pour tester le problème est exactement ce que nous pouvons faire dans un test automatisé, donc transformons cela en un test automatisé.
Un emplacement conventionnel pour les tests d'une application est dans le fichier tests.py de l'application ; le système de test trouvera automatiquement les tests dans tout fichier dont le nom commence par test.
Placez le code suivant dans le fichier tests.py de l'application polls :
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() renvoie False pour les questions dont le pub_date
est dans l'avenir.
"""
time = timezone.now() + datetime.timedelta(days=30)
future_question = Question(pub_date=time)
self.assertIs(future_question.was_published_recently(), False)
Ici, nous avons créé une sous-classe de django.test.TestCase avec une méthode qui crée une instance de Question avec un pub_date dans l'avenir. Nous vérifions ensuite la sortie de was_published_recently() - qui devrait être False.
Exécuter les tests
Dans le terminal, nous pouvons exécuter notre test :
python manage.py test polls
Et vous verrez quelque chose comme :
[object Object]
Erreur différente?
Si au lieu de cela vous obtenez une NameError ici, vous avez peut-être omis une étape dans la partie 2 datetime et timezone à polls/models.py. Copiez les importations de cette section et essayez de lancer à nouveau vos tests.
Voici ce qui s'est passé :
manage.py test pollsa cherché des tests dans l'applicationpolls- il a trouvé une sous-classe de la classe
django.test.TestCase - il a créé une base de données spéciale à des fins de test
- il a cherché des méthodes de test - celles dont les noms commencent par
test - dans
test_was_published_recently_with_future_questionil a créé une instance deQuestiondont le champpub_dateest dans 30 jours -... et en utilisant la méthodeassertIs(), il a découvert que sawas_published_recently()renvoieTrue, alors que nous souhaitions qu'elle renvoieFalse
Le test nous informe de quel test a échoué et même de la ligne sur laquelle l'échec s'est produit.
Corriger le bogue
Nous savons déjà quel est le problème : Question.was_published_recently() devrait renvoyer False si son pub_date est dans l'avenir. Modifiez la méthode dans models.py de sorte qu'elle ne renvoie True que si la date est également dans le passé :
def was_published_recently(self):
now = timezone.now()
return now - datetime.timedelta(days=1) <= self.pub_date <= now
Et exécutez le test à nouveau :
Création de la base de données de test pour l'alias 'default'...
La vérification systématique n'a identifié aucun problème (0 silencés).
.
----------------------------------------------------------------------
1 test exécuté en 0,001s
OK
Destruction de la base de données de test pour l'alias 'default'...
Après avoir identifié un bogue, nous avons écrit un test qui le démasque et corrigé le bogue dans le code de sorte que notre test passe.
De nombreuses autres choses pourraient se produire dans notre application à l'avenir, mais nous pouvons être sûrs que nous ne réintroduirons pas accidentellement ce bogue, car exécuter le test nous avertira immédiatement. Nous pouvons considérer cette petite partie de l'application comme étant solidement fixée pour toujours.
Tests plus complets
Pendant que nous sommes là, nous pouvons préciser davantage la méthode was_published_recently() ; en fait, il serait plutôt gênant si en corrigant un bogue nous avions introduit un autre.
Ajoutez deux autres méthodes de test à la même classe pour tester le comportement de la méthode de manière plus complète :
def test_was_published_recently_with_old_question(self):
"""
was_published_recently() renvoie False pour les questions dont le pub_date
est antérieur à 1 jour.
"""
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() renvoie True pour les questions dont le pub_date
est dans le dernier jour.
"""
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)
Et maintenant, nous avons trois tests qui confirment que Question.was_published_recently() renvoie des valeurs sensées pour les questions passées, récentes et futures.
Encore une fois, polls est une application minimale, mais quelle que soit la complexité qu'elle atteint à l'avenir et quelle que soit l'autre code avec lequel elle interagit, nous avons maintenant une certaine garantie que la méthode pour laquelle nous avons écrit des tests se comportera comme prévu.
Tester une vue
L'application de sondages est assez indiscriminée : elle publiera n'importe quelle question, y compris celles dont le champ pub_date est dans l'avenir. Nous devrions améliorer cela. Fixer une date de publication dans l'avenir devrait signifier que la question est publiée à ce moment-là, mais invisible jusqu'à ce moment.
Un test pour une vue
Lorsque nous avons corrigé le bogue ci-dessus, nous avons écrit le test d'abord puis le code pour le corriger. En fait, c'était un exemple de développement guidé par les tests, mais il n'est pas vraiment important dans quel ordre nous effectuons le travail.
Dans notre premier test, nous avons concentré notre attention sur le comportement interne du code. Pour ce test, nous voulons vérifier son comportement tel qu'il serait perçu par un utilisateur via un navigateur web.
Avant d'essayer de corriger quoi que ce soit, jetons un coup d'œil aux outils dont nous disposons.
Le client de test de Django
Django fournit un client de test ~django.test.Client pour simuler un utilisateur interagissant avec le code au niveau de la vue. Nous pouvons l'utiliser dans tests.py ou même dans le shell.
Nous allons recommencer avec le shell, où nous devons faire quelques choses qui ne seront pas nécessaires dans tests.py. La première est de configurer l'environnement de test dans le shell :
python manage.py shell
>>> from django.test.utils import setup_test_environment
>>> setup_test_environment()
~django.test.utils.setup_test_environment installe un afficheur de gabarit qui nous permettra d'examiner certains attributs supplémentaires sur les réponses telles que response.context qui ne seraient pas disponibles autrement. Notez que cette méthode ne configure pas une base de données de test, donc ce qui suit sera exécuté sur la base de données existante et la sortie peut différer légèrement selon les questions que vous avez déjà créées. Vous pouvez obtenir des résultats inattendus si votre TIME_ZONE dans settings.py n'est pas correcte. Si vous ne vous souvenez pas de l'avoir configurée plus tôt, vérifiez-la avant de continuer.
Ensuite, nous devons importer la classe de client de test (plus tard dans tests.py nous utiliserons la classe django.test.TestCase, qui a son propre client, donc cela ne sera pas nécessaire) :
>>> from django.test import Client
>>> ## créez une instance du client pour notre utilisation
>>> client = Client()
Avec tout cela prêt, nous pouvons demander au client de faire quelques travaux pour nous :
>>> ## obtenez une réponse de '/'
>>> response = client.get("/")
Page introuvable : /
>>> ## nous devrions nous attendre à un 404 de cette adresse ; si au lieu de cela vous voyez une
>>> ## erreur "Invalid HTTP_HOST header" et une réponse 400, vous avez probablement
>>> ## omis l'appel à setup_test_environment() décrit plus tôt.
>>> response.status_code
404
>>> ## d'un autre côté, nous devrions trouver quelque chose à '/polls/'
>>> ## nous utiliserons'reverse()' plutôt qu'une URL codée en dur
>>> 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's up?</a></li>\n \n </ul>\n\n'
>>> response.context["latest_question_list"]
<QuerySet [<Question: What's up?>]>
Amélioration de notre vue
La liste des sondages montre des sondages qui ne sont pas encore publiés (c'est-à-dire ceux qui ont une date de publication dans l'avenir). Corrigeons cela.
Dans **Traitement des formulaires et réduction de notre code**, nous avons introduit une vue basée sur une classe, basée sur ~django.views.generic.list.ListView :
class IndexView(generic.ListView):
template_name = "polls/index.html"
context_object_name = "latest_question_list"
def get_queryset(self):
"""Retourne les cinq dernières questions publiées."""
return Question.objects.order_by("-pub_date")[:5]
Nous devons modifier la méthode get_queryset() et la changer de sorte qu'elle vérifie également la date en la comparant avec timezone.now(). Tout d'abord, nous devons ajouter une importation :
from django.utils import timezone
puis nous devons modifier la méthode get_queryset comme ceci :
def get_queryset(self):
"""
Retourne les cinq dernières questions publiées (sans inclure celles qui sont
programmées pour être publiées dans l'avenir).
"""
return Question.objects.filter(pub_date__lte=timezone.now()).order_by("-pub_date")[
:5
]
Question.objects.filter(pub_date__lte=timezone.now()) renvoie un ensemble de requêtes contenant des Question dont la pub_date est inférieure ou égale à - c'est-à-dire antérieure ou égale à - timezone.now.
Tester notre nouvelle vue
Maintenant, vous pouvez vous assurer que cela se comporte comme prévu en démarrant runserver, en chargeant le site dans votre navigateur, en créant des Questions avec des dates passées et futures, et en vérifiant que seules celles qui ont été publiées sont listées. Vous ne voulez pas devoir le faire chaque fois que vous apportez un changement qui pourrait affecter cela - donc créons également un test, basé sur notre session shell ci-dessus.
Ajoutez le code suivant à polls/tests.py :
from django.urls import reverse
et nous créerons une fonction raccourcie pour créer des questions ainsi qu'une nouvelle classe de test :
def create_question(question_text, days):
"""
Crée une question avec le `question_text` donné et publiée avec un décalage
de `days` par rapport à maintenant (négatif pour les questions publiées
dans le passé, positif pour les questions qui n'ont pas encore été publiées).
"""
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):
"""
Si aucune question n'existe, un message approprié est affiché.
"""
response = self.client.get(reverse("polls:index"))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Aucun sondage n'est disponible.")
self.assertQuerySetEqual(response.context["latest_question_list"], [])
def test_past_question(self):
"""
Les questions avec une pub_date dans le passé sont affichées sur
la page d'accueil.
"""
question = create_question(question_text="Question passée.", days=-30)
response = self.client.get(reverse("polls:index"))
self.assertQuerySetEqual(
response.context["latest_question_list"],
[question],
)
def test_future_question(self):
"""
Les questions avec une pub_date dans l'avenir ne sont pas affichées sur
la page d'accueil.
"""
create_question(question_text="Question future.", days=30)
response = self.client.get(reverse("polls:index"))
self.assertContains(response, "Aucun sondage n'est disponible.")
self.assertQuerySetEqual(response.context["latest_question_list"], [])
def test_future_question_and_past_question(self):
"""
Même si des questions passées et futures existent, seule la question passée
est affichée.
"""
question = create_question(question_text="Question passée.", days=-30)
create_question(question_text="Question future.", days=30)
response = self.client.get(reverse("polls:index"))
self.assertQuerySetEqual(
response.context["latest_question_list"],
[question],
)
def test_two_past_questions(self):
"""
La page d'accueil des questions peut afficher plusieurs questions.
"""
question1 = create_question(question_text="Question passée 1.", days=-30)
question2 = create_question(question_text="Question passée 2.", days=-5)
response = self.client.get(reverse("polls:index"))
self.assertQuerySetEqual(
response.context["latest_question_list"],
[question2, question1],
)
Regardons de plus près certains d'entre eux.
Tout d'abord, il y a une fonction raccourcie pour les questions, create_question, pour éviter de répéter le processus de création de questions.
test_no_questions ne crée pas de questions, mais vérifie le message : "Aucun sondage n'est disponible." et vérifie que latest_question_list est vide. Notez que la classe django.test.TestCase fournit quelques méthodes d'assertion supplémentaires. Dans ces exemples, nous utilisons ~django.test.SimpleTestCase.assertContains() et ~django.test.TransactionTestCase.assertQuerySetEqual().
Dans test_past_question, nous créons une question et vérifions qu'elle apparaît dans la liste.
Dans test_future_question, nous créons une question avec une pub_date dans l'avenir. La base de données est réinitialisée pour chaque méthode de test, donc la première question n'est plus là, et donc à nouveau l'index ne devrait pas avoir de questions.
Et ainsi de suite. En fait, nous utilisons les tests pour raconter une histoire d'entrée d'administrateur et d'expérience utilisateur sur le site, et vérifions que à chaque état et pour chaque nouveau changement d'état du système, les résultats attendus sont publiés.
Tester la DetailView
Ce que nous avons fonctionne bien ; cependant, même si les questions futures n'apparaissent pas dans l'index, les utilisateurs peuvent toujours y accéder s'ils connaissent ou devinent l'URL correcte. Nous devons donc ajouter une contrainte similaire à DetailView :
class DetailView(generic.DetailView):
...
def get_queryset(self):
"""
Exclut toute question qui n'est pas encore publiée.
"""
return Question.objects.filter(pub_date__lte=timezone.now())
Nous devrions ensuite ajouter quelques tests pour vérifier qu'une Question dont la pub_date est dans le passé peut être affichée, et qu'une question avec une pub_date dans l'avenir ne l'est pas :
class QuestionDetailViewTests(TestCase):
def test_future_question(self):
"""
La vue détaillée d'une question avec une pub_date dans l'avenir
renvoie une erreur 404 non trouvée.
"""
future_question = create_question(question_text="Question future.", 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):
"""
La vue détaillée d'une question avec une pub_date dans le passé
affiche le texte de la question.
"""
past_question = create_question(question_text="Question passée.", days=-5)
url = reverse("polls:detail", args=(past_question.id,))
response = self.client.get(url)
self.assertContains(response, past_question.question_text)
Idées pour plus de tests
Nous devrions ajouter une méthode get_queryset similaire à ResultsView et créer une nouvelle classe de test pour cette vue. Cela sera très similaire à ce que nous venons de créer ; en fait, il y aura beaucoup de répétition.
Nous pourrions également améliorer notre application d'autres manières, en ajoutant des tests le long du chemin. Par exemple, il est absurde que des Questions puissent être publiées sur le site sans avoir de Choices. Donc, nos vues pourraient vérifier cela et exclure de telles Questions. Nos tests créeraient une Question sans Choices puis testeraient qu'elle n'est pas publiée, ainsi que créer une Question similaire avec Choices, et testeraient qu'elle est publiée.
Peut-être que les administrateurs connectés devraient être autorisés à voir les Questions non publiées, mais pas les visiteurs ordinaires. Encore une fois : tout ce qui doit être ajouté au logiciel pour accomplir cela devrait être accompagné d'un test, que vous écriviez le test d'abord puis que vous fassiez passer le code au test, ou que vous élaboriez d'abord la logique dans votre code puis écriviez un test pour la prouver.
À un certain moment, vous allez certainement regarder vos tests et vous demander si votre code souffre d'un gonflement de tests, ce qui nous amène à :
Lorsque l'on teste, plus c'est mieux
Il peut sembler que nos tests prennent de l'ampleur. À ce rythme, il y aura bientôt plus de code dans nos tests que dans notre application, et la répétition est esthétiquement déplaisante, comparativement à la concision élégante du reste de notre code.
Cela ne compte pas. Laissez-les grandir. Dans la plupart des cas, vous pouvez écrire un test une fois et puis l'oublier. Il continuera de jouer son rôle utile tandis que vous continuez à développer votre programme.
Parfois, les tests devront être mis à jour. Supposons que nous modifions nos vues de sorte que seules les Questions avec des Choices soient publiées. Dans ce cas, de nombreux tests existants échoueront - nous informant exactement quels tests doivent être amendés pour être à jour, de sorte que dans cette mesure, les tests aident à s'autogérer.
Au pire, au fur et à mesure que vous continuez à développer, vous pouvez constater que vous avez certains tests qui sont maintenant redondants. Même cela n'est pas un problème ; en testant, la redondance est une bonne chose.
Tant que vos tests sont correctement organisés, ils ne deviendront pas impraticables. De bons principes pratiques incluent :
- une
TestClassséparée pour chaque modèle ou vue - une méthode de test séparée pour chaque ensemble de conditions que vous voulez tester
- des noms de méthodes de test qui décrivent leur fonction
Tests supplémentaires
Ce tutoriel ne présente que quelques bases du test. Vous pouvez faire beaucoup plus, et disposez d'un certain nombre d'outils très utiles pour réaliser des choses très astucieuses.
Par exemple, alors que nos tests ici ont couvert une partie de la logique interne d'un modèle et la manière dont nos vues publient des informations, vous pouvez utiliser un framework "en navigateur" tel que Selenium pour tester la manière dont votre HTML est effectivement affiché dans un navigateur. Ces outils vous permettent de vérifier non seulement le comportement de votre code Django, mais également, par exemple, celui de votre JavaScript. C'est assez incroyable de voir les tests lancer un navigateur et commencer à interagir avec votre site, comme si une personne humaine le pilotait! Django inclut ~django.test.LiveServerTestCase pour faciliter l'intégration avec des outils comme Selenium.
Si vous avez une application complexe, vous pouvez souhaiter exécuter les tests automatiquement avec chaque commit dans le cadre de la continuous integration, de sorte que le contrôle de qualité soit lui-même - au moins en partie - automatisée.
Un bon moyen de repérer les parties non testées de votre application est de vérifier la couverture de code. Cela aide également à identifier le code fragile ou même inutilisé. Si vous ne pouvez pas tester un morceau de code, cela signifie généralement que le code devrait être refactorisé ou supprimé. La couverture vous aidera à identifier le code inutilisé. Consultez topics-testing-code-coverage pour plus de détails.
Testing in Django </topics/testing/index> contient des informations complètes sur les tests.
Sommaire
Félicitations! Vous avez terminé le laboratoire Créer des tests automatisés. Vous pouvez pratiquer d'autres laboratoires sur LabEx pour améliorer vos compétences.