Categorías
Python

Introducción a los canales de Django

 

Tabla de Contenidos

  • Por qué TDD? Configuración
  • primera prueba
  • Django
  • de control de versiones
  • funcional TestsAdmin LoginSetup los contactos Configuración de aplicaciones
  • Admin Login
  • los contactos de la unidad de aplicaciones
  • TestsViewsMain viewall Contactos ViewAdd Contacto ViewValidationCreate Contacto
  • Vistas
  • Vista principal
  • Todos contactos Ver
  • agregar contacto Ver
  • Validación
  • Crear contacto
  • pruebas funcionales Estructura Redux
  • prueba

  • Conclusión
  • admin Login
  • de configuración la aplicación contactos
  • Vistas
  • Vista principal
  • Todos Ver contactos
  • agregar contacto Ver
  • Validación
  • Crear contacto

Última actualización: – (! vic gracias)

  • 29/01/2014 código actualizado para la forma
  • 12/29/2013 – reestructurado todo entrada de blog

prueba de conducción del Developme nt (TDD) es un ciclo de desarrollo iterativo que hace hincapié en escribir pruebas automatizadas antes de escribir el código real.

El proceso es simple:

Por qué TDD?

Con TDD, aprenderá a romper el código en trozos lógicos, de fácil comprensión, lo que ayuda a garantizar la exactitud de código.

Esto es importante porque es difícil a-

TDD ayuda a solucionar estos problemas. Que no garantiza en absoluto que su código estará libre de error; Sin embargo, usted va a escribir mejor código, que se traduce en una mejor comprensión del código. Esto en sí mismo le ayudará con la eliminación de errores y, al menos, usted será capaz de errores de dirección mucho más fácil.

TDD es prácticamente un estándar de la industria también. charla

suficiente. Vamos a llegar a un código.

Para este tutorial, vamos a ser la creación de una aplicación de contactos almacén del usuario.

Nota: Este tutorial asume que está ejecutando un entorno basado en Unix – por ejemplo, Mac OS X, Linux recta, o Linux VM a través de Windows. También estaré usando Sublime 2 como mi editor de texto. También, asegúrese de que haya completado el tutorial oficial de Django y tener un conocimiento básico del lenguaje Python. Además, en este primer post, sin embargo, no vamos a entrar en algunas de las nuevas herramientas disponibles en Django 1.6. Este post establece las bases para posteriores mensajes que tienen que ver con diferentes formas de pruebas.

primera prueba

Antes de hacer nada tenemos que configura por primera vez una prueba. Para esta prueba sólo queremos hacer que Django es configurado correctamente. Vamos a estar usando una prueba de funcionamiento para esto – lo que vamos a estar explicamos más abajo.

Crear un nuevo directorio para su proyecto:

$ mkdir django-tdd
$ cd django-tdd

configuración Ahora un nuevo directorio para incluir sus pruebas funcionales:

$ mkdir ft
$ cd ft

Crear un nuevo archivo llamado “tests.py” y añadir el siguiente código:

from selenium import webdriver

browser = webdriver.Firefox()
browser.get('http://localhost:8000/')

body = browser.find_element_by_tag_name('body')
assert 'Django' in body.text

browser.quit()

Ahora ejecute el prueba:

$ python tests.py

Asegúrese de que tiene instalado el selenio – pip instalar el selenio

debería ver FireFox pop-up y tratar de navegar a http: // localhost: 8000 /. En su terminal debería ver:

Traceback (most recent call last):
File "tests.py", line 7, in
assert 'Django' in body.text
AssertionError

Felicidades! Usted escribió su primera prueba falla.

Ahora vamos a escribir código sólo lo suficiente como para hacerlo pasar, que simplemente equivale a la creación de un entorno de desarrollo de Django. Configuración

Django

Activar un virtualenv:

$ cd ..
$ virtualenv --no-site-packages env
$ source env/bin/activate

Instalar Django y la configuración de un proyecto:

$ pip install django==1.6.1
$ django-admin.py startproject contacts

Su estructura actual proyecto debe tener este aspecto:

├── contacts
│   ├── contacts
│   │   ├── __init__.py
│   │   ├── settings.py
│   │   ├── urls.py
│   │   └── wsgi.py
│   └── manage.py
└── ft
└── tests.py

Instalar Selenio:

$ pip install selenium==2.39.0

ejecutar el servidor:

$ cd contacts
$ python manage.py runserver

A continuación, abrir una nueva ventana en su terminal, navegue hasta el directorio “pies”, a continuación, ejecutar la prueba:

$ python tests.py

debería ver la ventana de Firefox navegar a http: // localhost: 8000 / nuevo. Esta vez no debe haber errores. Agradable. Que acaba de pasar su primera prueba!

Ahora, la creación de nuestro entorno de desarrollo acabado de let. Control de

Versión

En primer lugar, añadir un “.gitignore” e incluir el código siguiente en el archivo:

.Python
env
bin
lib
include
.DS_Store
.pyc

Ahora crear un repositorio Git y comprometerse:

$ git init
$ git add .
$ git commit -am "initial"

Con la configuración del proyecto, vamos a dar un paso atrás y discutir funcional pruebas.

pruebas funcionales

nos acercamos a la primera prueba a través de selenio a través de pruebas funcionales. Estas pruebas permiten a impulsar el navegador web como si fuéramos el usuario final, para ver cómo la aplicación de hecho funciones . Dado que estas pruebas siguen el comportamiento del usuario final – también llamado una historia de usuario – se trata de la prueba de una serie de características, en lugar de una sola función – que es más apropiado para pruebas unitarias. importante señalar que cuando se prueba código que no has escrito, usted debe comenzar con las pruebas funcionales de Se. Dado que estamos probando en esencia código de Django, pruebas funcionales son el camino correcto a seguir.

Otra forma de pensar funcional vs pruebas unitarias es que las pruebas funcionales se centran en las pruebas de la aplicación desde el exterior, desde la perspectiva del usuario, mientras que las pruebas unitarias se centran en la aplicación desde el interior, desde la perspectiva del desarrollador.

Esto hará mucho más sentido en la práctica.

Antes de continuar, vamos a reestructurar nuestro entorno de pruebas para hacer más fácil la prueba.

En primer lugar, vamos a volver a escribir la primera prueba en el archivo “tests.py”:

from selenium import webdriver
from selenium.webdriver.common.keys import Keys

from django.test import LiveServerTestCase

class AdminTest(LiveServerTestCase):

def setUp(self):
self.browser = webdriver.Firefox()

def tearDown(self):
self.browser.quit()

def test_admin_site(self):
# user opens web browser, navigates to admin page
self.browser.get(self.live_server_url + '/admin/')
body = self.browser.find_element_by_tag_name('body')
self.assertIn('Django administration', body.text)

continuación, se ejecuta:

$ python manage.py test ft

Se debe pasar:

----------------------------------------------------------------------
Ran 1 test in 3.304s

OK

Felicidades!

Antes de continuar, vamos a ver lo que está pasando aquí. Si todo ha ido bien debería pasar. También debe ver FireFox abierta y pasar por el proceso que se indica en el ensayo con el tearDown funciones setup () () y. La prueba en sí es simplemente probando si el “admin /” (self.browser.get (self.live_server_url + ‘/ admin /’) página se puede encontrar y que las palabras “administración Django” están presentes en el cuerpo de la etiqueta.

Confirmémoslo

ejecutar el servidor:.

$ python manage.py runserver

a continuación, vaya a http: // localhost: 8000 / admin / en su navegador y debería ver:

Nos puede confirmar que la prueba está funcionando correctamente con sólo las pruebas de lo que no debía actualizar la última línea de la prueba a:..

self.assertIn('administration Django', body.text)

ejecutarlo de nuevo debería ver el siguiente error (que se espera, por supuesto):..

AssertionError: 'administration Django' not found in u'Django administration\nUsername:\nPassword:\n '

corregir el test de nuevo Confirmar la código.

por último, ¿se fijaron que empezamos el nombre de la función para la prueba real se inició con test_. Esto es para que el corredor de prueba Django puede encontrar la prueba. En otras palabras, cualquier función que comienza con test_ serán tratados como una prueba por el corredor de prueba.

administración Iniciar sesión

A continuación, la prueba nos dejó para asegurarse de que el usuario puede iniciar sesión en el sitio de administración.

actualización test_admin_site la función en “tests.py”:

def test_admin_site(self):
# user opens web browser, navigates to admin page
self.browser.get(self.live_server_url + '/admin/')
body = self.browser.find_element_by_tag_name('body')
self.assertIn('Django administration', body.text)
# users types in username and passwords and presses enter
username_field = self.browser.find_element_by_name('username')
username_field.send_keys('admin')
password_field = self.browser.find_element_by_name('password')
password_field.send_keys('admin')
password_field.send_keys(Keys.RETURN)
# login credentials are correct, and the user is redirected to the main admin page
body = self.browser.find_element_by_tag_name('body')
self.assertIn('Site administration', body.text)

Por lo tanto –

  • find_element_by_name – se utiliza para la localización de los campos de entrada send_keys
  • – envía las pulsaciones de teclado

Ejecutar la prueba. Usted debe ver este error:

AssertionError: 'Site administration' not found in u'Django administration\nPlease enter the correct username and password for a staff account. Note that both fields may be case-sensitive.\nUsername:\nPassword:\n '

Esta fracasado porque no tenemos una configuración de usuario admin. Este es un fallo esperado, lo cual es bueno. En otras palabras, sabíamos que iba a fallar – que hace que sea mucho más fácil de solucionar.

sincronización de la base de datos: Configuración

$ python manage.py syncdb

un usuario administrador.

prueba

nuevo. Debe volver a fallar. ¿Por qué? Django crea una copia de la base de datos cuando las pruebas son corrió así que de esa manera las pruebas no afectan a la base de datos de producción.

Hay que configurar un accesorio, que es un archivo que contiene los datos que quiera cargar en la base de datos de prueba: las credenciales de inicio de sesión. Para ello, ejecute estos comandos para volcar la información de usuario administrador de la base de datos para el accesorio:

$ mkdir ft/fixtures
$ python manage.py dumpdata auth.User --indent=2 > ft/fixtures/admin.json

Ahora actualizar la clase AdminTest:

class AdminTest(LiveServerTestCase):

# load fixtures
fixtures = ['admin.json']

def setUp(self):
self.browser = webdriver.Firefox()

def tearDown(self):
self.browser.quit()

def test_admin_site(self):
# user opens web browser, navigates to admin page
self.browser.get(self.live_server_url + '/admin/')
body = self.browser.find_element_by_tag_name('body')
self.assertIn('Django administration', body.text)
# users types in username and passwords and presses enter
username_field = self.browser.find_element_by_name('username')
username_field.send_keys('admin')
password_field = self.browser.find_element_by_name('password')
password_field.send_keys('admin')
password_field.send_keys(Keys.RETURN)
# login credentials are correct, and the user is redirected to the main admin page
body = self.browser.find_element_by_tag_name('body')
self.assertIn('Site administration', body.text)

Ejecutar la prueba. Debe pasar.

Cada vez que una prueba es RAN, Django vuelca la base de datos de prueba. Entonces todos los accesorios especificados en el archivo “test.py” se cargan en la base de datos. Agregar un afirman más de

Let. Actualización de la prueba de nuevo:

def test_admin_site(self):
# user opens web browser, navigates to admin page
self.browser.get(self.live_server_url + '/admin/')
body = self.browser.find_element_by_tag_name('body')
self.assertIn('Django administration', body.text)
# users types in username and passwords and presses enter
username_field = self.browser.find_element_by_name('username')
username_field.send_keys('admin')
password_field = self.browser.find_element_by_name('password')
password_field.send_keys('admin')
password_field.send_keys(Keys.RETURN)
# login credentials are correct, and the user is redirected to the main admin page
body = self.browser.find_element_by_tag_name('body')
self.assertIn('Site administration', body.text)
# user clicks on the Users link
user_link = self.browser.find_elements_by_link_text('Users')
user_link[0].click()
# user verifies that user live@forever.com is present
body = self.browser.find_element_by_tag_name('body')
self.assertIn('live@forever.com', body.text)

ejecutarlo. Se debe fallar, porque necesitamos y otro usuario con el archivo de datos:

[
{
"pk": 1,
"model": "auth.user",
"fields": {
"username": "admin",
"first_name": "",
"last_name": "",
"is_active": true,
"is_superuser": true,
"is_staff": true,
"last_login": "2013-12-29T03:49:13.545Z",
"groups": [],
"user_permissions": [],
"password": "pbkdf2_sha256$12000$VtsgwjQ1BZ6u$zwnG+5E5cl8zOnghahArLHiMC6wGk06HXrlAijFFpSA=",
"email": "ad@min.com",
"date_joined": "2013-12-29T03:49:13.545Z"
}
},
{
"pk": 2,
"model": "auth.user",
"fields": {
"username": "live",
"first_name": "",
"last_name": "",
"is_active": true,
"is_superuser": false,
"is_staff": false,
"last_login": "2013-12-29T03:49:13.545Z",
"groups": [],
"user_permissions": [],
"password": "pbkdf2_sha256$12000$VtsgwjQ1BZ6u$zwnG+5E5cl8zOnghahArLHiMC6wGk06HXrlAijFFpSA=",
"email": "live@forever.com",
"date_joined": "2013-12-29T03:49:13.545Z"
}
}
]

Ejecutar de nuevo. Asegúrese de que su paso. Refactor la prueba si es necesario. Ahora pensar en lo demás que usted podría probar. Tal vez usted podría poner a prueba para asegurarse de que el usuario administrador puede agregar un usuario en el panel de administración. O tal vez una prueba para asegurarse de que alguien sin acceso de administrador no puede acceder al panel de administración. Escribir algunas pruebas más. Actualizar el código. Prueba de nuevo. Refactor si es necesario.

A continuación, vamos a añadir la aplicación para añadir contactos. No se olvide de cometer! Configuración

la aplicación Contactos

empezar con una prueba. Agregue la siguiente función:

def test_create_contact_admin(self):
self.browser.get(self.live_server_url + '/admin/')
username_field = self.browser.find_element_by_name('username')
username_field.send_keys('admin')
password_field = self.browser.find_element_by_name('password')
password_field.send_keys('admin')
password_field.send_keys(Keys.RETURN)
# user verifies that user_contacts is present
body = self.browser.find_element_by_tag_name('body')
self.assertIn('User_Contacts', body.text)

ejecutar el banco de prueba de nuevo. Debería ver la siguiente de errores

AssertionError: 'User_Contacts' not found in u'Django administration\nWelcome, admin. Change password / Log out\nSite administration\nAuth\nGroups\nAdd\nChange\nUsers\nAdd\nChange\nRecent Actions\nMy Actions\nNone available'

  • que se espera.

Ahora, escribir código sólo lo suficiente para que esto pase.

Crear la App:

$ python manage.py startapp user_contacts

Añadir al archivo “settings.py”:

INSTALLED_APPS = (
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'ft',
'user_contacts',
)

Dentro del archivo “admin.py” en el directorio user_contacts añadir el siguiente código:

from user_contacts.models import Person, Phone
from django.contrib import admin

admin.site.register(Person)
admin.site.register(Phone)

Su estructura del proyecto debe parecerse esto:

├── user_contacts
│   ├── __init__.py
│   ├── admin.py
│   ├── models.py
│   ├── tests.py
│   └── views.py
├── contacts
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── ft
│   ├── __init__.py
│   ├── fixtures
│   │   └── admin.json
│   └── tests.py
└── manage.py

Update “models.py”:

from django.db import models

class Person(models.Model):
first_name = models.CharField(max_length = 30)
last_name = models.CharField(max_length = 30)
email = models.EmailField(null = True, blank = True)
address = models.TextField(null = True, blank = True)
city = models.CharField(max_length = 15, null = True,blank = True)
state = models.CharField(max_length = 15, null = True, blank = True)
country = models.CharField(max_length = 15, null = True, blank = True)

def __unicode__(self):
return self.last_name +", "+ self.first_name

class Phone(models.Model):
person = models.ForeignKey('Person')
number = models.CharField(max_length=10)

def __unicode__(self):
return self.number

Ejecutar la prueba de nuevo ahora. Ahora debería ver: Ir de

Ran 2 tests in 11.730s

OK

Let adelante y añadir a la prueba para asegurarse de que el administrador puede agregar datos:

# user clicks on the Persons link
persons_links = self.browser.find_elements_by_link_text('Persons')
persons_links[0].click()
# user clicks on the Add person link
add_person_link = self.browser.find_element_by_link_text('Add person')
add_person_link.click()
# user fills out the form
self.browser.find_element_by_name('first_name').send_keys("Michael")
self.browser.find_element_by_name('last_name').send_keys("Herman")
self.browser.find_element_by_name('email').send_keys("michael@realpython.com")
self.browser.find_element_by_name('address').send_keys("2227 Lexington Ave")
self.browser.find_element_by_name('city').send_keys("San Francisco")
self.browser.find_element_by_name('state').send_keys("CA")
self.browser.find_element_by_name('country').send_keys("United States")
# user clicks the save button
self.browser.find_element_by_css_selector("input[value='Save']").click()
# the Person has been added
body = self.browser.find_element_by_tag_name('body')
self.assertIn('Herman, Michael', body.text)
# user returns to the main admin screen
home_link = self.browser.find_element_by_link_text('Home')
home_link.click()
# user clicks on the Phones link
persons_links = self.browser.find_elements_by_link_text('Phones')
persons_links[0].click()
# user clicks on the Add phone link
add_person_link = self.browser.find_element_by_link_text('Add phone')
add_person_link.click()
# user finds the person in the dropdown
el = self.browser.find_element_by_name("person")
for option in el.find_elements_by_tag_name('option'):
if option.text == 'Herman, Michael':
option.click()
# user adds the phone numbers
self.browser.find_element_by_name('number').send_keys("4158888888")
# user clicks the save button
self.browser.find_element_by_css_selector("input[value='Save']").click()
# the Phone has been added
body = self.browser.find_element_by_tag_name('body')
self.assertIn('4158888888', body.text)
# user logs out
self.browser.find_element_by_link_text('Log out').click()
body = self.browser.find_element_by_tag_name('body')
self.assertIn('Thanks for spending some quality time with the Web site today.', body.text)

que es para la funcionalidad de administración. Deje de mecanismos de conmutación y el enfoque de la aplicación, user_contacts, sí. ¿Ha olvidado para comprometerse? Si es así, hacerlo ahora.

Unidad

Pruebas de

Piense acerca de las características que hemos escrito hasta ahora. Sólo hemos definido nuestro modelo y los administradores podrán alterar el modelo. Basado en esto, y el objetivo general de nuestro proyecto, se centran en las funcionalidades de usuario restantes. Los usuarios deben

poder-

intentar formular la prueba de funcionamiento restantes (s) sobre la base de esos requisitos. Antes de escribir las pruebas funcionales, sin embargo, debemos definir el comportamiento del código a través de pruebas de unidad – lo que le ayudará a escribir bien, un código limpio, por lo que es más fácil escribir las pruebas funcionales.

Recuerde : Las pruebas funcionales son el último indicador de si el proyecto funciona o no, mientras que las pruebas de unidad son los medios para ayudar a alcanzar ese fin. Todo esto tendrá sentido pronto. pausa de

Let por un minuto y hablar de algunas convenciones.

Aunque los fundamentos de TDD (o extremos) – Test, código, Refactor – son universales, muchos desarrolladores de acercarse a los medios de manera diferente. Por ejemplo, me gusta escribir mis pruebas de unidad en primer lugar, para asegurar que mi código funciona a un nivel granular, a continuación, escribir las pruebas funcionales. Otros escriben pruebas funcionales en primer lugar, verlas fallan, entonces las pruebas de unidad de escritura, de ver fallan, a continuación, escribir el código para satisfacer primero las pruebas unitarias, que en última instancia deben satisfacer las pruebas de funcionamiento. No hay respuesta correcta o incorrecta aquí. Hacer lo que se siente más cómodo – pero siguen poniendo a prueba en primer lugar, a continuación, escribir código, y finalmente refactor.

Vistas

En primer lugar, comprobar para asegurarse de que todos los puntos de vista están configurados correctamente.

Vista principal

Como siempre, se inicia con una prueba:

from django.template.loader import render_to_string
from django.test import TestCase, Client
from user_contacts.models import Person, Phone
from user_contacts.views import *

class ViewTest(TestCase):

def setUp(self):
self.client_stub = Client()

def test_view_home_route(self):
response = self.client_stub.get('/')
self.assertEquals(response.status_code, 200)

Nombre test_views.py esta prueba y guardarlo en la user_contacts / Pruebas de directorio. También añadir un archivo __init__.py al directorio y borrar el archivo “tests.py” en el directorio principal user_contacts.

Run que:

$ python manage.py test user_contacts

Que falle – AssertionError: 404 = 200 – porque la URL, Vista, y la plantilla no existen. Si no está familiarizado con la forma en Django se encarga de la arquitectura MVC, por favor leer el breve artículo aquí.

La prueba es simple. En primer lugar, obtener la URL “/” con el cliente, que es parte de TestCase de Django. La respuesta se almacena, después comprobamos para asegurarse de que el código de estado devuelto es igual a 200.

Añadir la siguiente ruta de “contactos / urls.py”:

url(r'^', include('user_contacts.urls')),

actualización “user_contacts / urls.py”:

from django.conf.urls import patterns, url

from user_contacts.views import *

urlpatterns = patterns('',
url(r'^$', home),
)

Actualizar “views.py”:

from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import render_to_response, render
from django.template import RequestContext
from user_contacts.models import Phone, Person
# from user_contacts.new_contact_form import ContactForm

def home(request):
return render_to_response('index.html')

Añadir una plantilla de “index.html” en el directorio de plantillas:



Welcome.<itle><br /> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link href="http:/<br />etdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css" rel="stylesheet" media="screen"></p> <style> .container { padding: 50px; } </style> <p> </head><br /> <body></p> <div class="container"> <h1>What would you like to do?</h1> <ul> <li><a href="/all">View Contacts</a></li> <li><a href="/add">Add Contact</a></li> </ul> <div> <script src="http://code.jquery.com/jquery-1.10.2.min.js"></script><br /> <script src="http:/<br />etdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script><br /> </body><br /> </html><br /> </code> </p> <p> Ejecutar la prueba de nuevo. Se debe pasar muy bien. </p> <h3> Todos los contactos Ver </h3> <p> La prueba de este punto de vista es casi idéntica a la última prueba. Intentarlo por su cuenta antes de mirar a mi respuesta. </p> <p> Escribir la prueba por primera vez por la adición de la siguiente función a la clase ViewTest: </p> <p> <code>def test_view_contacts_route(self):<br /> response = self.client_stub.get('/all/')<br /> self.assertEquals(response.status_code, 200)<br /> </code> </p> <p> Cuando RAN, debería ver el mismo error: AssertionError: 404 = 200. </p> <p> Actualizar “user_contacts / urls.py” con la siguiente ruta : </p> <p> <code>url(r'^all/$', all_contacts),<br /> </code> </p> <p> Update “views.py”: </p> <p> <code>def all_contacts(request):<br /> contacts = Phone.objects.all()<br /> return render_to_response('all.html', {'contacts':contacts})<br /> </code> </p> <p> Añadir un “-todos.html Anuncios” plantilla para el directorio de plantillas: </p> <p> <code><!DOCTYPE html><br /> <html><br /> <head><br /> <title>All Contacts.<itle><br /> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link href="http:/<br />etdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css" rel="stylesheet" media="screen"></p> <style> .container { padding: 50px; } </style> <p></head><br /> <body></p> <div class="container"> <h1>All Contacts</h1> <table border="1" cellpadding="5"> <tr> <th>First Name<h></p> <th>Last Name<h></p> <th>Address<h></p> <th>City<h></p> <th>State<h></p> <th>Country<h></p> <th>Phone Number<h></p> <th>Email<h><br /> <r><br /> {% for contact in contacts %}</p> <tr> <td>{{contact.person.first\_name}}<d></p> <td>{{contact.person.last\_name}}<d></p> <td>{{contact.person.address}}<d></p> <td>{{contact.person.city}}<d></p> <td>{{contact.person.state}}<d></p> <td>{{contact.person.country}}<d></p> <td>{{contact.number}}<d></p> <td>{{contact.person.email}}<d><br /> <r><br /> {% endfor %}<br /> <able><br /> <br /> <a href="/">Return Home</a> </div> <p> <script src="http://code.jquery.com/jquery-1.10.2.min.js"></script><br /> <script src="http:/<br />etdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script><br /> </body><br /> </html><br /> </code> </p> <p> Esto debe pasar también. </p> <h3> Añadir Contacto Ver </h3> <p> Esta prueba es ligeramente diferente de los dos anteriores, así que por favor seguir a lo largo de cerca. </p> <p> Añadir la prueba para el conjunto de pruebas: </p> <p> <code>def test_add_contact_route(self):<br /> response = self.client_stub.get('/add/')<br /> self.assertEqual(response.status_code, 200)<br /> </code> </p> <p> Debe ver este error cuando RAN: AssertionError: 404 = 200 </p> <p> Update “urls.py”: </p> <p> <code>url(r'^add/$', add),<br /> </code> </p> <p> Update “views.py”: </p> <p> <code>def add(request):<br /> person_form = ContactForm()<br /> return render(request, 'add.html', {'person_form' : person_form}, context_instance = RequestContext(request))<br /> </code> </p> <p> Hacer Asegúrese de añadir la siguiente importación: </p> <p> <code>from user_contacts.new_contact_form import ContactForm<br /> </code> </p> <p> Crear un nuevo archivo llamado new_contact_form.py y agregue el siguiente código: </p> <p> <code>import re<br /> from django import forms<br /> from django.core.exceptions import ValidationError<br /> from user_contacts.models import Person, Phone</p> <p>class ContactForm(forms.Form):<br /> first_name = forms.CharField(max_length=30)<br /> last_name = forms.CharField(max_length=30)<br /> email = forms.EmailField(required=False)<br /> address = forms.CharField(widget=forms.Textarea, required=False)<br /> city = forms.CharField(required=False)<br /> state = forms.CharField(required=False)<br /> country = forms.CharField(required=False)<br /> number = forms.CharField(max_length=10)</p> <p> def save(self):<br /> if self.is_valid():<br /> data = self.cleaned_data<br /> person = Person.objects.create(first_name=data.get('first_name'), last_name=data.get('last_name'),<br /> email=data.get('email'), address=data.get('address'), city=data.get('city'), state=data.get('state'),<br /> country=data.get('country'))<br /> phone = Phone.objects.create(person=person, number=data.get('number'))<br /> return phone<br /> </code> </p> <p> Añadir “add.html” en el directorio de plantillas: </p> <p> <code><!DOCTYPE html><br /> <html><br /> <head><br /> <title>Welcome.<itle><br /> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link href="http:/<br />etdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css" rel="stylesheet" media="screen"></p> <style> .container { padding: 50px; } </style> <p></head><br /> <body></p> <div class="container"> <h1>Add Contact</h1> <p></p> <form action="/create" method ="POST" role="form"> {% csrf\_token %}<br /> {{ person_\_form.as\_p }}<br /> {{ phone\_form.as\_p }}<br /> <input type ="submit" name ="Submit" class="btn btn-default" value ="Add"><br /> </form> <p> <a href="/">Return Home</a> </div> <p> <script src="http://code.jquery.com/jquery-1.10.2.min.js"></script><br /> <script src="http:/<br />etdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script><br /> </body><br /> </html><br /> </code> </p> <p> ¿pasa? Debería. Si no es así, refactor. </p> <h3> Validación </h3> <p> Ahora que hemos terminado de probar las Vistas, vamos a añadir la validación de la forma. Pero primero tenemos que escribir una prueba. ¡Sorpresa! </p> <p> Crear un nuevo archivo llamado “test_validator.py” dentro del directorio “pruebas” y añadir el siguiente código: </p> <p> <code>from django.core.exceptions import ValidationError<br /> from django.test import TestCase<br /> from user_contacts.validators import validate_number, validate_string</p> <p> class ValidatorTest(TestCase):<br /> def test_string_is_invalid_if_contains_numbers_or_special_characters(self):<br /> with self.assertRaises(ValidationError):<br /> validate_string('@test')<br /> validate_string('tester#')<br /> def test_number_is_invalid_if_contains_any_character_except_digits(self):<br /> with self.assertRaises(ValidationError):<br /> validate_number('123ABC')<br /> validate_number('75431#')<br /> </code> </p> <p> Antes de ejecutar el conjunto de pruebas, se puede adivinar lo que podría suceder? <em> Consejo: Ponga mucha atención a las importaciones en el código de seguridad. </em> Usted debe obtener el siguiente error porque no tenemos un archivo “validators.py”: </p> <p> <code>ImportError: cannot import name validate_string<br /> </code> </p> <p> En otras palabras, estamos probando la lógica en un archivo de validación que no existe todavía. </p> <p> Añadir un nuevo archivo llamado “validators.py” al directorio user_contacts: </p> <p> <code>import re<br /> from django.core.exceptions import ValidationError</p> <p>def validate_string(string):<br /> if re.search('^[A-Za-z]+$', string) is None:<br /> raise ValidationError('Invalid')</p> <p>def validate_number(value):<br /> if re.search('^[0-9]+$', value) is None:<br /> raise ValidationError('Invalid')<br /> </code> </p> <p> ejecutar el banco de prueba de nuevo. Cinco ahora debe pasar: </p> <p> <code>Ran 5 tests in 0.019s</p> <p>OK<br /> </code> </p> <h3> Crear contacto </h3> <p> Dado que hemos añadido la validación, que queremos poner a prueba para asegurarse de que los validadores trabajan en el área de administración, por lo que la actualización “test_views.py”: pruebas </p> <p> <code>from django.template.loader import render_to_string<br /> from django.test import TestCase, Client<br /> from user_contacts.models import Person, Phone<br /> from user_contacts.views import *</p> <p>class ViewTest(TestCase):</p> <p> def setUp(self):<br /> self.client_stub = Client()<br /> self.person = Person(first_name = 'TestFirst',last_name = 'TestLast')<br /> self.person.save()<br /> self.phone = Phone(person = self.person,number = '7778889999')<br /> self.phone.save()</p> <p> def test_view_home_route(self):<br /> response = self.client_stub.get('/')<br /> self.assertEquals(response.status_code, 200)</p> <p> def test_view_contacts_route(self):<br /> response = self.client_stub.get('/all/')<br /> self.assertEquals(response.status_code, 200)</p> <p> def test_add_contact_route(self):<br /> response = self.client_stub.get('/add/')<br /> self.assertEqual(response.status_code, 200)</p> <p> def test_create_contact_successful_route(self):<br /> response = self.client_stub.post('/create',data = {'first_name' : 'testFirst', 'last_name':'tester', 'email':'test@tester.com', 'address':'1234 nowhere', 'city':'far away', 'state':'CO', 'country':'USA', 'number':'987654321'})<br /> self.assertEqual(response.status_code, 302)</p> <p> def test_create_contact_unsuccessful_route(self):<br /> response = self.client_stub.post('/create',data = {'first_name' : 'tester_first_n@me', 'last_name':'test', 'email':'tester@test.com', 'address':'5678 everywhere', 'city':'far from here', 'state':'CA', 'country':'USA', 'number':'987654321'})<br /> self.assertEqual(response.status_code, 200)</p> <p> def tearDown(self):<br /> self.phone.delete()<br /> self.person.delete()<br /> </code> </p> <p> Dos fallara. </p> <p> Lo que hay que hacer con el fin de obtener esta prueba a pasar? Bueno, en primer lugar hay que agregar una función a los puntos de vista que permiten agregar información a la base de datos. </p> <p> Agregar ruta: </p> <p> <code>url(r'^create$', create),<br /> </code> </p> <p> Update “views.py”: </p> <p> <code>def create(request):<br /> form = ContactForm(request.POST)<br /> if form.is_valid():<br /> form.save()<br /> return HttpResponseRedirect('all/')<br /> return render(<br /> request, 'add.html', {'person_form' : form},<br /> context_instance=RequestContext(request))<br /> </code> </p> <p> prueba de nuevo: </p> <p> <code>$ python manage.py test user_contacts<br /> </code> </p> <p> Esta vez sólo una prueba debe fallar – AssertionError: 302 = 200 – porque tratamos de agregar datos que no deberían haber pasado los validadores, pero lo hicieron. En otras palabras, tenemos que actualizar el archivo “models.py” así como la forma para tomar esas validadores en cuenta. </p> <p> Update “models.py”: </p> <p> <code>from django.db import models<br /> from user_contacts.validators import validate_string, validate_number</p> <p>class Person(models.Model):<br /> first_name = models.CharField(max_length = 30, validators = [validate_string])<br /> last_name = models.CharField(max_length = 30, validators = [validate_string])<br /> email = models.EmailField(null = True, blank = True)<br /> address = models.TextField(null = True, blank = True)<br /> city = models.CharField(max_length = 15, null = True,blank = True)<br /> state = models.CharField(max_length = 15, null = True, blank = True, validators = [validate_string])<br /> country = models.CharField(max_length = 15, null = True, blank = True)</p> <p> def __unicode__(self):<br /> return self.last_name +", "+ self.first_name</p> <p>class Phone(models.Model):<br /> person = models.ForeignKey('Person')<br /> number = models.CharField(max_length=10, validators = [validate_number])</p> <p> def __unicode__(self):<br /> return self.number<br /> </code> </p> <p> Eliminar la base de datos actual, “db.sqlite3”, y volver a sincronizar la base de datos: Configuración </p> <p> <code>$ python manage.py syncdb<br /> </code> </p> <p> un usuario administrador de nuevo. </p> <p> actualización new_contact_form.py mediante la adición de validación: </p> <p> <code>import re<br /> from django import forms<br /> from django.core.exceptions import ValidationError<br /> from user_contacts.models import Person, Phone<br /> from user_contacts.validators import validate_string, validate_number</p> <p>class ContactForm(forms.Form):<br /> first_name = forms.CharField(max_length=30, validators = [validate_string])<br /> last_name = forms.CharField(max_length=30, validators = [validate_string])<br /> email = forms.EmailField(required=False)<br /> address = forms.CharField(widget=forms.Textarea, required=False)<br /> city = forms.CharField(required=False)<br /> state = forms.CharField(required=False, validators = [validate_string])<br /> country = forms.CharField(required=False)<br /> number = forms.CharField(max_length=10, validators = [validate_number])</p> <p> def save(self):<br /> if self.is_valid():<br /> data = self.cleaned_data<br /> person = Person.objects.create(first_name=data.get('first_name'), last_name=data.get('last_name'),<br /> email=data.get('email'), address=data.get('address'), city=data.get('city'), state=data.get('state'),<br /> country=data.get('country'))<br /> phone = Phone.objects.create(person=person, number=data.get('number'))<br /> return phone<br /> </code> </p> <p> Ejecutar las pruebas de nuevo. 7 debe pasar. </p> <p> Ahora, desviándose de TDD por un minuto, quiero añadir una prueba adicional para la validación de prueba en el lado del cliente. Así que añadir test_contact_form.py: </p> <p> <code>from django.test import TestCase<br /> from user_contacts.models import Person<br /> from user_contacts.new_contact_form import ContactForm</p> <p>class TestContactForm(TestCase):<br /> def test_if_valid_contact_is_saved(self):<br /> form = ContactForm({'first_name':'test', 'last_name':'test','number':'9999900000'})<br /> contact = form.save()<br /> self.assertEqual(contact.person.first_name, 'test')<br /> def test_if_invalid_contact_is_not_saved(self):<br /> form = ContactForm({'first_name':'tes&t', 'last_name':'test','number':'9999900000'})<br /> contact = form.save()<br /> self.assertEqual(contact, None)<br /> </code> </p> <p> Ejecutar el paquete de pruebas. Todas las 9 pruebas deben pasar ahora. ¡Hurra! Ahora comprometerse. Pruebas funcionales </p> <h2> Redux </h2> <p> con la unidad de pruebas realizadas, que ahora puede añadir una prueba funcional para asegurar que la aplicación se ejecuta correctamente. Con suerte, con las unidades de prueba que pasa, deberíamos tener ningún problema con la prueba funcional. </p> <p> Añadir una nueva clase en el fichero “tests.py”: </p> <p> <code>class UserContactTest(LiveServerTestCase):</p> <p> def setUp(self):<br /> self.browser = webdriver.Firefox()<br /> self.browser.implicitly_wait(3)</p> <p> def tearDown(self):<br /> self.browser.quit()</p> <p> def test_create_contact(self):<br /> # user opens web browser, navigates to home page<br /> self.browser.get(self.live_server_url + '/')<br /> # user clicks on the Persons link<br /> add_link = self.browser.find_elements_by_link_text('Add Contact')<br /> add_link[0].click()<br /> # user fills out the form<br /> self.browser.find_element_by_name('first_name').send_keys("Michael")<br /> self.browser.find_element_by_name('last_name').send_keys("Herman")<br /> self.browser.find_element_by_name('email').send_keys("michael@realpython.com")<br /> self.browser.find_element_by_name('address').send_keys("2227 Lexington Ave")<br /> self.browser.find_element_by_name('city').send_keys("San Francisco")<br /> self.browser.find_element_by_name('state').send_keys("CA")<br /> self.browser.find_element_by_name('country').send_keys("United States")<br /> self.browser.find_element_by_name('number').send_keys("4158888888")<br /> # user clicks the save button<br /> self.browser.find_element_by_css_selector("input[value='Add']").click()<br /> # the Person has been added<br /> body = self.browser.find_element_by_tag_name('body')<br /> self.assertIn('michael@realpython.com', body.text)</p> <p> def test_create_contact_error(self):<br /> # user opens web browser, navigates to home page<br /> self.browser.get(self.live_server_url + '/')<br /> # user clicks on the Persons link<br /> add_link = self.browser.find_elements_by_link_text('Add Contact')<br /> add_link[0].click()<br /> # user fills out the form<br /> self.browser.find_element_by_name('first_name').send_keys("test@")<br /> self.browser.find_element_by_name('last_name').send_keys("tester")<br /> self.browser.find_element_by_name('email').send_keys("test@tester.com")<br /> self.browser.find_element_by_name('address').send_keys("2227 Tester Ave")<br /> self.browser.find_element_by_name('city').send_keys("Tester City")<br /> self.browser.find_element_by_name('state').send_keys("TC")<br /> self.browser.find_element_by_name('country').send_keys("TCA")<br /> self.browser.find_element_by_name('number').send_keys("4158888888")<br /> # user clicks the save button<br /> self.browser.find_element_by_css_selector("input[value='Add']").click()<br /> body = self.browser.find_element_by_tag_name('body')<br /> self.assertIn('Invalid', body.text)<br /> </code> </p> <p> Ejecutar las pruebas funcionales: </p> <p> <code>$ python manage.py test ft<br /> </code> </p> <p> Aquí sólo estamos probando el código que escribimos y ya probado con pruebas unitarias desde la perspectiva del usuario final. Las cuatro pruebas deben pasar. </p> <p> Por último, vamos a asegurar que la validación ponemos en su lugar se aplica al panel de administración mediante la adición de la siguiente función a la clase AdminTest: </p> <p> <code>def test_create_contact_admin_raise_error(self):<br /> # # user opens web browser, navigates to admin page, and logs in<br /> self.browser.get(self.live_server_url + '/admin/')<br /> username_field = self.browser.find_element_by_name('username')<br /> username_field.send_keys('admin')<br /> password_field = self.browser.find_element_by_name('password')<br /> password_field.send_keys('admin')<br /> password_field.send_keys(Keys.RETURN)<br /> # user clicks on the Persons link<br /> persons_links = self.browser.find_elements_by_link_text('Persons')<br /> persons_links[0].click()<br /> # user clicks on the Add person link<br /> add_person_link = self.browser.find_element_by_link_text('Add person')<br /> add_person_link.click()<br /> # user fills out the form<br /> self.browser.find_element_by_name('first_name').send_keys("test@")<br /> self.browser.find_element_by_name('last_name').send_keys("tester")<br /> self.browser.find_element_by_name('email').send_keys("test@tester.com")<br /> self.browser.find_element_by_name('address').send_keys("2227 Tester Ave")<br /> self.browser.find_element_by_name('city').send_keys("Tester City")<br /> self.browser.find_element_by_name('state').send_keys("TC")<br /> self.browser.find_element_by_name('country').send_keys("TCA")<br /> # user clicks the save button<br /> self.browser.find_element_by_css_selector("input[value='Save']").click()<br /> body = self.browser.find_element_by_tag_name('body')<br /> self.assertIn('Invalid', body.text)<br /> </code> </p> <p> ejecutarlo. Cinco pruebas deben pasar. Y se comprometen Vamos a llamar a un día. Estructura </p> <p> prueba </p> <h2> </h2> <p> TDD es una poderosa herramienta y una parte integral del ciclo de desarrollo, ayudando a los desarrolladores de programas rompen en pequeñas porciones, legibles. Dichas porciones son mucho más fáciles de escribir ahora y cambiar más tarde. Además, tener una suite completa de los ensayos, que cubre todas las características de su base de código, ayuda a asegurar que las nuevas implementaciones de funciones no rompa el código existente. </p> <p> Dentro del proceso, las pruebas funcionales <strong> </strong> son pruebas de alto nivel, se centró en la <em> características </em> que los usuarios finales interactuar con él. </p> <p> Mientras tanto, Unidad <strong> prueba Las pruebas funcionales de apoyo </strong> en que ponen a prueba cada característica del código. Tenga en cuenta que la unidad <strong> prueba </strong> son mucho más fáciles de escribir, en general, proporcionar una mejor cobertura, y son más fáciles de depurar, ya que ponen a prueba sólo una característica a la vez. También se desarrollan mucho más rápido, así que asegúrese de probar sus pruebas de unidad más a menudo que sus pruebas funcionales. La toma del </p> <p> Let un vistazo a nuestra estructura de prueba para ver cómo nuestras pruebas unitarias apoyan las pruebas funcionales: </p> </p> <p> <img src="https://eltecnofilo.es/wp-content/uploads/2020/03/django-tdd-test-structure.6d04849a84ea.png"> </p> <h2> Conclusión </h2> <p> Felicidades. Lo haces a través. ¿Que sigue? </p> <p> primer lugar, usted puede haber notado que yo no 100% de seguimiento del proceso de TDD. Esta bien. La mayoría de los desarrolladores involucrados en TDD no siempre se adhieren a ella en cada situación individual. Hay momentos en los que debe desvía de ella con el fin de simplemente hacer las cosas – que es perfectamente bien. Si desea refactorizar una parte del código / proceso para adherirse plenamente al proceso de TDD, se puede. De hecho, puede ser una buena práctica. </p> <p> En segundo lugar, pensar en las pruebas que echaba de menos. La determinación de qué y cuándo prueba es difícil. Se necesita tiempo y mucha práctica para ser bueno en las pruebas en general. He dejado muchos espacios en blanco que tengo la intención de Revel en mi próximo post. Vea si usted puede encontrar estos y añadir pruebas. </p> <p> Por último, recuerda el último paso en el proceso de TDD? Refactorización. Este paso es vital, ya que ayuda a crear legible, código mantenible que no sólo se entiende ahora – pero en el futuro también. Cuando uno mira hacia atrás en su código, piense en las pruebas se pueden combinar. También, qué pruebas debe añadir para asegurar que todo el código escrito se prueba? Se podía comprobar valores nulos y / o autenticación del lado del servidor, por ejemplo. Usted debe refactorizar el código antes de pasar a escribir ningún código nuevo – lo que no lo hice por el bien de tiempo. Tal vez en otra entrada del blog? Pensar en lo mal código puede contaminar todo el proceso? </p> <p> Gracias por leer. Coge el código final en el repositorio aquí. Por favor, comentario a continuación con cualquier pregunta. </p> </div><!-- .entry-content --> </div><!-- .post-inner --> <div class="section-inner"> </div><!-- .section-inner --> <nav class="pagination-single section-inner" aria-label="Entrada" role="navigation"> <hr class="styled-separator is-style-wide" aria-hidden="true" /> <div class="pagination-single-inner"> <a class="previous-post" href="https://eltecnofilo.es/hello-world/"> <span class="arrow" aria-hidden="true">←</span> <span class="title"><span class="title-inner">Hello world!</span></span> </a> <a class="next-post" href="https://eltecnofilo.es/basico-de-entrada-de-salida-y-el-formato-de-cadenas-en-python/"> <span class="arrow" aria-hidden="true">→</span> <span class="title"><span class="title-inner">Básico de entrada, de salida, y el formato de cadenas en Python</span></span> </a> </div><!-- .pagination-single-inner --> <hr class="styled-separator is-style-wide" aria-hidden="true" /> </nav><!-- .pagination-single --> <div class="comments-wrapper section-inner"> <div id="respond" class="comment-respond"> <h2 id="reply-title" class="comment-reply-title">Deja un comentario <small><a rel="nofollow" id="cancel-comment-reply-link" href="/introduccion-a-los-canales-de-django/#respond" style="display:none;">Cancelar la respuesta</a></small></h2><form action="https://eltecnofilo.es/wp-comments-post.php" method="post" id="commentform" class="section-inner thin max-percentage" novalidate><p class="comment-notes"><span id="email-notes">Tu dirección de correo electrónico no será publicada.</span> Los campos obligatorios están marcados con <span class="required">*</span></p><p class="comment-form-comment"><label for="comment">Comentario</label> <textarea id="comment" name="comment" cols="45" rows="8" maxlength="65525" required="required"></textarea></p><p class="comment-form-author"><label for="author">Nombre <span class="required">*</span></label> <input id="author" name="author" type="text" value="" size="30" maxlength="245" required='required' /></p> <p class="comment-form-email"><label for="email">Correo electrónico <span class="required">*</span></label> <input id="email" name="email" type="email" value="" size="30" maxlength="100" aria-describedby="email-notes" required='required' /></p> <p class="comment-form-url"><label for="url">Web</label> <input id="url" name="url" type="url" value="" size="30" maxlength="200" /></p> <p class="comment-form-cookies-consent"><input id="wp-comment-cookies-consent" name="wp-comment-cookies-consent" type="checkbox" value="yes" /> <label for="wp-comment-cookies-consent">Guardar mi nombre, correo electrónico y sitio web en este navegador para la próxima vez que haga un comentario.</label></p> <p class="form-submit"><input name="submit" type="submit" id="submit" class="submit" value="Publicar el comentario" /> <input type='hidden' name='comment_post_ID' value='6495' id='comment_post_ID' /> <input type='hidden' name='comment_parent' id='comment_parent' value='0' /> </p></form> </div><!-- #respond --> </div><!-- .comments-wrapper --> </article><!-- .post --> </main><!-- #site-content --> <div class="footer-nav-widgets-wrapper header-footer-group"> <div class="footer-inner section-inner"> <aside class="footer-widgets-outer-wrapper" role="complementary"> <div class="footer-widgets-wrapper"> <div class="footer-widgets column-one grid-item"> <div class="widget widget_search"><div class="widget-content"><form role="search" method="get" class="search-form" action="https://eltecnofilo.es/"> <label for="search-form-2"> <span class="screen-reader-text">Buscar:</span> <input type="search" id="search-form-2" class="search-field" placeholder="Buscar …" value="" name="s" /> </label> <input type="submit" class="search-submit" value="Buscar" /> </form> </div></div> <div class="widget widget_recent_entries"><div class="widget-content"> <h2 class="widget-title subheading heading-size-3">Entradas recientes</h2> <ul> <li> <a href="https://eltecnofilo.es/vps-cloud-hosting/">VPS cloud hosting</a> </li> <li> <a href="https://eltecnofilo.es/versiones-de-ejecucion-de-python-en-acoplable-como-probar-la-ultima-release-python/">Versiones de ejecución de Python en acoplable: Cómo probar la última Release Python</a> </li> <li> <a href="https://eltecnofilo.es/leer-y-escribir-archivos-csv/">Leer y escribir archivos CSV</a> </li> <li> <a href="https://eltecnofilo.es/python-puro-vs-vs-numpy-tensorflow-comparacion-de-rendimiento/">Python puro vs vs NumPy TensorFlow Comparación de Rendimiento</a> </li> <li> <a href="https://eltecnofilo.es/estructura-python-programa-lexico/">Estructura Python Programa léxico</a> </li> </ul> </div></div><div class="widget widget_recent_comments"><div class="widget-content"><h2 class="widget-title subheading heading-size-3">Comentarios recientes</h2><ul id="recentcomments"><li class="recentcomments"><span class="comment-author-link"><a href="https://wordpress.org/" rel="external nofollow ugc" class="url">A WordPress Commenter</a></span> en <a href="https://eltecnofilo.es/hello-world/#comment-1">Hello world!</a></li></ul></div></div> </div> <div class="footer-widgets column-two grid-item"> <div class="widget widget_archive"><div class="widget-content"><h2 class="widget-title subheading heading-size-3">Archivos</h2> <ul> <li><a href='https://eltecnofilo.es/2020/04/'>abril 2020</a></li> <li><a href='https://eltecnofilo.es/2020/03/'>marzo 2020</a></li> <li><a href='https://eltecnofilo.es/2019/12/'>diciembre 2019</a></li> </ul> </div></div><div class="widget widget_categories"><div class="widget-content"><h2 class="widget-title subheading heading-size-3">Categorías</h2> <ul> <li class="cat-item cat-item-22"><a href="https://eltecnofilo.es/category/python/">Python</a> </li> <li class="cat-item cat-item-1"><a href="https://eltecnofilo.es/category/uncategorized/">Uncategorized</a> </li> </ul> </div></div><div class="widget widget_meta"><div class="widget-content"><h2 class="widget-title subheading heading-size-3">Meta</h2> <ul> <li><a rel="nofollow" href="https://eltecnofilo.es/wp-login.php">Acceder</a></li> <li><a href="https://eltecnofilo.es/feed/">Feed de entradas</a></li> <li><a href="https://eltecnofilo.es/comments/feed/">Feed de comentarios</a></li> <li><a href="https://es.wordpress.org/">WordPress.org</a></li> </ul> </div></div> </div> </div><!-- .footer-widgets-wrapper --> </aside><!-- .footer-widgets-outer-wrapper --> </div><!-- .footer-inner --> </div><!-- .footer-nav-widgets-wrapper --> <footer id="site-footer" role="contentinfo" class="header-footer-group"> <div class="section-inner"> <div class="footer-credits"> <p class="footer-copyright">© 2020 <a href="https://eltecnofilo.es/">My Blog</a> </p><!-- .footer-copyright --> <p class="powered-by-wordpress"> <a href="https://es.wordpress.org/"> Funciona gracias a WordPress </a> </p><!-- .powered-by-wordpress --> </div><!-- .footer-credits --> <a class="to-the-top" href="#site-header"> <span class="to-the-top-long"> Ir arriba <span class="arrow" aria-hidden="true">↑</span> </span><!-- .to-the-top-long --> <span class="to-the-top-short"> Subir <span class="arrow" aria-hidden="true">↑</span> </span><!-- .to-the-top-short --> </a><!-- .to-the-top --> </div><!-- .section-inner --> </footer><!-- #site-footer --> <script src='https://eltecnofilo.es/wp-includes/js/comment-reply.min.js?ver=5.3.4'></script> <script src='https://eltecnofilo.es/wp-includes/js/wp-embed.min.js?ver=5.3.4'></script> <script> /(trident|msie)/i.test(navigator.userAgent)&&document.getElementById&&window.addEventListener&&window.addEventListener("hashchange",function(){var t,e=location.hash.substring(1);/^[A-z0-9_-]+$/.test(e)&&(t=document.getElementById(e))&&(/^(?:a|select|input|button|textarea)$/i.test(t.tagName)||(t.tabIndex=-1),t.focus())},!1); </script> </body> </html>