Quantcast
Channel: Adictos al trabajo
Viewing all 989 articles
Browse latest View live

Primeros pasos con AWS – crear una instancia de EC2

$
0
0

Índice de contenidos

1. Introducción

En este tutorial, crearemos nuestra primera instancia de EC2 en AWS. A través de este proceso, estudiaremos cada paso y aclararemos diferentes definiciones y conceptos para familiarizarse con el funcionamiento de AWS. Echaremos un vistazo más de cerca a los puntos clave para asegurarnos de que entendemos cada paso importante que damos.

Para completar el tutorial, necesitará una cuenta activa en AWS y un usuario con permisos para crear instancias. Si no tienes una cuenta segura y usas el usuario root, te recomiendo que primero leas el tutorial – Crear y securizar una cuenta en AWS

2. Antes de empezar

Antes de proceder a crear una instancia EC2, veamos las definiciones de algunos de los puntos clave en el proceso que sigue al tutorial.

  • Instancia EC2 – Amazon EC2 (Elastic Cloud Computing) permite a los usuarios crear máquinas virtuales (servidores) de su propia elección de configuraciones. Podemos definir la potencia de los servidores y básicamente, puede hacer lo que quiera con ellos. Como si fuera su propio ordenador físico, pero en la nube. Se puede, por ejemplo, instalar el software requerido que necesita y alojar un sitio web.
  • Regiones – En AWS, cualquier ubicación geográfica que tenga un Centro de datos se denomina Región. Por ejemplo, si visitas la página de infraestructura global de AWS y scrolleas hacia abajo, verás que tienen diferentes regiones en todo el mundo. Además, una región puede tener dos o más centros de datos diferentes. El propósito de estas regiones es permitirnos elegir dónde crear nuestras instancias para tener los niveles más bajos de latencia posibles.
  • AMI – Imagine que necesitamos una máquina con algún software preinstalado, una versión específica del sistema operativo con una configuración de seguridad. Cada vez que creamos una nueva instancia EC2, necesitaremos crear manualmente una nueva máquina con la misma configuración. Ahí viene el AMI (Amazon Machine Image). Cada imagen tiene una potencia de hardware diferente y un software preinstalado. En escenarios más avanzados, puede crear su AMI personalizada. Sin embargo, en este tutorial, utilizaremos uno que ya esté creado y disponible.

3. Crear una instancia de EC2

3.1 Encuentre y elija crear una instancia EC2

Desde la consola de gestión (AWS management console) podemos visualizar distintas opciones o servicios que nos ofrece AWS. Así que hagamos clic en el menú expandible y veamos que nos ofrece todos los servicios de AWS, ya categorizados. Tiene una variedad tan grande que puede ser confuso para un nuevo usuario. Lo que nos interesa a nosotros es EC2. Vamos a clicar allí.

img-aws-console

Ahora estamos ubicados en el panel de EC2. Antes de ingresar al proceso de inicio de la instancia, hay otra configuración no tan obvia que debemos establecer – establecer la región donde queremos que se cree nuestra instancia. La región activa actual se encuentra en la esquina superior derecha. Cuando proceda a crear una instancia EC2, se ubicará físicamente en el Centro de datos de la región que haya elegido desde arriba. Por lo tanto, cambie la región a la deseada (en nuestro caso, París porque es la más cercana) y haga clic en «Launch Instance» como se muestra en la imagen a continuación.

img-ec2-dashboard

3.2 Elegir el AMI

En la siguiente pantalla, debemos elegir la imagen que nos gustaría usar para nuestra instancia EC2. Igual, en el menú de la derecha, tenemos la opción de filtrar solo AMI de nivel libre. Entonces, cuando ingresamos con fines de investigación, hacemos clic en esa casilla de verificación así que solo aparecen imágenes de nivel libre. Después, seleccionamos la AMI de Amazon Linux.

img-elegir-ami

Importante: Que elijamos usar AMI solo con el nivel gratuito no significa que estemos exentos de pagar por este servicio. Cada AMI tiene diferente «instance type» y algunas de ellas son de pago, otras no. Podemos distinguir si un AMI es parte del nivel libre por la etiqueta que buscaremos a continuación.

El siguiente paso es elegir nuestro tipo de instancia (el AMI). Podemos ver que hay muchos tipos con diferentes características, para diferentes necesidades. A medida que avanzamos en la lista, se vuelven cada vez más caros. Observe de cerca el AMI preseleccionado automáticamente. Sobre la columna de tipo de instancia, tiene una etiqueta verde que dice «Free tier eligible».

img-elegir-ami-2

3.3 Parar por un momento y entender el pago

La etiqueta dice: «Las micro instancias son elegibles para el nivel de uso gratuito de AWS. Durante los primeros 12 meses posteriores a la fecha de registro de AWS, obtendrá hasta 750 horas de micro instancias cada mes. Cuando su nivel de uso gratuito caduca o si su uso excede las restricciones del nivel gratuito, usted paga tarifas estándar de servicio de pago por uso. Obtenga más información sobre la elegibilidad y restricciones del nivel de uso gratuito»

Las siguientes dos preguntas que surgen en nuestras mentes son:

  • ¿pero cuáles son las otras limitaciones?
    En el momento en que escribo este tutorial, se le cobrará si:
    • Superamos más de 750 horas de tiempo de actividad por mes o la configuración de hardware de la máquina (RAM, cpu credits, .etc).
    • Los 12 meses de free plan han terminado.
    • Usando un AMI que NO tiene la etiqueta que indica que es parte del plan gratuito.
  • Para conocer las otras limitaciones que nos ofrece el nivel gratuito de cada servicio de AWS, le sugiero que visite el sitio web de AWS y haga clic en el menu Pricing -> AWS Free Tier. El precio y las limitaciones pueden cambiar para cuando lea este tutorial. Por lo tanto, ingrese el enlace que he proporcionado y asegúrese de conocer los límites del servicio que está utilizando.
  • ¿cómo saber el precio de un AMI en particular? – Para averiguar el precio del AMI, visitamos la página de precios de AWS, o volvemos al menú superior Pricing -> Learn More About AWS pricing. Desplácese hacia abajo hasta la sección «Services Pricing» de la página y elija EC2″img-priicngEn la página siguiente, haga clic en el encabezado «On Demand»

    img-pricing-on-demand

    Aquí debemos elegir la región en la que estamos interesados en los precios y luego encontrar el AMI que queremos usar en la tabla. En nuestro caso, estamos buscando t2-micro. Como podemos ver, si los primeros 12 meses gratuitos caducan o superamos las limitaciones de AWS, se nos cobrará 0.0132 USD por hora.

    img-pricing-ami-particular

    Sin embargo, si bajamos la tabla, podemos ver que hay imágenes de hasta 12,00 USD por hora. Por lo tanto, una buena práctica es siempre investigar el precio de un AMI antes de comenzar a usarlo, para que no tengamos sorpresas al final del mes!

    3.4 Configurar EC2 User Data Script

    Ahora que entendemos el pago, procedamos al siguiente paso para crear nuestra instancia EC2. Entonces, regrese a la pantalla donde la dejamos , elija la instancia t2.micro y continue adelante. Verá la siguiente pantalla que nos brinda la oportunidad de configurar los detalles de la instancia. Para este tutorial, no necesitamos sumergirnos en esto. Sin embargo, quiero mostrarle los detalles avanzados de esta página, en particular el User Data.

    EC2 User Data representa un script que se ejecutará solo una vez después de que la máquina se inicie y nunca se ejecutará nuevamente. Es realmente fácil automatizar algunas tareas de arranque como instalar software particular, actualizaciones necesarias, descargando archivos comunes de internet etc.

    Importante: El User Data Script se ejecuta como root. Entonces, cualquier comando tendrá derechos de sudo, así que tenga cuidado.

    Así que desplácese hacia abajo hasta la configuración avanzada de la pantalla actual y elija ejecutar el script como texto.
    Para este ejemplo, incluyamos un script que instalará Apache Http Server y mostrará una página web simple.

    Desplácese hacia abajo hasta la parte inferior de la página actual y abra las opciones avanzadas. Luego inserte la siguiente secuencia de comandos en el cuadro de entrada de user data. La primera línea

    #!/bin/bash
    es realmente importante y si no está allí, las siguientes líneas no se leerán.
    #!/bin/bash
    sudo yum update -y
    sudo yum -y install httpd -y
    sudo service httpd start
    echo "Hello world from EC2 $(hostname -f)" > /var/www/html/index.html

    La configuración debería verse así

    img-config

    Los siguientes dos pasos «Add Storage» y «Add Tags» los podemos omitir por ahora. Así que vaya al paso 6 «Configure Security Group». Lo importante es saber que el storage que tenemos (el storage local de archivos de la instancia) no persiste. Eso significa que si cierra la instancia, se pierde. Hay soluciones a este problema. Por ejemplo, podemos usar el Amazon Simple Storage Service (S3).

    3.5 Configurar Security Group

    Un grupo de seguridad actúa como un firewall virtual para su instancia para controlar el tráfico entrante y saliente. Si no tiene ningún grupo de seguridad, le pedirá que cree uno de inmediato. Entonces, de manera predeterminada, hemos habilitado el acceso SSH a la máquina, y agregamos un HTTP para que podamos acceder a nuestra página web. Para hacerlo, siga los pasos de la captura de pantalla a continuación. Tenga en cuenta que con las flechas 4 y 5 estamos configurando que podemos acceder a la máquina a través de SSH y HTTP desde cualquier otra dirección IP. Para nuestro ejemplo no importa, pero en algunos proyectos serios, la práctica es permitir solo direcciones IP conocidas.

    img-steps-4-5

    3.6 Review and launch

    En esta página vemos el resumen de nuestra instancia, haga clic en «botón de inicio» en la esquina inferior derecha y verá la siguiente pantalla, pidiéndole que cree un par clave-valor.

    img-key-value-pair

    Eso será necesario si queremos conectarnos a la instancia desde nuestra máquina local. Porque, por supuesto, no tendremos una conexión de escritorio, por lo que esa sería la única forma de acceder a ella. Así que vamos a conectarnos a través de la línea de comando, y para identificarnos, necesitaríamos ese par clave-valor. Entonces elija crear un nuevo par. Luego inserte el nombre del par, descárguelo e inicie la instancia.

    img-download-key-value

    El archivo que acaba de descargar se requerirá explícitamente cuando intente acceder a la máquina a través de ssh.

    ¡Genial! ¡La instancia que acaba de crear ahora se está iniciando! Para ver sus instancias EC2, haga clic en el botón «Ver instancias» que se muestra en la captura de pantalla a continuación.
    img-preview-and-launch

    4. Observar la instancia en ejecución

    Ahora, en esta página, debería ver su instancia EC2 arrancando. Allí podemos ver su estado, su IP pública e información adicional. Además, podemos realizar acciones como start, stop o terminate (es igual a eliminar). La instancia no tendrá nombre, así que simplemente haga clic en la columna de nombre para cambiarla.

    img-ec2-instances-list

    Entonces, para acceder a nuestro servidor web, simplemente debemos copiar la dirección IP pública de la máquina y pegarla en el navegador. Es importante que se espera a que el estado de la máquina sea «running» y que las «status checks» sean 2/2. De lo contrario, puede intentar acceder a él mientras todavía se está iniciando, por lo que no podrá. Tenga en cuenta que cada vez que reiniciemos nuestra instancia, recibirá una IP pública diferente.

    img-status-check

    Bien, acabamos de acceder a la página web que creamos usando el script de User Data.

    5. SSH Access

    Para acceder a la máquina a través de SSH, abre la terminal y ejecuta el siguiente comando:

    ssh -i /path/to/key/value/pair/my_instance_access.pem ec2-user@instance_public_ip

    Una vez que accedemos a la máquina, podemos verificar que lo hemos hecho todo correctamente, ejecuta el siguiente comando

    cat /var/www/html/index.html

    Importante: En algunos casos, es posible que salga un error indicando que no tienes permisos para acceder al archivo «.pem». Entonces, ejecute

    chmod 400
    contra el archivo. Establecerá – a + rwx, u-wx, g-rwx, o-rwx. Esto significa que el usuario / propietario puede leer, no puede escribir y no puede ejecutar. Los grupos y los otros usuarios no tienen ningún permiso.

    Deberías recibir el siguiente resultado

    img

    6. Conclusión

    Acabamos de crear nuestra primera instancia de EC2 en AWS, pasando por el proceso lentamente con la comprensión de los puntos clave. Además, hemos entendido cómo funcionan los precios. Cómo acceder a la máquina a través de ssh y configurar el acceso SSH y HTTP a través de la configuración del grupo de seguridad. Ahora podemos proceder a ejemplos más específicos y ampliar nuestro conocimiento de AWS.

    Cuando termine de usar las instancias, no olvide detenerlas para evitar cargos adicionales.img

La entrada Primeros pasos con AWS – crear una instancia de EC2 se publicó primero en Adictos al trabajo.


A11y Pill: La importancia de organizar el contenido

$
0
0

Índice

1 – Introducción

La organización del contenido es muy importante para ofrecer una experiencia de usuario satisfactoria. La velocidad de navegación y la facilidad con la que los usuarios encuentran la información que buscan tiene mucho que ver con el orden.

En términos de accesibilidad, organizar el contenido de forma adecuada también tiene una fuerte repercusión. En este artículo vamos a ver algunas pautas que podemos seguir para tener nuestro contenido bien organizado para cualquier usuario.

2 – El flujo del documento

Es difícil establecer un orden sólido entre los elementos de una página si nos guiamos puramente por el aspecto visual. Diferentes regiones siguen orientaciones del contenido distintas. Un elemento inferior debería ser leído antes que uno que se encuentra a la derecha; o no. Los detalles de un elemento deberían intercalarse entre él y el siguiente cuando se despliegan…

Obviamente, este enfoque no funciona. Necesitamos tener un criterio sólido y predecible. Por eso, las tecnologías de asistencia no se basan en el layout de los elementos, sino en el flujo del documento. Esto es, simplemente, el orden en que los elementos son introducidos dentro del árbol de vistas, ya sea en una página web o en una aplicación nativa.

El orden en el que se presentan los elementos es importante. Nuestro cerebro deduce gran parte de la información implícita gracias al contexto. Pero, de igual forma que lo hacemos cuando vemos algo a través de la vista, también debemos poder hacerlo cuando no tenemos acceso a este sentido.

Un problema que presentan muchos sitios y aplicaciones, es que, aunque visualmente sus elementos están relacionados, estos quedan muy lejos dentro del flujo del documento. Es típico utilizar desplegables que se añaden al final del HTML, aunque el control que los expandió esté en mitad de la página. Además de un buen manejo del foco y de aportar más información mediante la semántica de WAI-ARIA, es más conveniente insertar este nuevo desplegable lo más próximo posible detrás del elemento que lo invocó. Así es como queremos que se interprete, al fin y al cabo.

3 – Navegación consistente

Igual de importante que el orden de los elementos es encontrar una navegación consistente. Por suerte, este es un problema casi extinto en los sitios modernos. El sentido común y las plantillas han impuesto modelos de navegación homogéneos entre las páginas de un mismo sitio web. Contadas son las aberraciones que presentan un layout diferente con cada página que te adentras en ellas.

Aquí también cabría hablar de los iconos y su significado. Debemos procurar que este se mantenga homogéneo a lo largo de toda nuestra aplicación. No es coherente que un aspa indique la acción de cerrar en un punto y la de eliminar en otro. Esto puede conducir a problemas severos en la predictibilidad de nuestro producto y causar inseguridad a los usuarios.

4 – Jerarquía del contenido

Algo imprescindible cuando el contenido tiene cierto tamaño es poder establecer una jerarquía. Encontrar la información es más fácil si la tenemos dividida en piezas que siguen una estructura lógica.

Podemos conseguir esto mediante los encabezados (h1, h2, h3…). Sin embargo, estos no siempre se utilizan bien. No son pocas las páginas que usan estas etiquetas para destacar contenido o, simplemente, para darle mayor importancia visual. Que quede claro: las etiquetas de encabezado no tienen ninguna relación con el tamaño de la fuente. Los navegadores le otorgan uno mayor por defecto, porque es lo lógico. Pero podría ser igual de grande que el resto del texto o incluso más pequeño. La apariencia del texto debe ser proporcionada por estilos. Las etiquetas deben reservarse para su función semántica.

Del mismo modo, estamos hablando de jerarquías. Debemos respetar los diferentes niveles de encabezados como si se tratasen de un árbol. No podemos saltarnos un nivel. Semánticamente, no tiene sentido. Esto sólo conduce a tener peores experiencias de usuario cuando utilizamos tecnologías de asistencia, ya que uno no sabe si el siguiente apartado tendrá algo que ver con el tema que estábamos viendo antes o no.

Además, estos encabezados pueden ser utilizados como puntos de salto por las tecnologías de asistencia. Ofrecer una jerarquía bien estructurada y realmente útil puede suponer un ahorro considerable en el tiempo que un usuario tarda en encontrar el dato que busca.

6 – Conclusiones

Organizar el contenido de nuestro sitio web o aplicación es algo sencillo que puede aportar un gran beneficio a los usuarios. Cómo hacerlo depende de cada caso concreto, pero suele responder a una lógica intrínseca.

La entrada A11y Pill: La importancia de organizar el contenido se publicó primero en Adictos al trabajo.

Usar Delegates y Events en Unity

$
0
0

Introducción

El objetivo de este tutorial es entender cómo usar Delegates y Events en Unity. No existen muchos ejemplos prácticos en este tema en español por lo que con este tutorial trato de llenar ese hueco.

Para seguir este tutorial se asume que se tiene un conocimiento básico de Unity: se sabe navegar en el editor, crear componentes y trabajar con colisiones con Colliders y Triggers. Para empezar en Unity recomiendo este tutorial oficial de un proyecto en 3D y por supuesto el manual.

Entorno

El tutorial se ha realizado en el siguiente entorno:

  • Portatil MSI GL62M (CPU 7700HQ, GPU 1050Ti y 8 GB de RAM).
  • Windows 10 Education.
  • Unity 2020.2 Personal.

Conceptos

Un Delegate es una referencia a un método. Nos permite tratar ese método como una variable y pasarlo como variable para un callback. Cuando se invoca, notifica a todos los métodos que hacen referencia al Delegate. El funcionamiento es el del patrón Observador.

Por poner un ejemplo del mundo real es como el de un periódico. Una persona puede suscribirse a dicho periódico y cada vez que salga una nueva entrega todas las personas suscritas la recibirán.

Los eventos añaden una capa de abstracción y protección al Delegate. Esto previene al cliente del Delegate de resetearlo y eliminar la lista de suscriptores.

Ejemplo práctico

Vamos a aplicarlo en un ejemplo práctico. Tenemos una escena en la que hay una puerta y una baldosa en el suelo que funciona como interruptor. Queremos que cuando un objeto entre en contacto con la baldosa se abra la puerta.

En la imagen se muestra la escena:

En la imagen se muestra la escena con los distintos objetos.

El cubo azul va a ser lo que mueva el jugador, la baldosa marrón claro el interruptor y el prisma rojo la puerta. El resto de la escena son cubos que hacen de pared y suelo.

Interruptor

El interruptor es un cubo con un Box Collider con el valor trigger activado.

Definamos un script para la puerta:

using UnityEngine;
using System.Collections;

public class DoorSwitch : MonoBehaviour
{

    public delegate void Contact();
    public event Contact OnContact;
     
    private void OnColliderEnter(Collider other)
    {
        if(OnContact != null)
            OnContact();
    }

}

Estamos usando OnColliderEnter, que se va a disparar en la frame en la que el objeto entre en contacto con un Collider. Para que este evento se dispare es necesario que:

  1. Alguno de los dos objetos que participan en el contacto debe tener un Rigidbody.
  2. El objeto que entra en contacto tiene un Collider y no es trigger.

Es una imagen de los componentes del objeto del interruptor.

Puerta

Necesitamos que la puerta se suscriba al evento de OnContact del interruptor para que se abra cuando se dispare. Hay que dar de baja el evento con el método OnDisable() (el cual se llama cuando se elimine el objeto o se desactive el componente) para evitar que se quede en memoria, ya que Unity no va a encargarse de ello.

using UnityEngine;
using System.Collections;

public class Door : MonoBehaviour
{

    public DoorSwitch doorSwitch;

    void Start()
    {
        doorSwitch.OnContact += Open;
    }

    private void Open()
    {
        transform.Translate(Vector3.up * 3f);
    }

    void OnDisable()
    {
        doorSwitch.OnContact -= Open;
    }

}

La puerta, al recibir el evento de OnContact, va invocar el método Open() y subir 3 metros hacia arriba. Hemos definido el campo doorSwitch como public para poder arrastrar el objeto del interruptor en el editor.

Es una imagen de los componentes del objeto puerta.

Jugador

El jugador va a ser el objeto que colisione con el interruptor y abra la puerta. Visualmente va a ser un simple cubo, que va a tener un Box Collider y un Rigidbody. Le vamos a añadir un componente con el siguiente script para que pueda moverse en el plano horizontal con las teclas WASD:

using UnityEngine;
using System.Collections;public class Player : MonoBehaviour
{

    void Update()
    {
        if (Input.GetKey(KeyCode.A))
            transform.Translate(2 * Time.deltaTime * Vector3.left);
        if (Input.GetKey(KeyCode.D))
            transform.Translate(2 * Time.deltaTime * Vector3.right);
        if (Input.GetKey(KeyCode.W))
            transform.Translate(2 * Time.deltaTime * Vector3.up);
        if (Input.GetKey(KeyCode.S))
            transform.Translate(2 * Time.deltaTime * Vector3.down);
    }
}

Es una imagen de los componentes del objeto jugador.

Probar el juego

Al entrar en el modo Play y mover el cubo azul hasta el interruptor comprobamos que la puerta sube.

Se muestra la escena en la que la puerta ha subido.

Conclusión

En este tutorial se ha visto cómo  usar Delegates y Events en Unity, montando una pequeña escena de ejemplo.

El uso de Delegates y Events de C# nos permite tener un código más simple y evitar referencias directas entre los componentes. La clase que emite el evento no tiene que saber qué objetos van a querer estar suscritos al evento y en tiempo de ejecución pueden añadirse o eliminarse suscriptores.

Tiene la limitación de que no tiene representación en el editor y para los miembros del equipo que no sean programadores es un obstáculo, pero en caso de que se quieran manejar eventos en el editor siempre se pueden utilizar UnityEvents.

También hay que recordar dar de baja los eventos cuando se eliminen los objetos que lo usan, para evitar fugas de memoria.

Referencias

http://www.unitygeek.com/delegates-events-unity/

 

La entrada Usar Delegates y Events en Unity se publicó primero en Adictos al trabajo.

Primeros pasos con Terraform – crear instancia EC2 en AWS

$
0
0

Índice de contenidos

1. Introducción

En este tutorial, conoceremos Terraform junto con algunos otros términos que nos ayudarán a comprender mejor su propósito. Luego crearemos una instancia EC2 en AWS. Durante el proceso, analizaremos una práctica para ocultar datos confidenciales cuando use Terraform, configuraremos un grupo de seguridad con reglas entrantes y salientes, y mostraremos una página web simple en el navegador.

    Para completar este tutorial necesitarás:
  • Máquina Linux / macOS
  • Cuenta de AWS
  • Comprensión básica de lo que es un Grupo De Seguridad, Región, Instancia EC2 y AMI en AWS

Si no tienes una cuenta de AWS configurada y segura, puedes completar el tutorial AWS – Crear y securizar una cuenta

Si estas empezando con AWS también, te recomiendo que primero revises este tutorial – Primeros pasos con AWS – crear una instancia de EC2

2. Que es Terraform

Terraform es una herramienta open source, desarrollada por HashiCorp que le brinda el poder de automatizar y administrar su infraestructura, y también su plataforma y servicios, utilizando un lenguaje declarativo. Escribe planes y crea Infraestructura como código.

Cuando trabajamos en Terraform, vamos a utilizar comandos específicos que son como «etapas». Son:

  • init – este comando instalará todos los binarios del proveedor y preparará el directorio de trabajo para trabajar con él.
  • plan – lee el código de configuración que has escrito, compara el estado deseado de la infraestructura con lo que realmente existe. Luego crea un plan de qué hacer para que pueda alcanzar su estado deseado y muestra lo que se cambiará.
  • apply – este comando ejecuta terraform plan al principio y luego procede a crear su infraestructura
  • destroy – se usa para destruir la infraestructura administrada por Terraform.

3. Infrastructure as code

Entonces, la infraestructura como código es básicamente poder usar un cliente o una herramienta para escribir y ejecutar su infraestructura utilizando comandos o lenguaje declarativo en lugar de hacerlo manualmente. Cuando hablamos de automatizar el «proceso de rotación» de nuestra infraestructura, tenemos dos enfoques diferentes que podemos usar.

  • Enfoque imperativo – este enfoque está relacionado con los comandos de CLI de cableado de un desarrollador para que la infraestructura esté en funcionamiento en algún proveedor. Por ejemplo, si queremos tener una infraestructura ejecutándose con Kubernetes y una máquina virtual en una VPC, decimos:
    client create Kuberenetes
        ...more commands
    client create Virtual Machine
        ...more commands				
    client wrap Kubernetes and Virtual Machine into a VPC
        ...more commands
  • Enfoque declarativo – lo que este enfoque le permite hacer es definir el estado final requerido de la infraestructura sin ejecutar un solo comando y luego dejar que el proveedor se encargue del resto.
    I want to have Kubernetes resource running:
        - additional config
    I want a VM resource too:
        - additional config
    I want them to be wrapped into a VPC resource:
        - additional config

En conclusion. Si ejecutamos el script de enfoque imperativo, terminaremos con múltiples entornos. Si uno de los pasos falla, deberá proporcionar una forma adicional de manejar el error, revertir algunos cambios necesarios o intentar recrear la instancia. Con el enfoque declarativo, no importa cuántas veces ejecute el script, siempre termina con el mismo resultado que ha declarado porque la herramienta (en nuestro caso Terraform) se encarga de todo por nosotros. Entonces tenemos esa independencia cuando trabajamos con un enfoque declarativo.

4. Cómo instalar Terraform

Yo, como lo tengo instalado en mi máquina local, lo instalaré de nuevo en mi RaspberryPi en Ubuntu 20.04 LTS. Todos los comandos van a servir para los usuarios de Linux y macOS. Vamos a proceder con los siguientes pasos:

1. Ir a la página de descarga oficial y copiar el enlace de la instalación para tu sistema operativo específico.


2. Ir a la terminal y lanza el comando siguiente para bajar el zip. Reemplazar el enlace con el enlace que acaba de copiar del sitio web para su sistema operativo

wget https://releases.hashicorp.com/terraform/0.12.26/terraform_0.12.26_linux_arm.zip

3. Descomprimir el archivo. Si no tienes unzip, puedes instalarlo con apt-get install unzip

unzip terraform_0.12.26_linux_arm.zip

Verás que se extrajo un archivo binario llamado «terraform».

4. Si intentas ejecutar el comando

$ terraform
en el terminal, dirá «comando no encontrado». Dado que es un archivo binario y no será cómodo ejecutarlo cada vez, establezcamos una ruta hacia él. Ejecuta los comandos:
echo $"export PATH=\$PATH:$(pwd)" >> ~/.bash_profile

source ~/.bash_profile

La parte

\$PATH:$(pwd)
del primer comando es simplemente la ruta actual en la que estamos. Así que asegúrate de estar en el directorio donde descomprimiste Terraform o sustituye esa parte con
"/path/to/terraform/binary/file"
.

Ahora, cuando ejecutes

terraform
en el terminal, deberías ver el siguiente resultado que indica que lo hemos instalado con éxito y está listo para usar.

Te pedí intencionalmente que ejecutaras el comando

$ terraform
en lugar de
$ terraform -version
para que pidieras echar un vistazo y ver la lista de comandos, ¡siempre es bueno hacerlo!

5. Crear una EC2 instancia simple

Si usas VS Code para un editor de texto, ten en cuenta que hay un plugin de Terraform que ayuda a escribir la configuración. Se parece a esto:

Vamos a crear una instancia EC2 simple usando Terraform y AWS como proveedor de la nube. Antes de eso, navega a tu carpeta de practica de Terraform y crea un archivo con la extensión «.tf». Este será tu archivo de configuración principal. Lo he llamado «main.tf».

Entonces, el primer paso es preparar el directorio de trabajo para usarlo con AWS. Para hacerlo, en nuestro archivo main.tf, escribe la siguiente configuración de proveedor:

provider "aws" {
	region = "eu-west-3"
    access_key = "aws_user_access_key"
	secret_key = "aws_user_secret_key"
}

En los campos de clave de acceso y clave secreta, debemos proporcionar las credenciales para nuestro usuario específico de AWS.

No se recomienda usar la cuenta raíz para las tareas diarias, por lo que es mejor proporcionar credenciales para el usuario de IAM que tiene los permisos de administrador. Para obtener las credenciales de usuario, ve a la página del documentación oficial y sigue los pasos.

Guarda el fichero y ejecuta el comando

terraform init
desde el terminal. Este comando instalará todos los binarios del proveedor y preparará el directorio de trabajo para trabajar con él. Deberías ver el siguiente resultado que indica que la inicialización fue exitosa:

Dado que el archivo principal de terraform se va a cargar en el repositorio de código con todo el proyecto, claramente no podemos almacenar ningún dato sensible. Así que pongamos la clave secreta y la clave de acceso en un archivo diferente, y coloquemos este archivo en nuestro gitignore.

Para separar los datos sensibles, crea un archivo en el mismo directorio llamado terraform.tfvars. Y decláralos así:

my_access_key = "RANDOM_TEST_ACCESS_KEY"
my_secret_key = "RANDOM_TEST_SECRET_KEY"

Los archivos de valores variables con nombres que no coinciden con terraform.tfvars o *.auto.tfvars se pueden especificar con la opción

-var-file

Para usar las variables en el archivo Terraform principal, necesitamos declararlas con el mismo nombre que les dimos en el archivo «terraform.tfvars» y luego acceder a cada una así 

var.varName
. Para probar que está funcionando, incluyamos también la salida de estas variables para que podamos verlas en la consola.

Su archivo principal de Terraform debería verse así:

variable "my_access_key" {
  description = "Access-key-for-AWS"
  default = "no_access_key_value_found"
}

variable "my_secret_key" {
  description = "Secret-key-for-AWS"
  default = "no_secret_key_value_found"
}

output "access_key_is" {
  value = var.my_access_key
}

output "secret_key_is" {
  value = var.my_secret_key
}

provider "aws" {
	region = "eu-west-3"
	access_key = var.my_access_key
	secret_key = var.my_secret_key
}

Ejecuta

terraform apply
y deberías recibir un mensaje de éxito que muestra las variables de salida con sus valores:

Ahora reemplaza los valores de prueba de la clave secreta y de acceso con los reales de AWS y elimina los output variables del archivo principal.

El último paso para iniciar la instancia ec2 es agregar la configuración de recursos. Estamos especificando el ID de AMI que tomamos del sitio web de AWS y el tipo de la instancia.

resource "aws_instance" "example" {
	ami = "ami-0357d42faf6fa582f"
	instance_type = "t2.micro"
}

Ejecuta

terraform plan
y verás que indica que se va a crear una instancia de EC2 y todos los cambios que implica.

Luego ejecute

terraform apply
y verá que le da una vez más todas las cosas que van a cambiar. Entonces, ¿por qué ejecutar
terraform plan
antes?

El plan Terraform se usa cuando desea hacer una revisión de lo que has configurado, es realmente útil durante las revisiones de código y nos ayuda a comprender mejor los cambios que hemos realizado.

Te preguntará si deseas continuar, escribe

yes
y presiona enter. Luego debes esperar hasta que Terraform cree y lance la instancia, y reciba el siguiente resultado:

Si vamos a la consola de administración de aws y elegimos la ventana de instancia EC2, veremos nuestra instancia ec2 en funcionamiento.

Pero observa que la instancia no tiene nombre. Vamos a cambiar nuestra configuración de Terraform para incluir una etiqueta de nombre en la instancia y ver cómo se comporta al cambiar.

Agrega una configuración de objeto de etiquetas a nuestra parte de recursos EC2 como esta y ejecuta

terraform apply
:
resource "aws_instance" "example" {
	ami = "ami-0357d42faf6fa582f"
	instance_type = "t2.micro"
	
	tags = {
		Name = "My first EC2 using Terraform"
	}
}

Terraform indica que hay un cambio detectado.

Podemos ver que tenemos la misma instancia, pero con una etiqueta de nombre agregada. Por lo tanto, Terraform fue lo suficientemente inteligente como para descubrir que no es necesario crear una nueva instancia con ese nombre, sino actualizar la existente.

6. Añadir User Data y Security Group

En esta sección, incluyamos un script de User Data que instalará Apache Http Server y mostrará una página web simple. Luego creamos un Security Group con rules de seguridad que abren el puerto 80 para conexiones desde fuera.

Entonces, para crear un grupo de seguridad para que podamos permitir las conexiones a través de HTTP en el puerto 80, debemos agregarlo como un recurso en nuestro archivo main.tf. En la parte de «ingress» estamos escribiendo la configuración de la regla (inbound rules). El rango de puertos será de 80 a 80, el protocolo es TCP. Y en los bloques CIDR estamos diciendo que permitimos el acceso desde cualquier IP. El bloque de «egress» define la regla para el tráfico saliente (outbound rules) y aquí la hemos definido para permitir todo. La combinación de from_port = 0, to_port = 0 y el protocol = «-1» significa «permitir el tráfico saliente desde todos los puertos y todos los protocolos».

resource "aws_security_group" "instance" {
	name = "terraform-tcp-security-group"
	
	ingress {
		from_port = 80
		to_port = 80
		protocol = "tcp"
		cidr_blocks = ["0.0.0.0/0"]
	}

    egress {
        from_port = 0
        to_port = 0
        protocol = "-1"
        cidr_blocks = ["0.0.0.0/0"]
    }
}

Nos quedan dos cosas por hacer. Escribir el script de User Data y vincular la instancia de EC2 al grupo de seguridad que acabamos de crear. De lo contrario, no estarán involucrados.

Agreguemos el script de user data dentro del EC2 resource:

user_data = <<-EOF
	        #!/bin/bash
		    sudo yum update -y
		    sudo yum -y install httpd -y
		    sudo service httpd start
		    echo "Hello world from EC2 $(hostname -f)" > /var/www/html/index.html
		    EOF

Y luego, en la misma configuración de EC2 resource, necesitamos especificar la instancia que necesita usar el grupo de seguridad que creamos.

vpc_security_group_ids = [aws_security_group.instance.id]

Al final, el archivo main.tf debería verse así:

variable "my_access_key" {
  description = "Access-key-for-AWS"
  default = "no_access_key_value_found"
}

variable "my_secret_key" {
  description = "Secret-key-for-AWS"
  default = "no_secret_key_value_found"
}

provider "aws" {
	region = "eu-west-3"
	access_key = var.my_access_key
	secret_key = var.my_secret_key
}

resource "aws_instance" "example" {
	ami = "ami-0357d42faf6fa582f"
	instance_type = "t2.micro"

	user_data = <<-EOF
	        #!/bin/bash
		    sudo yum update -y
		    sudo yum -y install httpd -y
		    sudo service httpd start
		    echo "Hello world from EC2 $(hostname -f)" > /var/www/html/index.html
		    EOF
				
	tags = {
		Name = "My first EC2 using Terraform"
	}
	vpc_security_group_ids = [aws_security_group.instance.id]
}

resource "aws_security_group" "instance" {
	name = "terraform-tcp-security-group"

	ingress {
		from_port = 80
		to_port = 80
		protocol = "tcp"
		cidr_blocks = ["0.0.0.0/0"]
	}

    egress {
        from_port = 0
        to_port = 0
        protocol = "-1"
        cidr_blocks = ["0.0.0.0/0"]
    }
}

Ejecuta

terraform apply
y espera hasta que cree todo para nosotros. Luego, ingresa al panel de control EC2 y verás que tenemos nuestra instancia EC2 actualizada y el grupo de seguridad creado con sus reglas de entrada y salida.

Ahora intente acceder a la máquina a través de su IP pública y verá la siguiente respuesta

7. Conclusión

Al final, tenemos una instancia EC2 en funcionamiento que muestra una página web simple a través del acceso HTTP, creada usando Terraform. Utilizamos variables de Terraform y buenas prácticas para ocultar datos sensibles. Aprendimos a crear y vincular un grupo de seguridad simple e insertar un Script de User Data.

No olvides ninguna instancia en ejecución en tu cuenta de AWS. Así que ejecuta

terraform destroy
. Siempre puedes recrear esta instancia de EC2 con la configuración que tenemos con un
terraform apply
🙂

La entrada Primeros pasos con Terraform – crear instancia EC2 en AWS se publicó primero en Adictos al trabajo.

Extensiones de VS Code necesarias para desarrollos java

$
0
0

En este tutorial descubriremos las extensiones imprescindibles para tener nuestro Visual Studio Code “tuneado” y poder realizar desarrollos en java y así no envidiar nada a los demás IDE.

Índice de contenidos

  1. Introducción
  2. Entorno
  3. Extensiones obligatorias
    1.  Visual Studio IntelliCode
    2. Path Intellisense
    3. Bracket Pair Colorizer
    4. GitLens
    5. Prettier
    6. Color Highlight
    7. Indent Rainbow
  4. Extensiones para proyectos Java
    1. Java Extension Pack
    2.  Spring Boot
    3. Docker
    4. Tomcat
  5. Conclusiones
  6. Referencias

1. Introducción

Visual Studio Code para el que no lo conozca es un editor de código fuente sencillo y potente desarrollado por Microsoft. Está disponible para MacOs, Linux y Windows.

Nos ofrece la posibilidad de instalar extensiones y así agregarle la funcionalidad que deseemos.

Desde su lanzamiento en 2015 ha ido tomando cada vez más protagonismo hasta convertirse en uno de los editores de código más utilizado.

Estos datos podemos verlos en las encuestas que realiza StackOverflow cada año y que sitúan a Visual Studio Code como el claro favorito entre los desarrolladores. 

–> Encuesta 2019 <–

La gran mayoría de proyectos que se realizan con Visual Studio Code son de front-end, es sin duda alguna el referente en estos desarrollos. 

¿Y para proyectos Java? 

Pues la verdad, me vi gratamente sorprendido ya que instalando unas pocas extensiones no tiene nada que envidiar a los demás IDE.

¿Por qué se me ocurrió usar Visual Studio Code para un desarrollo Java? 

Mi anterior ordenador (MacBook 17” de 2009) sufría mucho cuando el entorno estaba levantado. Gracias a la herramienta “monitor de actividad” que tiene mac pude ver que el uso de memoria de mi IDE era bastante elevado, más o menos de 1GB a 1,5 GB, por lo que me vi “obligado” a buscar un nuevo IDE y me topé con Visual Studio Code que el uso que hace de memoria es muy inferior más o menos de 100 MB a 300 MB sumando todos sus subprocesos.

2. Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro 15′ (2,5 Ghz Intel Core i7, 16GB DDR3).
  • Sistema Operativo: Mac OS Catalina 10.15.1

3. Extensiones obligatorias

Existen extensiones que dejando aparte el tipo de desarrollo que estés realizando, su instalación es casi obligatoria.

3.1. Visual Studio IntelliCode

Es una extensión que incorpora inteligencia artificial para ayudarte a codificar. Admite Python, JavaScript / TypeScript y Java.

Visual Studio IntelliCode

3.2. Path Intellisense

Esta extensión permite escribir fácilmente nombres de rutas de archivos.

Path intellisense

3.3. Bracket Pair Colorizer

Nos ayuda a ver más fácilmente el bloque de código que se encuentra entre los caracteres (), {}, [] trazando una línea. Permite configurar otros tipos de caracteres.

Bracker Pair Colorized ← 

3.4. GitLens

Sobrealimenta las capacidades de Git que ya se encuentran integradas en Visual Studio Code. Ayuda a visualizar el autor del código, navegar y explorar sin problemas los repositorios de Git, obtener información valiosa a través de potentes comandos de comparación y mucho más.

GitLens ← 

3.5. Prettier

Herramienta que formatea el código automáticamente, esto permite despreocuparse de si nuestro código esta bien indentado.

Prettier ← 

3.6. Color Highlight

Facilita la visualización de los colores. Rodea el código hexadecimal del color en un rectángulo con el color elegido.

Color Highlight ← 

3.7. Indent Rainbow

Esta extensión colorea la sangría frente a su texto alternando cuatro colores diferentes en cada paso, ayuda a visualizar el correcto indentado del código.

Indent Rainbow ← 

4. Extensiones para proyectos Java

Usar Visual Studio Code como IDE para desarrollar un proyecto en Java no es lo más habitual, pero le he dado una oportunidad y no me ha decepcionado para nada. Junto con las extensiones necesarias no tiene nada que envidiar a los demás IDE y además es necesario recordar que es GRATIS.

4.1. Java Extension Pack

Este pack contiene las extensiones que juntas nos brindan todas las herramientas que necesitamos para realizar nuestro desarrollo en Java.

  • Language Support for Java by Red Hat
    • Utiliza M2Eclipse esto permite que los proyectos Maven, Eclipse y Gradle Java sean compatibles pudiendo trabajar con código procedente de otros proyectos anteriores.
  • Debugger for Java
    • Es un depurador de Java ligero basado en el servidor de depuración de Java. Funciona con el Soporte de idiomas para Java de Red Hat y permite depurar nuestro código Java dentro de Visual Studio Code.
  • Java Test Runner
    • Podemos ejecutar, depurar y administrar fácilmente nuestros casos de prueba JUnit y TestNG.
  • Maven for Java
    • Podemos generar proyectos y navegar a través de todos los proyectos de Maven dentro de su área de trabajo.
  • Java Dependency Viewer
    • Es una extensión ligera que proporciona características adicionales al explorador de proyectos. Funciona con el Soporte de idiomas para Java de Red Hat, nos permite ver las dependencias de nuestro proyecto.
  • Visual Studio IntelliCode
    • Se trata de un conjunto de capacidades basadas inteligencia artificial que mejora nuestra productividad como desarrolladores, nos brinda sugerencias basadas en buenas prácticas dentro del contexto en el cual trabajamos.

Java Extension Pack  ← 

4.2. Spring Boot

Podemos agregarle aún más funcionalidad, si queremos trabajar en un proyecto hecho con Spring Boot debemos instalar las siguientes extensiones:

  • Spring Boot Tools
    • Proporciona validación y asistencia de contenido para los archivos de propiedades application.properties, application.yml de Spring Boot. Además de soporte específico de arranque para archivos .java

Instala las extensiones para realizar el desarrollo y despliegue de una aplicación hecha con Spring Boot.

    • Spring Initializr Java Support
      • Es una extensión ligera para generar rápidamente un proyecto Spring Boot en Visual Studio Code (VS Code). Ayuda a personalizar proyectos con configuraciones y administrar dependencias de Spring Boot.
    • Spring Boot Dashboard
      • Agrega en la barra lateral una opción más con la que podemos ver y administrar todos los proyectos Spring Boot que tenemos en el workspace. También admite las funciones para iniciar, detener o depurar rápidamente un proyecto Spring Boot.

4.3. Docker

Facilita la creación, administración e implementación de aplicaciones en contenedores desde Visual Studio Code.

Docker ← 

4.4. Tomcat

Esta extensión nos facilita el despliegue de nuestra aplicación hecha con spring en local con un solo click.

Tomcat for Java ← 

5. Conclusiones

Gracias a las múltiples extensiones podemos convertir Visual Studio Code en un auténtico IDE para realizar nuestros desarrollos en java, ocupando poca memoria y lo mejor de todo es que todo es GRATIS.

6. Referencias

La entrada Extensiones de VS Code necesarias para desarrollos java se publicó primero en Adictos al trabajo.

WWDC 2020, novedades en el mundo del desarrollo Apple. (1ª parte)

$
0
0

Índice de contenidos

  • 1. Introducción
  • 2. Transición a ARM (Apple Silicon)
  • 3. Xcode 12
  • 4. Mac Catalyst
  • 5. Novedades en SwiftUI
  • 6. Novedades en UIKit
  • 7. Conclusiones

 

1. Introducción.

La navidad para los desarrolladores de entornos Apple ha llegado y este año de una forma totalmente virtual debido a las complicaciones del COVID. Sin público, y todas las sesiones  pre grabadas y con posibilidad de hablar con los ingenieros de Apple mediante un sistema de preselección de las dudas que podemos presentar. Vamos a ver por encima las novedades más destacas desde el punto de vista del desarrollo.

 

2.Transición a ARM (Apple Silicon)

A día 23 de Junio de 2020 Apple ha dado un paso hacia la independencia total de su producción de hardware anunciando la transición en Mac a sus propios procesadores con arquitectura ARM llamados Apple Silicon. 

En la keynote han usado un Mac con un chip A12Z (los que llevan los actuales iPad Pro) en la que mostraban aplicaciones ya corriendo en los mac con esta arquitectura. Apple ya está trabajando con las grandes de la industria del desarrollo para que porten sus aplicaciones a la nueva arquitectura, de todas formas si no se lleva a cabo la importación, Apple ha creado Roseta 2.0 que se encargará de traducir de una arquitectura a otra en tiempo de instalación.

Con este cambio también llegan las apps universales, esto es, que con un solo binario vamos a poder desplegar en todas las plataformas de Apple,  y ya que corren la misma arquitectura, vamos a poder ejecutar aplicaciones de iOS e iPadOS sin modificación alguna en nuestros mac.

Todo indica que Apple tiene muy claro su objetivo de unificar su ecosistema, con el proyecto Catalyst y con SwiftUI cada vez tenemos más fácil los desarrolladores escribir una sola aplicación compatible con todas las plataformas de la manzana.

 

3. Xcode 12

Las novedades de Xcode tampoco han pasado desapercibidas, el nuevo IDE de Apple ahora contiene las siguientes características:

  • Fuentes personalizables en el navegador.
  • Mejoras en el auto completado.
  • Pestañas para documentos.
  • Creación de apps universales, con plantillas multiplataforma.
  • Nuevo Organizer.
  • Mejoras para testar StoreKit en local.
  • Mejoras en la auto-indentación.
  • Previsualización de widgets, App clips y contenido de paquetes (SPM).
  • Soporte a formato SVG en Assets catalogs.
  • Playgrounds ahora soporta Swift Package Manager.

 

4. Mac Catalyst

Las novedades que encontramos en Mac Catalyst son:

  • Mejoras en el escalado de la aplicación.
  • Nuevos frameworks disponibles para la migración(HomeKit y AVCapture).
  • Mejoras en la API de teclado.
  • Adaptación a la apariencia de MacOS Big Sur automática.

 

5. Novedades en SwiftUI

Apple está apostando muy fuerte con esta tecnología, hemos que tener claro que éste es el futuro para ellos y todos los esfuerzos se están enfocando a ello. Los nuevos frameworks de WidgetKit y Appclip están escritos enteramente en SwiftUI para SwiftUI.

Hay muchas, muchísimas novedades en SwiftUI que nos ha traído la WWDC2020, vamos a ver las más destacas:

  • Ahora podemos acceder a la vista vista de debug desde las preview sin necesidad de compilar el código.
  • Las apps escritas en SwiftUI ocupan mucho menos espacio.
  • Podemos despedirnos de los storyboards por completo en una aplicación 100% SwiftUI modificando el nuevo parámetro del plist Launch Screen.

5.1 Nuevos protocolos, estructuras y property wrappers:

  • App: Hasta ahora para iniciar nuestra app construida exclusivamente mediante SwiftUI teníamos que usar UIHostingViewController, ahora ya no es necesario gracias al protocolo App y la anotación @main:

@main
struct Mail: App {
    var body: some Scene {
        WindowGroup {
            MailViewer() // Declare a view hierarchy here.
        }
    }
}

  • Scene: El protocolo Scene es el que deben adoptar las vistas que construyan nuestra escena, que son el análogo de UIScene en UIKit, en el ejemplo del protocolo App, WindowGroup es un tipo que adopta este protocolo para vistas normales, también tenemos DocumentGroup para las escenas de apps basadas en documentos o Settings para dirigir a la pantalla de ajustes de nuestra app. También podemos crear nuestras propias implementaciones de este protocolo:

struct MyScene: Scene {
    @Environment(\.scenePhase) private var phase

    var body: some Scene {
        WindowGroup {
            TabView {
                FirstView()
                SecondView()
            }
        }
        .onChange(of: phase) { newPhase in
            switch newPhase {
            case .active:
                // App became active
            case .inactive:
                // App became inactive
            case .background:
                // App is running in the background
            @unknown default:
                // Fallback for future cases
            }
        }
    }
}

  • @SceneStorage: Ahora se pueden almacenar los datos específicos de una escena usando SceneStorage. Lo podemos usar por ejemplo para mantener la pestaña seleccionada de un tabView entre sesiones:

struct ContentView: View {
    
    @SceneStorage("selectedTab") var selectedTab: Tab = .first
    
    var body: some View {
        TabView(selection: $selectedTab) {
            FirstView()
                .tabItem {
                    Image(systemName: "nose.fill")
                }.tag(Tab.first)
            SecondView()
                .tabItem {
                    Image(systemName: "mustache.fill")
                }.tag(Tab.second)
        }
    }
    
    enum Tab: String {
        case first
        case second
    }
}

  • @StateObject: Un tipo de contenedor de propiedades que crea instancias de un objeto observable. Es como un @ObservableObject pero que se mantendrá vivo durante las actualizaciones de la vista que lo contiene:

@StateObject var dataSource = DataSource()

  • @AppStorage: Un property wrapper que encapsula el UserDefaults e invalida la vista que lo usa en función a los cambios de éste:

struct ContentView: View {
    @AppStorage("termsAndConditions") var termsAndConditions: Bool = false

    var body: some View {
        VStack {
            Toggle(isOn: termsAndConditions) {
                self.termsAndConditions.toggle()
            }
        }
    }
}

  • @FocusedBinding: Se trata de un property wrapper que nos soluciona el problema de tener varios publicadores que usan la misma key, éste identificará el más cercano al foco:

@propertyWrapper struct FocusedBinding<Value>

  • @ScaledMetric: Nos sirve para calcular números con la escala proporcionada por el escalado dinámico del sistema que configura el usuario, es decir, en función al zoom que selecciona un usuario en la configuración de su dispositivo el valor de esta variable cambiará, nosotros solo hemos de poner un valor por defecto:

@ScaledMetric var size: CGFloat = 50

  • @UIApplicationDelegateAdaptor: Este property wrapper nos va a facilitar el acceso al AppDelegate de nuestra aplicación en caso de que lo necesitemos, si creamos una app 100% swiftUI hemos de crear previamente la clase:

@main
struct MyApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

class AppDelegate: NSObject, UIApplicationDelegate {
    // app delegate methods...
}

 

5.2 Nuevos componentes:

  • Toolbar y ToolbarItem: Hay un nuevo modificador toolbar() en la vista NavigationView que nos sirve para poner una barra de herramientas:

NavigationView {
    Text("Hello world").padding()
        .navigationTitle("Title")
        .toolbar {
            ToolbarItem(placement: .bottomBar) {
                HStack {
                   Button("First") {
                       print("Pressed")
                   }
                   Button("Second") {
                       print("Pressed")
                   }
                }
            }
        }
}

  • TextEditor: Es la nueva vista de SwiftUI análoga a UITextView de UIKit, es decir, un componente para poder escribir en varias lineas de texto. Es altamente configurable como el componente Text:

struct ContentView: View {
    @State private var description: String = ""

    var body: some View {
        TextEditor(text: $description)
    }
}

  • Menu: Se trata de una vista que tendrá la apariencia del menú de la plataforma en la que lo ejecutemos y contiene un modificador para darle distintos estilos:

Menu("Actions") {
    Button("Duplicate", action: duplicate)
    Button("Rename", action: rename)
    Button("Delete…", action: delete)
    Menu("Copy") {
        Button("Copy", action: copy)
        Button("Copy Formatted", action: copyFormatted)
        Button("Copy Library Path", action: copyPath)
    }
}

  • SignInWithAppleButton: Es la vista que se encarga de generar el botón de login con Apple, así como de recibir los eventos en ella:

SignInWithAppleButton(
    .signIn,
    onRequest: { request in
        request.requestedScopes = [.fullName, .email]
    },
    onCompletion: { result in
        switch result {
        case .success (let authResults):
            print("Authorization successful.")
        case .failure (let error):
            print("Authorization failed: " + error.localizedDescription)
        }
    }
)

  • ColorPicker: Es un componente para seleccionar colores, se debe almacenar en una variable @State o similar.

struct ContentView: View {
    @State private var color = Color.white

    var body: some View {
        VStack {
            ColorPicker("Set color", selection: $color)
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(color)
    }
}

  • ProgressView: Una vista para mostrar el progreso de un valor sobre un total, es como el Activity Indicator de UIKit pero vitaminado ya que es altamente configurable, podemos mostrar un progreso lineal o circular:

struct ShadowedProgressViews: View {
    var body: some View {
        VStack {
            ProgressView(value: 0.25)
            ProgressView(value: 0.75)
        }
        .progressViewStyle(DarkBlueShadowProgressViewStyle())
        VStack {
            ProgressView ("Text", value: 10, total: 100)
        }
    }
}

struct DarkBlueShadowProgressViewStyle: ProgressViewStyle {
    func makeBody(configuration: Configuration) -> some View {
        ProgressView(configuration)
            .shadow(color: Color(red: 0, green: 0, blue: 0.6),
                    radius: 4.0, x: 1.0, y: 2.0)
    }
}

  • Gauge: Una vista que muestra un valor dentro de un rango:

struct Gauge<Label, CurrentValueLabel, BoundsLabel, MarkedValueLabels> where Label : View, CurrentValueLabel : View, BoundsLabel : View, MarkedValueLabels : View

  • Label: Una vista para mostrar una imagen y una etiqueta:

Label("mustache", systemImage: "mustache.fill")

//

Label {
    Text("Text")
        .foregroundColor(.primary)
        .font(.largeTitle)
        .padding()
        .background(Color.black)
} icon: {
    RoundedRectangle(cornerRadius: 10)
        .fill(Color.red)
        .frame(width: 54, height: 54)
}

  • Link: Una vista con la apariencia de un botón que abre una URL, permite personalización como la fuente o los colores y la posibilidad de crearla manualmente con su propio @ViewBuilder:

Link("adictos", destination: URL(string: "https://www.adictosaltrabajo.com/")!)

  • ScrollViewReader: Una vista que nos permite desplazarnos a una determinada posición de un ScrollView:

struct ContentView: View {

    var body: some View {
        ScrollView {
            ScrollViewReader { value in
                Button("Jump to 4") {
                    value.scrollTo(8)
                }

                ForEach(0..<10) { i in
                    Text("Example \(i)")
                        .id(i)
                }
            }
        }
    }
}

  • LazyViews y GridItem: Es el análogo a los collection views de iOS, ahora disponemos de una forma de cargar perezosamente nuestras listas, tanto verticales como horizontales, la diferencia con las stack views es que éstas cargan todo el contenido antes de presentarse, ahora las gridViews lo cargan bajo demanda en función a los items que se están mostrando.

var rows: [GridItem] =
        Array(repeating: .init(.fixed(20)), count: 2)
ScrollView(.horizontal) {
    LazyHGrid(rows: rows, alignment: .top) {
        ForEach((0...79), id: \.self) {
            let codepoint = $0 + 0x1f600
            let codepointString = String(format: "%02X", codepoint)
            Text("\(codepointString)")
                .font(.footnote)
            let emoji = String(Character(UnicodeScalar(codepoint)!))
            Text("\(emoji)")
                .font(.largeTitle)
        }
    }
}

  • DisclosureGroup: Una vista desplegable que muestra u oculta otra vista de contenido, basada en el estado de un control de tipo Bool.

struct ToggleStates {
    var oneIsOn: Bool = false
    var twoIsOn: Bool = true
}
@State private var toggleStates = ToggleStates()
@State private var topExpanded: Bool = true

var body: some View {
    DisclosureGroup("Items", isExpanded: $topExpanded) {
        Toggle("Toggle 1", isOn: $toggleStates.oneIsOn)
        Toggle("Toggle 2", isOn: $toggleStates.twoIsOn)
        DisclosureGroup("Sub-items") {
            Text("Sub-item 1")
        }
    }
}

struct ContentView: View {
    @State private var region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 40,6146, longitude: -3,7211), span: MKCoordinateSpan(latitudeDelta: 0.5, longitudeDelta: 0.5))

    var body: some View {
        Map(coordinateRegion: $region)
    }
}

  • SceneViewSpriteView: Ahora podemos representar escenas de SceneKit o vistas de SpriteKit directamente en SwiftUI:

SpriteView(scene: scene)
    .frame(width: 300, height: 400)

  • VideoPlayer: Vista de reproducción de video de AVKit para SwiftUI, puede visualizar tanto local como remoto:

VideoPlayer(player: AVPlayer(url:  Bundle.main.url(forResource: "video", withExtension: "mp4")!))
//
VideoPlayer(player: AVPlayer(url:  URL(string: "https://adictos.com/video")!))
//
VideoPlayer(player: AVPlayer(url:  URL(string: "https://adictos.com/video")!)) {
    VStack {
        Spacer()
        Text("descripción del video")
            .font(.caption)
            .foregroundColor(.white)
            .background(Color.black)
            .clipShape(Capsule())
    }
}

5.3 Cambios destacados en vistas existentes:

  • List: ahora en iOS 14 se comporta de forma «lazy» al igual que los grids.
  • Text: Nuevos constructores para iniciar la vista con una Imagen o fechas (sin DateFormatter ✌) y modificadores para cambiar a mayúsculas o minúsculas.
  • Group: Muchos modificadores nuevos, por ejemplo, ahora se le puede asignar un navigationTitle para propósitos de navegación.
  • TabView: Ahora retiene el estado de navegación de una pestaña mientras cambias entre ellas.

6. Novedades en UIKit

En Apple no se han olvidado de la veterana UIKit, y lo han demostrado dándola una serie de mejoras que merecen mucho la pena, veamos algunas de ellas:

  • UIDatePicker: El selector de fecha recibe un lavado de cara con un estilo mucho más cómodo mostrando un calendario completo, además permite muchas formas de personalización:

private let picker = UIDatePicker(frame: .zero)

  • UIColorPickerViewController: En nuevo selector de color como el de SwiftUI, pero éste funciona mediante delegación o mediante publicadores con Combine:

let colorPicker = UIColorPickerViewController()

let cancellable = colorPicker.publisher(for: \.selectedColor)
.sink() { [weak self] color in
    self?.view.backgroundColor = color
}

  • UIAction: Se ha potenciado el uso de esta clase, incluyéndola en la mayoría de constructores de los componentes de UIKit

let dismiss = UIAction(title: "done") { [weak self] action in
    self?.dismiss(animated: true)
}
let navItem = UIBarButtonItem(systemItem: .done, primaryAction: dismiss)

  • UIMenu: Ahora los botones (UIButton, UIBarButton…)  aceptan menús, esto es muy practico por ejemplo para casos como poner el historial de navegación en un botón «atrás»:

let navItem = UIBarButtonItem(systemItem: .done)
let menu = UIMenu(title: "", children: [UIAction(title: "done") { handler in print("done")}])

navItem.menu = menu

  • UIListContentView: Se trata de una vista que está pensada para ser insertada en un contenedor tipo UIStackView o UITableView, seguramente sea la nueva forma en la que Apple nos invita a crear «celdas»:

var config:UIListContentConfiguration = UIListContentConfiguration.subtitleCell()
config.text = "Title"
config.secondaryText = "Subtitle"
        
let list:UIListContentView = UIListContentView(configuration: config)
list.frame = view.bounds
        
let stackView = UIStackView(frame: view.bounds)
stackView.addArrangedSubview(list)

let splitVC = UISplitViewcontroller()

let masterVC = MasterViewController()
let suppVC = SupplementaryViewController()
let detailVC = DetailViewController()

splitVC.setViewController(masterVC, for: .primary)
splitVC.setViewController(suppVC, for: .supplementary)
splitVC.setViewController(detailVC, for: .secondary)

  • UIScribbleInteraction: La novedad más destacada en iPad es la posibilidad de escribir a mano en los campos texto, esta característica la vamos a tener de forma automática, no obstante disponemos de la clase UIScribbleInteraction para poder jugar con la interacción en sí:

let scribble = UIScribbleInteraction(delegate: self)
let text = UITextField(frame: .zero)
text.addInteraction(scribble)

func scribbleInteraction(_ interaction: UIScribbleInteraction, shouldBeginAt location: CGPoint) -> Bool {
    return true
}

 

7. Conclusiones

Una WWDC cargada de contenido muy potente, enfocada al desarrollo (como debe ser), y sin hardware. Con todo lo que han presentado tenemos horas y horas para indagar en la documentación nueva y seguir aprendiendo. En el siguiente artículo veremos el nuevo framework de Widgets, las Apps Clips, las novedades en SFSymbols, y muchos más 😜.

La entrada WWDC 2020, novedades en el mundo del desarrollo Apple. (1ª parte) se publicó primero en Adictos al trabajo.

JMeter Thread Groups y su configuración – Guía rápida

$
0
0

Introducción

En este artículo, veremos la configuración del Thread Group en JMeter y cuál es el propósito de cada opción que podemos usar. Entenderemos qué es un Thread Group y los diferentes tipos.

2. Qué es un Thread Group

Un Thread Group es el punto de partida de cualquier plan de prueba de Jmeter. Es la parte más alta del árbol y todos los elementos de un plan de prueba deben definirse debajo de él. Además de toda la lógica y las muestras de la prueba, un Thread Group también almacena la configuración requerida para la ejecución del script Jmeter.

Hay varios tipos de Thread Group:

  • setUp – Se utiliza para realizar las acciones necesarias antes de que comience la ejecución del Thread Group normal. El comportamiento de los hilos mencionados en Configurar grupo de hilos es exactamente el mismo que el grupo de hilos normal. Puede ser útil, por ejemplo, hacer la parte de inicio de sesión de la prueba, obtener / extraer y establecer request headers, ejecutar scripts de base de datos predefinidos, etc.
  • normal – Utilizado para ejecutar nuestro test con toda su lógica. Va entre los setUp y tearDown thread groups, y puede tener N números Thread Groups normales.
  • tearDown – Se utiliza para realizar las acciones necesarias después de la ejecución del Thread Group normal. Puede ser útil para ejecutar acciones como borrar la base de datos, hacer solicitudes contra ciertos puntos finales para deshacer cambios, etc.

3. Action to be performed after a Sampler hits an error

Esta opción le dice a Jmeter qué hacer si la ejecución encuentra un error porque cuando una muestra falla por algún motivo, o falla alguna afirmación Jmeter le ofrece 5 opciones para manejar la falla de un muestreador, y son «Continuar», «Iniciar siguiente secuencia de thread group», «Detener thread», «Detener prueba» y «Detener prueba ahora».

Por defecto, la opción «Continuar» está seleccionada. Ahora veamos el significado de estas opciones.

1. Continuar: Jmeter ignorará el error, continuará la ejecución y solo el muestreador afectado fallará en el oyente. Normalmente el error sampler se queda en rojo en los informes.

2. Iniciar siguiente Thread Loop: Jmeter ignorará el error y continuará con la ejecución del siguiente thread loop.

3. Detener thread: la ejecución del thread actual se detendrá, los threads restantes se ejecutarán según lo definido.

4. Detener prueba: la ejecución completa de la prueba se detendrá si algún muestreador encuentra un error y procede a crear informes (si están configurados).

5. Detener prueba ahora: la ejecución de la prueba se detendrá abruptamente y todos los muestreadores actualmente activos se interrumpirán para finalizar la ejecución.

4. Thread count

Thread Count define el número de usuarios que desea simular para la ejecución. En Jmeter, observamos cada thread producido como un usuario virtual que abre la conexión a nuestro back-end y comienza a ejecutar solicitudes.

5. Ramp-up period

La aceleración es la cantidad de tiempo que Jmeter debería tomar para obtener todos los hilos enviados para la ejecución. La aceleración debería ser suficiente para evitar una carga de trabajo innecesaria y grande desde el comienzo de la ejecución de la prueba.

Por ejemplo, si el número de threads es 10 y el ramp-up period es de 100 segundos, Jmeter tardará 100 segundos en poner en funcionamiento los 10 threads. El primer thread se enviará el segundo 0 y luego cada thread se nace después de 10 segundos (100/10).

6.Loop count

Usando Loop Count puede especificar el número de veces que ejecutará la prueba dentro del thread group específico. Puede seleccionar la casilla de verificación «forever», seguirá ejecutando el mismo script de prueba en bucle. La única forma de detenerlo es manualmente.

El Loop count se usa con frecuencia con Ramp-up time y el Thread count. Veamos algunos ejemplos:

Escenario 1: : Thread Count = 25, Ramp Up Time = 100 seconds & Loop Count = 1
Cada 4 segundos (100/25) un Thread llegará al servidor. La ejecución comenzará con una solicitud a la vez. La lógica dentro del hilo se ejecutará a tiempo.

Escenario 2 : Thread Count = 25, Ramp Up Time = 100 seconds & Loop Count = 5
En este caso, como podemos ver, la única diferencia es el Loop Count. Entonces, Cada 4 segundos (100/25) 5 Threads llegarán al servidor. Una vez que el primer thread completa la primera ronda de ejecución, comenzará el segundo bucle ejecutando la misma solicitud HTTP. La ejecución dura hasta que los 25 threads ejecuten todas las solicitudes HTTP 5 veces.

7. Scheduler Configuration

Thread Group ofrece la opción de ejecutar su script durante un período de tiempo específico. Una schedule checkbox se encuentra en la parte inferior de la pantalla de una Thread Group, lo que habilitará algunas opciones más. Se puede ver la duración de la ejecución de la prueba, el retraso de inicio, la hora de inicio y la hora de finalización.

Entonces, nos permite configurar una cantidad de tiempo específica como parte de la duración. Jmeter ejecutará el Thread Group durante el tiempo que ha mencionado en la Duración. Después del inicio de la ejecución, Jmeter espera la cantidad exacta de tiempo que ha mencionado en el Startup Delay. Estas 2 opciones anulan la hora de inicio (start time) y la hora de finalización (end time).

8. Delay Thread Creation Until Needed

Antes de la versión 2.8 de JMeter, cuando iba a aumentar 1000 usuarios durante las pruebas de rendimiento, JMeter asignó memoria para todos los threads de inmediato, incluso si tenía un tiempo de aceleración para los usuarios. Esto significaba que incluso si tiene una configuración para incluir a un usuario en la prueba después de 2 horas desde la hora de inicio, todos los recursos necesarios ya estaban asignados para este usuario cuando comenzó el script.

En conclusión, teniendo en cuenta que hay algunos escenarios de prueba con miles y miles de usuarios con una configuración de aceleración, siempre es una buena idea pensar en activar esta opción para facilitar su máquina y asignar menos RAM en un momento específico.

La entrada JMeter Thread Groups y su configuración – Guía rápida se publicó primero en Adictos al trabajo.

Interfaces gráficas en Python con Tkinter

$
0
0

Índice de contenidos

1. Introducción
2. Entorno
3. Widgets
4. Configuración
5. Gestión de la composición
6. Ejecución
7. Ejemplo práctico

1. Introducción

Tkinter es el paquete más utilizado para crear interfaces gráficas en Python. Es una capa orientada a objetos basada en Tcl (sencillo y versátil lenguaje de programación open-source) y Tk (la herramienta GUI estándar para Tcl).

2. Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro 15′ (2.2 Ghz Intel Core i7, 16GB DDR3).
  • Sistema Operativo: Mac OS Mojave 10.14
  • Aplicación de desarrollo: Sublime Text 3.2.2

3. Widgets

A la hora de montar una vista con Tkinter, nos basaremos en widgets jerarquizados, que irán componiendo poco a poco nuestra interfaz. Algunos de los más comunes son:

  • Tk: es la raíz de la interfaz, donde vamos a colocar el resto de widgets.
  • Frame: marco que permite agrupar diferentes widgets.
  • Label: etiqueta estática que permite mostrar texto o imagen.

  • Entry: etiqueta que permite introducir texto corto (típico de formularios).

  • Text: campo que permite introducir texto largo (típico para añadir comentarios).

  • Button: ejecuta una función al ser pulsado.

  • Radiobutton: permite elegir una opción entre varias.

  • Checkbutton: permite elegir varias de las opciones propuestas.

  • Menu: clásico menú superior con opciones (Archivo, Editar…).

  • Dialogs: ventana emergente (o pop-up).

Cuando vayamos a inicializar el componente, debemos pasar por constructor el elemento que quede “por encima” en la jerarquía de la vista (si queremos colocar una label dentro de un frame, al construir la etiqueta le pasaremos el marco como argumento del constructor).

4. Configuración

Para configurar un widget, simplemente llamamos a .config() y pasamos los argumentos que queramos modificar. Algunas opciones son:

  • bg: modifica el color de fondo. Se puede indicar con el color en inglés (incluyendo modificadores, como “darkgreen”) o su código RGB en hexadecimal (“#aaaaaa” para blanco). Ojo: en MacOS no se puede modificar el color de fondo de los botones; aunque indiquemos un nuevo color, se mostrará en blanco. Lo más parecido que podemos hacer es configurar el highlightbackground, que pintará el fondo alrededor del botón del color que indiquemos.
  • fg: cambia el color del texto.
  • cursor: modifica la forma del cursor. Algunos de los más utilizados son “gumby”, “pencil”, “watch” o “cross”.
  • height: altura en líneas del componente.
  • width: anchura en caracteres del componente.
  • font: nos permite especificar, en una tupla con nombre de la fuente, tamaño y estilo, la fuente a utilizar en el texto del componente. Por ejemplo, Font(“Times New Roman”, 24, “bold underline”).
  • bd: modificamos la anchura del borde del widget.
  • relief: cambiamos el estilo del borde del componente. Su valor puede ser “flat”, “sunken”, “raised”, “groove”, “solid” o “ridge”.
  • state: permite deshabilitar el componente (state=DISABLED); por ejemplo, una Label en la que no se puede escribir o un Button que no se puede clickar.
  • padding: espacio en blanco alrededor del widget en cuestión.
  • command: de cara a que los botones hagan cosas, podemos indicar qué función ejecutar cuando se haga click en el mismo.

5. Gestión de la composición

Es MUY IMPORTANTE que, cuando tengamos configurado el componente, utilicemos un gestor de geometría de componentes. Si no, el widget quedará creado pero no se mostrará.

Los tres más conocidos son:

  • Pack: cuando añadimos un nuevo componente, se “hace hueco” a continuación de los que ya están incluidos (podemos indicar que se inserte en cualquiera de las 4 direcciones), para finalmente calcular el tamaño que necesita el widget padre para contenerlos a todos.
  • Place: este es el más sencillo de entender, pero puede que no el más sencillo de utilizar para todo el mundo. Al insertar un componente, podemos indicar explícitamente la posición (coordenadas X e Y) dentro del widget padre, ya sea en términos absolutos o relativos.
  • Grid: la disposición de los elementos es una matriz, de manera que para cada uno debemos indicar la celda (fila y columna) que queremos que ocupe. Podemos además especificar que ocupe más de una fila y/o columna (rowspan/columnspan=3), “pegarlo” a cualquiera de los 4 bordes de la celda en vez de centrarlo (sticky=W para el borde izquierdo, por ejemplo)…

Personalmente, me siento mucho más cómodo utilizando un Grid: de esta manera te aseguras la distribución de los componentes, y la hora de añadir nuevos es muy visual poder pensar en ello como matriz, en vez de coordenadas o posiciones relativas a otros widgets.

6. Ejecución

Una vez tenemos todos los componentes creados, configurados y añadidos en la estructura, debemos terminar el script con la instrucción tk.mainloop() (“tk” = variable Tk). Así, cuando lo ejecutemos, se abrirá la ventana principal de nuestra GUI.

Bricotruco: si guardamos nuestro script con formato .pyw en vez de .py, al ejecutarlo se abrirá nuestra interfaz, sin tener que pasar por terminal o abrir algún IDE para ello.

7. Ejemplo práctico

Finalmente, os dejo por aquí el código de una sencilla calculadora, que hace uso de varios componentes y configuraciones. Espero que os haya gustado Tkinter y que lo explotéis al máximo en vuestras aplicaciones pythoneras!

from tkinter import *
from tkinter import messagebox


class Pycalc(Frame):

    def __init__(self, master, *args, **kwargs):
        Frame.__init__(self, master, *args, **kwargs)
        self.parent = master
        self.grid()
        self.createWidgets()

    def deleteLastCharacter(self):
        textLength = len(self.display.get())

        if textLength >= 1:
            self.display.delete(textLength - 1, END)
        if textLength == 1:
            self.replaceText("0")

    def replaceText(self, text):
        self.display.delete(0, END)
        self.display.insert(0, text)

    def append(self, text):
        actualText = self.display.get()
        textLength = len(actualText)
        if actualText == "0":
            self.replaceText(text)
        else:
            self.display.insert(textLength, text)

    def evaluate(self):
        try:
            self.replaceText(eval(self.display.get()))
        except (SyntaxError, AttributeError):
            messagebox.showerror("Error", "Syntax Error")
            self.replaceText("0")
        except ZeroDivisionError:
            messagebox.showerror("Error", "Cannot Divide by 0")
            self.replaceText("0")

    def containsSigns(self):
        operatorList = ["*", "/", "+", "-"]
        display = self.display.get()
        for c in display:
            if c in operatorList:
                 return True
        return False

    def changeSign(self):
        if self.containsSigns():
            self.evaluate()
        firstChar = self.display.get()[0]
        if firstChar == "0":
            pass
        elif firstChar == "-":
            self.display.delete(0)
        else:
            self.display.insert(0, "-")

    def inverse(self):
        self.display.insert(0, "1/(")
        self.append(")")
        self.evaluate()

    def createWidgets(self):
        self.display = Entry(self, font=("Arial", 24), relief=RAISED, justify=RIGHT, bg='darkblue', fg='red', borderwidth=0)
        self.display.insert(0, "0")
        self.display.grid(row=0, column=0, columnspan=4, sticky="nsew")

        self.ceButton = Button(self, font=("Arial", 12), fg='red', text="CE", highlightbackground='red', command=lambda: self.replaceText("0"))
        self.ceButton.grid(row=1, column=0, sticky="nsew")
        self.inverseButton = Button(self, font=("Arial", 12), fg='red', text="1/x", highlightbackground='lightgrey', command=lambda: self.inverse())
        self.inverseButton.grid(row=1, column=2, sticky="nsew")
        self.delButton = Button(self, font=("Arial", 12), fg='#e8e8e8', text="Del", highlightbackground='red', command=lambda: self.deleteLastCharacter())
        self.delButton.grid(row=1, column=1, sticky="nsew")
        self.divButton = Button(self, font=("Arial", 12), fg='red', text="/", highlightbackground='lightgrey', command=lambda: self.append("/"))
        self.divButton.grid(row=1, column=3, sticky="nsew")

        self.sevenButton = Button(self, font=("Arial", 12), fg='white', text="7", highlightbackground='black', command=lambda: self.append("7"))
        self.sevenButton.grid(row=2, column=0, sticky="nsew")
        self.eightButton = Button(self, font=("Arial", 12), fg='white', text="8", highlightbackground='black', command=lambda: self.append("8"))
        self.eightButton.grid(row=2, column=1, sticky="nsew")
        self.nineButton = Button(self, font=("Arial", 12), fg='white', text="9", highlightbackground='black', command=lambda: self.append("9"))
        self.nineButton.grid(row=2, column=2, sticky="nsew")
        self.multButton = Button(self, font=("Arial", 12), fg='red', text="*", highlightbackground='lightgrey', command=lambda: self.append("*"))
        self.multButton.grid(row=2, column=3, sticky="nsew")

        self.fourButton = Button(self, font=("Arial", 12), fg='white', text="4", highlightbackground='black', command=lambda: self.append("4"))
        self.fourButton.grid(row=3, column=0, sticky="nsew")
        self.fiveButton = Button(self, font=("Arial", 12), fg='white', text="5", highlightbackground='black', command=lambda: self.append("5"))
        self.fiveButton.grid(row=3, column=1, sticky="nsew")
        self.sixButton = Button(self, font=("Arial", 12), fg='white', text="6", highlightbackground='black', command=lambda: self.append("6"))
        self.sixButton.grid(row=3, column=2, sticky="nsew")
        self.minusButton = Button(self, font=("Arial", 12), fg='red', text="-", highlightbackground='lightgrey', command=lambda: self.append("-"))
        self.minusButton.grid(row=3, column=3, sticky="nsew")

        self.oneButton = Button(self, font=("Arial", 12), fg='white', text="1", highlightbackground='black', command=lambda: self.append("1"))
        self.oneButton.grid(row=4, column=0, sticky="nsew")
        self.twoButton = Button(self, font=("Arial", 12), fg='white', text="2", highlightbackground='black', command=lambda: self.append("2"))
        self.twoButton.grid(row=4, column=1, sticky="nsew")
        self.threeButton = Button(self, font=("Arial", 12), fg='white', text="3", highlightbackground='black', command=lambda: self.append("3"))
        self.threeButton.grid(row=4, column=2, sticky="nsew")
        self.plusButton = Button(self, font=("Arial", 12), fg='red', text="+", highlightbackground='lightgrey', command=lambda: self.append("+"))
        self.plusButton.grid(row=4, column=3, sticky="nsew")

        self.negToggleButton = Button(self, font=("Arial", 12), fg='red', text="+/-", highlightbackground='lightgrey', command=lambda: self.changeSign())
        self.negToggleButton.grid(row=5, column=0, sticky="nsew")
        self.zeroButton = Button(self, font=("Arial", 12), fg='white', text="0", highlightbackground='black', command=lambda: self.append("0"))
        self.zeroButton.grid(row=5, column=1, sticky="nsew")
        self.decimalButton = Button(self, font=("Arial", 12), fg='white', text=".", highlightbackground='lightgrey', command=lambda: self.append("."))
        self.decimalButton.grid(row=5, column=2, sticky="nsew")
        self.equalsButton = Button(self, font=("Arial", 12), fg='red', text="=", highlightbackground='lightgrey', command=lambda: self.evaluate())
        self.equalsButton.grid(row=5, column=3, sticky="nsew")


Calculator = Tk()
Calculator.title("AdictoCalculator")
Calculator.resizable(False, False)
Calculator.config(cursor="pencil")
root = Pycalc(Calculator).grid()
Calculator.mainloop()

La entrada Interfaces gráficas en Python con Tkinter se publicó primero en Adictos al trabajo.


WWDC 2020, novedades en el mundo del desarrollo Apple. (Parte 2)

$
0
0

Índice de contenidos

  • 1. Introducción
  • 2. SFSymbols
  • 3. App clips
  • 4. Widgets
  • 5. Otras novedades destacadas
  • 6. Conclusiones

1. Introducción

Seguimos con la segunda parte de las principales novedades vistas en la WWDC. En la primera parte vimos como Apple está potenciando la convergencia de todos los sistemas a nivel de desarrollo, gracias a Catalyst y SwiftUI cada vez tenemos más fácil escribir una sola base de código y desplegar para múltiples plataformas (de Apple, claro). Veamos ahora algunas otras cosas interesantes que nos ha presentado Apple.

 

2. SFSymbols 2.0

En la pasada WWDC 2019 Apple nos regalo SFSymbols, una aplicación gratuita para visualizar e integrar más de 1500 iconos en nuestras apps de forma gratuita y disponibles para todas las plataformas de Apple. La forma de usar un símbolo es tan sencillo como esto:

let image = UIImage(systemName: "trash")

Con la versión 2.0 vamos a disponer de las siguientes mejoras:

  • Mas de 750 símbolos nuevos.
  • Alineación mejorada gracias a los márgenes negativos.
  • Símbolos multicolor.
  • Localización, especialmente útil en idiomas de escritura de derecha a izquierda.

 

3. App Clips

Con App clips podemos crear un versión «reducida» de nuestra app que se ejecutará con un disparador como un código QR o una etiqueta NFC. Están pensados para crear una experiencia limitada y contenida de lo que la app principal puede ofrecer, como vender algún producto o mostrar algún tipo de información. Es una forma perfecta de captación de nuevos usuarios ya que desde el propio clip puedes acceder directamente al App Store para descargar la app completa.

Cuando los clips son descargados se mantienen un tiempo en el dispositivo y son borrados al paso de unas horas de inactividad. Podemos crear tantos clips como queramos, basta con añadir un nuevo target en tu proyecto como hacemos hoy en día con las extensiones.

Limitaciones de los Clips

Los clips no pueden superar los 10 megas, su código ha de ser 100% nativo (Nada de usar tecnologías híbridas) y tienen  que ser desarrollados mediante SwiftUI ya que el propio framework ha sido creado con esa tecnología.

Los clips no pueden acceder a datos de salud y si el usuario descarga la aplicación asociada al clip, lo permisos de éste son migrados a la aplicación principal (como el permiso de acceso a la cámara o de localización).

 

4. Widgets

Con WidgetKit vamos a poder crear los nuevos widgets, que son una versión renovada de los que ya teníamos antes. No obstante las cosas han cambiado, ya que estos nuevos widgets están pensados para posicionarlos dentro del propio screenboard y su desarrollo y funcionalidades recuerdan mas a las complicaciones del Apple Watch que los antiguos widgets.

WidgetKit es el framework que vamos a usar para crearlos, está creado enteramente en SwiftUI por lo que los widget hemos de desarrollarlos mediante esta tecnología. Disponemos de varios tamaños con lo que crear el widget y podremos mostrar desde información estática relativa a nuestra app hasta el resultado de un Intent de Siri.

El propio Widget puede ser configurable para que el usuario pueda elegir qué contenido mostrar, por ejemplo el widget del tiempo puedes configurarlo para mostrar la localización concreta.

Al tener la posibilidad de ponerlos en el home screen, Apple ha proporcionado una API para que no se estén ejecutando todo el tiempo, en lugar de eso, ante la previsión de que el contenido que vayamos a mostrar cambie a lo largo del tiempo debemos configurar un proveedor de eventos con previsión de futuro, por ejemplo, el widget del calendario no consulta continuamente su store, sino que tiene una linea de tiempo en la que el propio Widget sabe que mostrar en función al momento en que se visualiza, como decía antes, es lo mismo que las complicaciones del Apple Watch. Si la naturaleza de nuestro widget no permite configurar una programación de eventos, siempre podemos hacer un único evento en nuestra linea de tiempo de forma que el sistema no pueda predecir qué se ha de mostrar, forzando una actualización del contenido.

Si configuramos una linea de tiempo en nuestro widget tenemos la posibilidad de establecer prioridades en función al momento del día, esto lo utilizará el sistema para saber qué widget es más relevante.

5. Otras novedades destacadas

Visto lo más relevante, vamos a pasar a ver las novedades que pasaron más desapercibidas pero que a los desarrolladores nos interesan especialmente:

  • Nueva API para registro de trazas con el struct Logger.
  • La app Buscar ahora tiene soporte para productos de terceros mediante el nuevo programa de Apple «Find my network«.
  • Los CollectionViews reciben una actualización en esta versión para acabar de matar a los UITableView que  ahora podemos configurar un layout de tipo lista. Además, disponemos de funciones de registro de celdas con closures para hacer la configuración de cada celda más limpia.
  • El nuevo framework NearbyInteracion hace uso del chip U1 para localizar e interactuar con dispositivos cercanos, y funciona con el simulador de Xcode!
  • En HealthKit podemos hacer consultas sobre la funcionalidad electrocardiograma mediante HKElectrocardiogramQuery.
  • En la aplicación atajos ahora puedes marcar un atajo (INIntent) como deprecado.
  • WatchOS 7 elimina el forceTouch.
  • Las complicaciones de WatchOS 7 tienen más posibilidades de personalización.
  • En MacOS tenemos una nueva guía de estilo.

En relación a la privacidad tenemos las siguientes novedades:

  • Poder compartir una ubicación aproximada en lugar de una ubicación precisa.
  • En el selector de imágenes puedes indicar permiso a ciertas imágenes en lugar de a toda la librería.
  • Nuevo indicador en la pantalla que indica si están la cámara o el micrófono activados.
  • Los usuarios ahora pueden aceptar o rechazar el seguimiento de sitios web y apps.
  • Información en la app store acerca de los datos que pide y comparte tu app.
  • En safari puedes ver y deshabilitar el rastreador de anuncios en sitios web.
  • En safari de MacOS hay un control de permisos para las extensiones que instalamos.

6. Conclusiones

Ha sido una semana intensa, a pesar de las dificultades de la pandemia, Apple ha podido liberar gran cantidad de novedades y material para tenernos todo el año entretenidos, ahora solo queda ir probando las nuevas características por nosotros mismos. ¡A por ello!

La entrada WWDC 2020, novedades en el mundo del desarrollo Apple. (Parte 2) se publicó primero en Adictos al trabajo.

Mountebank – Customizando la configuración

$
0
0

Con este doy comienzo a una serie de artículos sobre Mountebank, como herramienta que nos va a permitir generar “test doubles” para mockear las llamadas de nuestras APIs, el objetivo que se persigue con esta serie de artículos es aprender a utilizar de la forma más práctica posible la herramienta de virtualización de servicios Mountebank con todas sus características y particularidades.

 

Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro 15′ (2,3 Ghz Intel Core i7, 16GB DDR4).
  • Sistema Operativo: Mac OS Catalina 10.15.5
  • Entorno de desarrollo: JDK 11, IntelliJ, Docker, Postman

 

Introducción

La estrategia que se seguirá en los artículos será enseñar la teoría para tener una idea de cuál sería el funcionamiento, e ir añadiendo, sobre un proyecto desde cero, los conceptos aprendidos mediante ejemplos que las cumplan.

Este artículo está dividido en 5 partes:

 

¿Qué es Mountebank?

Mountebank es una herramienta de virtualización de servicios que proporciona una API REST para crear y configurar servicios virtuales, que se denominan impostores. En lugar de configurar el servicio objeto de prueba para que apunte a las URLs de servicios reales, se configura para que apunte a los impostores que creamos a través de la API de Mountebank.

Dentro del ecosistema de herramientas de virtualización de servicios WireMock es probablemente la alternativa más popular a Mountebank, pero también existen otras herramientas como Hoverfly, Moco y stubby4j entre otras.

 

Cómo funciona Mountebank

Mountebank se centra en los impostores. Un impostor define cómo debe funcionar un servicio de mock. Cada impostor representa un socket que actúa como el servicio virtual. El primer trabajo del impostor es simplificar una petición específica de protocolo en una estructura JSON para que pueda comparar la petición con un conjunto de predicados.

Cada impostor se configura con una lista de stubs (comprobantes). Un stub devuelve una respuesta establecida basada en la solicitud, un stub no es más que una colección de una o más respuestas y, opcionalmente, una lista de predicados.

Para entender qué son y cómo funcionan los impostores es necesario conocer primero cómo se comunican los servicios, para ello vamos a ver un ejemplo de una petición HTTP y su respuesta y a continuación veremos cómo Mountebank la “traduce” a JSON para trabajar con los impostores más fácilmente.

Aquí podemos ver un ejemplo sencillo de petición/solicitud:

Donde la primera línea de cualquier petición HTTP contiene tres componentes: el método, la ruta, y la versión del protocolo.

En este ejemplo, se trata de una petición de tipo GET, que denota que se quiere recuperar información en lugar de intentar cambiar el estado de algún recurso del servidor. Su ruta es /courses, y está utilizando la versión 1.1 del protocolo HTTP.

La segunda línea inicia los encabezados (headers), que son un conjunto de nuevos pares de valores clave separados por líneas.

En el ejemplo, vemos que tenemos el encabezado Host que se combina con la ruta y el protocolo para dar la URL completa como se vería en un navegador: http://api.autentia.com/courses. El encabezado Accept le indica al servidor que está esperando que devuelva JSON.

En cuanto a las respuestas, es bastante similar.

La primera línea de la respuesta contiene los metadatos, donde el código de estado/respuesta es el más importante. Las cabeceras (headers) siguen una vez más a los metadatos y en último lugar el cuerpo (body) que siempre está separado de los encabezados por una línea vacía.

Mountebank traduce los campos del protocolo de aplicación HTTP a JSON tanto para las solicitudes como para las respuestas, en el siguiente ejemplo podemos ver como se encuentran claramente distinguidas propiedades que hemos visto de la llamada HTTP como pueden ser el método, el path, el body, etc, dentro de la estructura JSON del impostor.

¿Y entonces, cómo es el proceso que realiza exactamente Mountebank cuando recibe una solicitud?

Mountebank pasa la solicitud a cada stub en orden de lista y escoge el primero que coincida con todos los predicados. Si la solicitud no coincide con ninguno de los stubs definidos, mountebank devuelve una respuesta predeterminada. De lo contrario, devuelve la primera respuesta para ese stub. Siguiendo con nuestro ejemplo, podemos ver que frente a un impostor como el siguiente Mountebank nos devolverá la primera respuesta del segundo stub.

{
...
    "stubs": [{
            "predicates": [{
                "deepEquals": {
                    "method": "POST",
                    "path": "/courses",
                    "query": {
                        "page": 1,
                        "limit": 50
                    },
                    "body": {
                        "key": "abc123"
                    }
                }
            }],
            "responses": [{
                "is": {
                    "body": {
                        "courses": [{
                                "id": "c9c87d8f41a6",
                                "name": "Ejb3 Timer Service: Scheduling",
                                "level": "INTERMEDIATE"
                            },
                            {
                                "id": "13694821e30c",
                                "name": "Envío de correo electrónico con el soporte de Jboss Seam",
                                "level": "BASIC"
                            },
                            {
                                "id": "ab495fdeb18a",
                                "name": "Registro dinámico de beans en el contexto de Spring",
                                "level": "ADVANCED"
                            }
                        ]
                    }
                }
            }]
        },
        {
            "predicates": [{
                "deepEquals": {
                    "method": "POST",
                    "path": "/courses",
                    "query": {
                        "page": 2,
                        "limit": 50
                    },
                    "body": {
                        "key": "abc123"
                    }
                }
            }],
            "responses": [{
                "is": {
                    "body": {
                        "courses": [{
                                "id": "621dab355d67",
                                "name": "Integración de Spring con el envío de emails",
                                "level": "INTERMEDIATE"
                            },
                            {
                                "id": "0a38296474c2",
                                "name": "Spring Security: haciendo uso de un servidor LDAP embebido",
                                "level": "ADVANCED"
                            },
                            {
                                "id": "877454fb00de",
                                "name": "Primeros pasos con github: subir un proyecto al repositorio.",
                                "level": "BASIC"
                            }
                        ]
                    }
                }
            }]
        },
        {
            "responses": [{
                "courses": []
            }]
        }
    ]
}

 

«Dockerizando» Mountebank

Desde mi punto de vista una de las formas más sencillas de trabajar con Mountebank dentro de nuestros servicios Java es la que os voy a comentar ya que no sería necesario realizar instalaciones manuales mediante node y arrancar manualmente nuestro servidor de mocks de Mountebank.

Hasta la versión 2.2.0, Mountebank no disponía de una imagen oficial docker por lo que lo más sencillo era crear nuestra propia imagen a partir del siguiente Dockerfile y subirlo a nuestro repositorio de imágenes:

FROM node:10.11.0-alpine
ENV MB_VERSION=1.16.0
RUN npm config set unsafe-perm true && npm install -g mountebank@${MB_VERSION} --production && mkdir -p /mountebank/servers
CMD [ -s /mountebank/servers/service.ejs ] && mb --configfile /mountebank/servers/service.ejs --loglevel debug --allowInjection
EXPOSE 2525 80

Con el siguiente comando construimos nuestra imagen de Mountebank customizada:

docker build --tag mountebank:0.0.1 .

Con el siguiente comando “taggeamos” nuestra imagen antes de subirla al repositorio:

docker tag mountebank:0.0.1 nexus.<url-repositorio>/mountebank:latest

Y por último, la subimos a nuestro repositorio:

docker push nexus.<url-repositorio>/mountebank:latest

Una vez que tenemos la imagen alojada en nuestro repositorio, vamos a crear dentro de nuestro servicio un docker-compose que configuraremos con dicha imagen y que al arrancar levante nuestro servidor de Mountebank.

version: '3.7'
networks:
 autentia:
   name: autentia
   driver: bridge
services:
 autentia-it-mountebank:
   image: mountebank_image:latest
   ports:
     - "80:80"
     - "2525:2525"
   volumes:
     - "../mountebank:/mountebank/servers"
   networks:
     - autentia

Como he comentado anteriormente desde la versión 2.2.0 es posible crear nuestro docker-compose configurando la imagen oficial de Mountebank, dando como resultado el siguiente archivo:

version: '3.7'
networks:
 autentia:
   name: autentia
   driver: bridge
services:
 autentia-it-mountebank:
   image: bbyars/mountebank:latest
   command: mb start --configfile /mountebank/servers/service.ejs --loglevel debug --allowInjection
   ports:
     - "80:80"
     - "2525:2525"
   volumes:
     - "../mountebank:/mountebank/servers"
   networks:
     - autentia

 

Probando nuestra configuración

Como ya hemos comentado anteriormente podemos crear nuestros impostores usando la API RESTful de Mountebank o a través de configuración si nos fijamos en nuestro docker-compose previo podemos observar que hemos configurado nuestro servidor de Mountebank para que lea del directorio mountebank de nuestro proyecto, por lo que bastaría con configurar en dicho archivo nuestros impostores antes de levantar el servidor.

Vamos a verlo con un ejemplo, creamos una carpeta dentro de ‘src/test/resources’ que se llame mountebank, dentro de ella vamos a crear el archivo ‘service.ejs’ con el siguiente contenido:

{
 "port": 80,
 "protocol": "http",
 "name": "origin",
 "defaultResponse": {
   "statusCode": 400
 },
 "stubs": [
   {
     "predicates": [
       {
         "deepEquals": {
           "method": "GET",
           "path": "/courses",
           "query": {
             "page": 1,
             "limit": 50
           }
         }
       }
     ],
     "responses": [
       {
         "is": {
           "statusCode" : 200,
           "headers":{
               "Content-Type":"application/json"
           },
           "body": [{
             "id": "fb68d450-3038-4bd5-9854-9cfba4dc5fb5",
             "name": "Ejb3 Timer Service: Scheduling",
             "level": "INTERMEDIATE"
           },
           {
             "id": "b2aa00b2-5fff-43c0-a9ca-172169b4fd5d",
             "name": "Envío de correo electrónico con el soporte de Jboss Seam",
             "level": "BASIC"
           },
           {
             "id": "13b88aee-1412-4290-b9a0-d6ea4ba5ca0f",
             "name": "Registro dinámico de beans en el contexto de Spring",
             "level": "ADVANCED"
           }]
         }
       }
     ]
   },
   {
     "predicates": [
       {
         "deepEquals": {
           "method": "GET",
           "path": "/courses",
           "query": {
             "page": 2,
             "limit": 50
           }
         }
       }
     ],
     "responses": [
       {
         "is": {
           "statusCode" : 200,
           "headers":{
               "Content-Type":"application/json"
           },
           "body": [{
             "id": "e646a1dc-61a8-4324-8fd4-58f84328be5d",
             "name": "Integración de Spring con el envío de emails",
             "level": "INTERMEDIATE"
           },
           {
             "id": "f8d918da-7eb7-4423-8662-b8bf1ffdb2d5",
             "name": "Spring Security: haciendo uso de un servidor LDAP embebido",
             "level": "ADVANCED"
           },
           {
             "id": "d355ef6e-1f12-4731-9ae7-64a863aca822",
             "name": "Primeros pasos con github: subir un proyecto al repositorio.",
             "level": "BASIC"
           }]
         }
       }
     ]
   },
   {
     "responses": [
       {
         "is": {
           "statusCode" : 200,
           "headers":{
               "Content-Type":"application/json"
           },
           "body": []
         }
       }
     ]
   }
 ]
}

Configurado nuestro impostor, vamos a levantar nuestro contenedor con la imagen del servidor Mountebank con el siguiente comando:

docker-compose up

Para probar nuestro impostor bastaría con ejecutar el siguiente curl, donde estamos simulando la llamada a nuestro servicio real de cursos pero invocando a nuestro servicio virtualizado de Mountebank en el puerto 80 definido anteriormente.

curl --location --request GET 'http://localhost:80/courses?page=2&limit=50'

Como podéis observar obtenemos la respuesta que configuramos en nuestro impostor:

{
    "courses": [
        {
            "id": "621dab355d67",
            "name": "Integración de Spring con el envío de emails",
            "level": "INTERMEDIATE"
        },
        {
            "id": "0a38296474c2",
            "name": "Spring Security: haciendo uso de un servidor LDAP embebido",
            "level": "ADVANCED"
        },
        {
            "id": "877454fb00de",
            "name": "Primeros pasos con github: subir un proyecto al repositorio.",
            "level": "BASIC"
        }
    ]
}

A partir de este momento cualquier llamada que hagamos al servicio de cursos desde nuestro servicio va a ser interceptada por Mountebank, el cual va a devolver la respuesta que hayamos configurado, decimos que “entrenamos” a Mountebank para que simule el servicio real.

 

Conclusiones

En este artículo hemos repasado de forma muy práctica cómo configurar nuestro servidor de Mountebank que puede ayudarnos en nuestro día a día con nuestros tests de microservicios.

Puedes descargar el proyecto completo aquí.

 

Referencias

http://www.mbtest.org/

https://github.com/bbyars/mountebank

https://github.com/bbyars/mountebank-in-action

https://hub.docker.com/r/bbyars/mountebank

La entrada Mountebank – Customizando la configuración se publicó primero en Adictos al trabajo.

Integración continua para iOS y Android con Fastlane + Gitlab CI

$
0
0
  1. Introducción
  2. Crear el contenedor docker para compilar Android
  3. Usar GitLab Runner para compilar iOS
  4. El uso de fastlane
  5. Los instrumentos de GitLab CI
  6. Conclusiones
  7. Referencias

Introducción

En este tutorial vamos a aprender cómo se puede montar el entorno de integración continua para aplicaciones móviles iOS y Android utilizando las herramientas que nos proporciona la plataforma GitLab. ¿Para qué sirve la integración continua? Si tienes alguna experiencia en la programación, tal vez hayas tenido una situación en la que al bajar algunos cambios de un repositorio algo dejó de funcionar. En un equipo de múltiples personas es bastante complicado controlar la consistencia y la calidad del código. La integración continua puede prevenir las situaciones de este tipo. Pero si eres la única persona que trabaja en un proyecto las compilaciones automáticas y la ejecución de pruebas después de cada commit pueden ser muy útiles también. Con la integración continua, puedes notar problemas más rápido y solucionarlos más pronto.

Crear docker contenedor para compilar Android

Para compilar el proyecto de Android vamos a utilizar el contenedor de Docker. Es una manera eficaz y bastante sencilla para construir y ejecutar las aplicaciones sin tener que utilizar un equipo real. Los contenedores Docker son una manera segura y consistente de crear un entorno de pruebas.

FROM openjdk:8

ENV ANDROID_SDK_URL https://dl.google.com/android/repository/sdk-tools-linux-3859397.zip
ENV ANDROID_HOME /usr/local/android-sdk-linux
ENV PATH $PATH=$ANDROID_HOME/tools:$ANDROID_HOME/tools/bin:$ANDROID_HOME/platform-tools:$ANDROID_HOME/build-tools:$PATH

WORKDIR /home

RUN mkdir "$ANDROID_HOME" .android && \
    cd "$ANDROID_HOME" && \
    curl -o sdk.zip $ANDROID_SDK_URL && \
    unzip sdk.zip && \
    rm sdk.zip && \
# Download Android SDK
yes | sdkmanager --licenses && \
sdkmanager --update && \
sdkmanager "build-tools;29.0.3" && \
sdkmanager "platforms;android-29" && \
sdkmanager "platform-tools" && \
sdkmanager "extras;android;m2repository" && \
sdkmanager "extras;google;m2repository" && \
# Install Additional Packages
apt-get update && \
apt-get --quiet install --no-install-recommends -y --allow-unauthenticated build-essential vim-common git ruby-full openssl libssl-dev
gem install fastlane && \
gem install bundler && \
# Clean up
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && \
apt-get autoremove -y && \
apt-get clean

En este fichero docker aparte de Android SDK metemos también las gemas de Ruby como Bundler y Fastlane que nos ayudarán en el proceso de la subida de nuestros binarios a las tiendas online de las aplicaciones AppStore (iOS) y PlayStore (Android).

También tenemos que hacer un par de cambios en nuestra configuración básica de Gradle para facilitar la publicación. En el fichero app/build.gradle tenemos que poner las variables que se utilizarán para firmar nuestra aplicación Android. Podemos usar un fichero que contiene todas variables o utilizar las variables de GitLab. Es más seguro el segundo.

// Try reading secrets from file
def secretsPropertiesFile = rootProject.file("secrets.properties")
def secretProperties = new Properties()

if (secretsPropertiesFile.exists()) {
    secretProperties.load(new FileInputStream(secretsPropertiesFile))
}
// Otherwise read from environment variables, this happens in CI
else {
    secretProperties.setProperty("signing_keystore_password", "${System.getenv('signing_keystore_password')}")
    secretProperties.setProperty("signing_key_password", "${System.getenv('signing_key_password')}")
    secretProperties.setProperty("signing_key_alias", "${System.getenv('signing_key_alias')}")
}

También tenemos que subir la versión de código cada vez que subamos el binario. Para ello introducimos pequeños cambios en el mismo fichero:

android {
    defaultConfig {
        applicationId "im.gitter.gitter"
        minSdkVersion 19
        targetSdkVersion 26
        versionCode Integer.valueOf(System.env.VERSION_CODE ?: 1)

Para firmar la versión release inyectamos nuestras variables que hemos introducido antes:

signingConfigs {
        release {
            // You need to specify either an absolute path or include the
            // keystore file in the same directory as the build.gradle file.
            storeFile file("../android-signing-keystore.jks")
            storePassword "${secretProperties['signing_keystore_password']}"
            keyAlias "${secretProperties['signing_key_alias']}"
            keyPassword "${secretProperties['signing_key_password']}"
        }
    }
    buildTypes {
        release {
            minifyEnabled false
            testCoverageEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            signingConfig signingConfigs.release
        }
    }
}

Usar GitLab Runner para compilar iOS

Desafortunadamente para compilar iOS aplicaciones en el entorno Gitlab hace falta un equipo físico. No se puede usar ni los contenedores Docker, ni maquinas virtuales. La única solución es instalar una aplicación GitLab Runner en en equipo físico. Se hace bastante rápido y fácilmente. Aquí puedes ver las instrucciones para todos los sistemas operativos. Después de instalar Gitlab Runner tienes que registrarlo con un token de tu proyecto. También tienes que ponerle un tag para usarlo luego en el script de Gitlab CI. De este modo, las tareas vinculadas al iOS se van a ejecutar en el equipo con el Gitlab Runner instalado.

El uso de fastlane

Para usar Gitlab CI, necesitamos una forma de construir y archivar nuestro proyecto desde terminal. Aunque podemos hacer esto sin herramientas adicionales usando el comando xcodebuild, la mejor solución será usar la herramienta Fastlane. Si no has utilizado Fastlane antes, en resumen, es una herramienta que, basada en un script simple preparado por nosotros, puede realizar automáticamente varias operaciones relacionadas con todo el proceso de desarrollo de la aplicación. Por ejemplo: crear un proyecto, ejecutar pruebas, generar informes de cobertura de código, enviar compilaciones a TestFlight, HockeyApp, etc., generar automáticamente capturas de pantalla de la aplicación, enviar notificaciones de compilación y muchas otras operaciones.

Para asegurarnos de que usamos las mismas versiones de todas las herramientas necesarias en cada ordenador, podemos usar la herramienta Bundler. Se puede decir que Bundler es algo similar a Cocoapods, pero en lugar de un Podfile tenemos un Gemfile y en lugar de administrar versiones de bibliotecas de iOS, se usa para administrar versiones de gemas Ruby, por ejemplo, CocoaPods y Fastlane. Por lo tanto, es otra herramienta que en algunos casos facilita la vida y ayuda a evitar problemas de incompatibilidad entre versiones particulares de gemas de Ruby.

El problema con las diferentes versiones de CocoaPods está relacionado con la situación en la que los Pods no se guardan en el repositorio de git. Si todo el directorio de Pods se mantiene en el repositorio de git y no tienes que descargar bibliotecas externas para compilar su proyecto, entonces el problema no se produce y quizás no necesites Bundler en este caso. Dependiendo de tus preferencias y necesidades, puedes decidir si vale la pena usar Bundler en tu caso o tal vez valga la pena mantener Pods en git. Configuremos Fastlane para que podamos construir y exportar el binario de nuestro proyecto desde la terminal.

De acuerdo con la descripción en https://docs.fastlane.tools/getting-started/ios/setup/ puedes instalar Fastlane con este comando:

# Using RubyGems
sudo gem install fastlane -NV

# Alternatively using Homebrew
brew install fastlane

El uso de «gem install fastlane -NV» puede requerir sudo si no se usa RVM en tu ordenador ni has cambiado la ruta de instalación de gemas configurando la variable de entorno GEM_HOME. Cuando finalice la instalación de Fastlane, puedes ir al directorio con tu proyecto en la terminal e inicializar Fastlane con el siguiente comando:

fastlane init

Después de eso, se creará un nuevo directorio fastlane con un archivo llamado Fastfile. Alternativamente, en lugar de fastlane init, simplemente puedes crear ese directorio y archivo manualmente. Abre ese archivo en cualquier editor de texto y cambia su contenido a lo siguiente para iOS:

platform :ios do
  desc "Build the application release version"
  lane :buildRelease do
    build_app(scheme: "App",
            workspace: "ios/App/App.xcworkspace",
            clean: true,
            include_bitcode: true,
            output_directory: "."
      )
  end
end

Y para Android:

platform :android do
  desc "Builds the debug code"
  lane :buildDebug do
    gradle(task: "assembleDebug",
          project_dir: "./android")
  end

  desc "Builds the release code"
  lane :buildRelease do |options|
    gradle(task: "assemble",
          flavor: options[:flavor],
          build_type: "Release",
          project_dir: "./android")
  end
end

Ahora puedes probar desde la terminal de tu equipo si todo está bien definido.

fastlane iOS buildRelease

fastlane android buildRelease

Fastlane también nos puede ayudar en subir los binarios a las tiendas de aplicaciones (markets). Para eso hay que utilizar en el caso de Android el comando upload_to_play_store:

desc "Submit a new Internal Build to Play Store"
lane :internal do |options|
 upload_to_play_store(track: 'internal', apk: options[:apkPath], version_name: '1.0', package_name: options[:packageName])
end

y en el caso de iOS upload_to_test_flight:

desc "Upload build to the testflight"
  lane :uploadToTestFlight do |options|
    upload_to_testflight(
      uses_non_exempt_encryption: false,
      distribute_external: true,
      changelog: (options[:changeLog] ? options[:changeLog]: "Nueva versión"))
  end

Ambos tienen muchos parámetros pero los principales son la ubicación del binario (en el caso de iOS lo puedes definir en el fichero Gymfile), los parámetros de las pruebas internas o externas, la información para los testers etc.

Los instrumentos de Gitlab CI

Ya hemos configurado Gitlab Runner y podemos construir nuestro proyecto desde la terminal. Es hora de hacer que nuestro proyecto se construya automáticamente después de cada cambio en git. Necesitamos un archivo de configuración .gitlab-ci.yml que debemos crear en el directorio principal de nuestro repositorio. Entonces deberíamos poner en él el siguiente contenido:

.upload-iOS-app:
  stage: publish-apps
  allow_failure: true
  tags:
    - ios
  before_script:
    - export LC_ALL=en_US.UTF-8
    - export LANG=en_US.UTF-8
  when: manual
  only:
    - master

upload-iOS-dev-app:
  extends: .upload-iOS-app
  script:
    - bundle exec fastlane ios incrementBuildNumber
    - bundle exec fastlane ios buildRelease
    - bundle exec fastlane ios uploadToTestFlight changeLog:"La versión Dev"

Este job utiliza el template .upload-iOS-app con los ajustes previos de fastlane como export LC_ALL=en_US.UTF-8 y export LANG=en_US.UTF-8 y se ejecutan los comandos de fastlane para incrementar el numero de build, compilar la versión release y subir el fichero ipa (binario) a TestFlight.

Para Android el script es diferente. Utilizamos el contenedor de Docker que ya hemos preparado antes. Tenemos que preparar las variables de GitLab para que funcione nuestro job. $signing_jks_file_hex es un hexdump de nuestro almacenamiento de claves de Android. Luego lo convertimos con el comando «echo «$signing_jks_file_hex» | xxd -r -p – > ./android/key.keystore» en el fichero inicial. Subimos la versión del código de la app utilizando la variable $CI_PIPELINE_IID (el id de nuestro pipeline). Sacamos de otra variable GitLab $google_play_api_key_json la clave de Google Play Api que se utiliza para acceder al Google Play Store. Al final, borramos los ficheros con las claves.

.upload-android-app:
  image: docker.eienergia.com/cicd/android-container:0.1.0
  stage: publish-apps
  cache:
    key: ${CI_COMMIT_REF_SLUG}
    paths:
      - node_modules
    policy: pull
  allow_failure: true
  when: manual
  before_script:
    # We store this binary file in a variable as hex with this command, `xxd -p gitter-android-app.jks`
    # Then we convert the hex back to a binary file
    - echo "$signing_jks_file_hex" | xxd -r -p - > ./android/key.keystore
    # We add 1 to get this high enough above current versionCodes that are published
    - 'export VERSION_CODE=$((100 + $CI_PIPELINE_IID)) && echo $VERSION_CODE'
    - echo $google_play_api_key_json > ./fastlane/playstore_key.json
    - bundle update --all
  after_script:
    - rm ./fastlane/playstore_key.json
    - rm ./android/key.keystore
  only:
    - master

upload-android-dev-app:
  extends: .upload-android-app
  script:
    - bundle exec fastlane android buildRelease flavor:dev
    - bundle exec fastlane android internal apkPath:"./android/app/build/outputs/apk/dev/release/app-dev-release.apk" packageName:"com.app"

El job «upload-android-dev-app» lanza el comando «bundle exec fastlane android buildRelease flavor:dev» para compilar la aplicación con el flavor (un variante de compilación) «dev». Después se utiliza bundle exec fastlane android internal para subir el binario a Play Store.

Conclusiones

Hay que tener en cuenta que por defecto GitLab ofrece 2 000 minutos de integración continua gratuitos y luego si se te acabarán los minutos tienes que pagar. Aun así para los proyectos pequeños es una gran ayuda. No tienes que montar tu propio entorno de integración continua comprando y manteniendo los equipos propios. Y si ya los tienes puedes usarlos instalando GitLab Runner y gestionando todo el proceso desde la pagina web de GitLab. El equipo de GitLab promete implementar los runners compartidos con Mac OS en el futuro: GitLab issues. Su principal competidor GitHub ya tiene esta funcionalidad. Esperemos que sea pronto y podemos evitar así instalar GitLab Runner en una máquina real. Como veis montar un entorno de pruebas con GitLab CI y Fastlane es bastante sencillo y no requiere mucho tiempo.

Referencias

  1. Android Publishing with Fastlane
  2. iOS CI with fastlane

 

 

 

 

La entrada Integración continua para iOS y Android con Fastlane + Gitlab CI se publicó primero en Adictos al trabajo.

Mountebank – Jugando con los predicados

$
0
0

Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro 15′ (2,3 Ghz Intel Core i7, 16GB DDR4).
  • Sistema Operativo: Mac OS Catalina 10.15.5
  • Entorno de desarrollo: JDK 11, IntelliJ, Docker, Postman

 

Introducción

En este artículo vamos a ver el uso de predicados para enviar diferentes respuestas a diferentes solicitudes, simplificación de predicados en cuerpos de peticiones JSON y por último vamos a usar JSONPath y XPath para simplificar los predicados en los cuerpos de peticiones XML, espero que os guste y/o sirva de ayuda en vuestros tests.

Este artículo está dividido en 4 partes:

 

¿Qué son los predicados?

Los predicados permiten a los impostores tener un comportamiento mucho más rico definiendo si un stub coincide o no con una petición, esto quiere decir que predicates permite que mountebank responda de manera diferente ante diferentes solicitudes.

Cualquier campo de la petición HTTP se puede utilizar en los predicados: method, path, query y headers, además el campo predicates es un array, esto quiere decir que todos los predicados de la matriz deben coincidir para que Mountebank use ese stub, debido a que una solicitud puede coincidir con varios stubs, mountebank escogerá la primera coincidencia, siempre basado en el orden de la matriz.

Todos los predicados son insensibles a mayúsculas y minúsculas por defecto, pero Si se necesita sensibilidad a mayúsculas y minúsculas, se puede ajustar el parámetro caseSensitive a true. 

 

Tipos de predicados

La siguiente tabla proporciona la lista completa de los operadores predicados que mountebank soporta:

Operador Descripción
equals Requiere que el campo de petición sea igual al valor predicado
deepEquals Realiza igualdad de conjuntos anidados en los campos de solicitud de objetos
contains Requiere que el campo de petición contenga el valor predicado
startsWith Requiere que el campo de petición comience con el valor predicado
endsWith Requiere que el campo de petición termine con el valor predicado
Matches Requiere que el campo de petición coincida con la expresión regular proporcionada como valor predicado
exists Requiere que el campo de solicitud exista como un valor no vacío (si es verdadero) o no (si es falso).
not Invierte el subpredicador
or Requiere que se cumpla cualquiera de los subprevistos
and Requiere que todos los subpredicadores estén satisfechos
inject Requiere una función proporcionada por el usuario para devolver true

Destacar de entre los tipos de predicados de Mountebank, el predicado deepEquals ya que es el único que no solo trabaja en un campo de la petición, sino con estructuras de pares de valores clave más complejas, como por ejemplo objetos, además difiere del predicado equals en que para que el predicado deepEquals es el único que requiere una coincidencia exacta. 

Vamos a ver que quiero decir con un ejemplo, suponemos que tenemos el siguiente impostor:

{
    ...
    "stubs": [
        {
            "predicates": [
                {
                    "deepEquals": {
                        "query": {
                            "q": [
                                "BASIC",
                                "INTERMEDIATE"
                            ]
                        }
                    }
                }
            ],
            "responses": [
                {
                    "is": {
                        "body": "deepEquals matched"
                    }
                }
            ]
        },
        {
            "predicates": [
                {
                    "equals": {
                        "query": {
                            "q": [
                                "BASIC",
                                "INTERMEDIATE"
                            ]
                        }
                    }
                }
            ],
            "responses": [
                {
                    "is": {
                        "body": "equals matched"
                    }
                }
            ]
        }
    ]
}

Si enviamos una petición a /courses?q=INTERMEDIATE&q=BASIC, el cuerpo de la respuesta mostrará que el predicado deepEquals coincide porque todos los elementos del array coinciden y no hay elementos adicionales en la petición. Pero si envía una petición a /courses?q=INTERMEDIATE&q=BASIC&q=ADVANCED, el predicado de deepEquals ya no coincidirá porque la definición de predicado no espera al «ADVANCED» como elemento de la matriz. El predicado equals coincidirá, porque permite elementos adicionales en la matriz de peticiones que no están especificados en la definición del predicado.

Y el predicado matches que es uno de los predicados más versátiles de mountebank, ya que con un par de metacaracteres adicionales puede reemplazar completamente a cualquiera de los otros predicados, la siguiente tabla muestra ejemplos de metacaracteres soportados por Mountebank

Metacharacter Descripción Ejemplo
A menos que forme parte de un metacaracter como los que se describen a continuación, escapa al siguiente personaje, forzando una coincidencia literal. 4 * 2? matches “What is 4 * 2?”
^ Coincide con el principio de la cadena ^Hello matches “Hello, World!” butnot “Goodbye. Hello.”
$ Coincide con el final de la cadena World!$ matches “Hello, World!”but not “World! Hello.”
. Coincide con cualquier carácter que no sea de línea nueva …. matches “Hello” but not “Hi”
* Coincide con el carácter anterior 0 o más veces a*b matches “b” and “ab” and “aaaaaab” 
? Coincide con el carácter anterior 0 ó 1 vez a?b matches “b” and “ab” but not “aab”
+ Coincide con el carácter anterior 1 o más veces a+b matches “ab” and “aaaab” but not “b”
d Coincide con un dígito ddd matches “123” but not “12a”
D Invierte los caracteres que no coinciden con los dígitos DDD matches “ab!” but not “123”
w Concuerda con un carácter alfanumérico de «word» www matches “123” and “abc” but not “ab!”
W Invierte, emparejando pero nosímbolos no alfanuméricos WWW matches “!?.” but not “ab.”
s Coincide con un carácter de espacio en blanco (principalmente espacios, tabulaciones y líneas nuevas). Hellosworld matches “Hello world” and “Hello world”
S Invierte, haciendo coincidir cualquier carácter no espacial HelloSworld matches “Hello-world” and “Hello­­­­world”

Las expresiones regulares permiten definir patrones robustos para que coincidan con los caracteres de la solicitud.

 

JSONPath y XPath

Mountebank trata los cuerpos HTTP como JSON y haciendo uso de JSONPath y XPath para llamadas en formato XML, nos permite navegar por la estructura de objetos tantos niveles como se necesite para seleccionar los valores del documento que nos interese como podemos ver en el siguiente ejemplo, esto nos da versatilidad a la hora de trabajar con estructuras de datos grandes y/o con muchos niveles.

Los parámetros predicados jsonpath y xpath limitan el alcance en el campo de petición a la parte que coincide con el selector JSONPath o XPath.

Vamos a ver a continuación ejemplos de cada uno de ellos, en ambos casos partimos de una petición que inicializa el conjunto de cursos enviando un comando PUT a la ruta /courses, pasando una matriz de cursos, como se muestra aquí:

{
    "courses": [
        {
            "id": "e646a1dc-61a8-4324-8fd4-58f84328be5d",
            "name": "Integración de Spring con el envío de emails",
            "level": "INTERMEDIATE"
        },
        {
            "id": "d355ef6e-1f12-4731-9ae7-64a863aca822",
            "name": "Primeros pasos con github: subir un proyecto al repositorio.",
            "level": "BASIC"
        }
    ]
}

Este escenario de prueba requiere que se devuelva un 400 si el comando PUT incluye un curso «Ejb3 Timer Service: Scheduling» como último miembro del array y un 200 en caso contrario. Esto es obviamente un poco exagerado, pero me permite mostrar el poder de JSONPath. 

 

JSONPath

JSONPath es un lenguaje de consulta que simplifica la tarea de seleccionar valores de un documento JSON y sobresale con documentos grandes y complejos.

A continuación se muestra cómo sería el impostor para documentos en formato JSON.

{
    "protocol": "http",
    "port": 3000,
    "stubs": [
        {
            "predicates": [
                {
                    "equals": {                                             1
                        "method": "PUT"                                     1
                    }                                                       1
                },
                {
                    "equals": {                                             1
                        "path": "/courses"                                  1
                    }                                                       1
                },
                {
                    "jsonpath": {                                           2
                        "selector": "$.courses[(@.length­1)].name"           2
                    },                                                      2
                    "equals": {                                             3
                        "body": "Ejb3 Timer Service: Scheduling"            3
                    }                                                       3
                }
            ],
            "responses": [
                {
                    "is": {
                        "statusCode": 400                                   4
                    }
                }
            ]                                                               5
        }
    ]
}

  1. Sólo hace coincidir una petición PUT con /courses
  2. Limita el alcance del predicado a la consulta JSONPath
  3. El valor JSON seleccionado dentro del cuerpo debe ser igual a «Ejb3 Timer Service: Scheduling».
  4. Devuelve un código de estado de 400
  5. Devuelve la respuesta predeterminada 200 incorporada si el predicado no coincide

 

XPath

Aunque XML no es tan común en los servicios creados en los últimos años, sigue siendo un formato de servicio frecuente y se utiliza universalmente para los servicios SOAP, para el ejemplo en XML el cuerpo de la petición sería:

<courses>
    <course id="e646a1dc-61a8-4324-8fd4-58f84328be5d">
        <name>Integración de Spring con el envío de emails</name>
        <level>INTERMEDIATE</location>
    </course>
    <course id="d355ef6e-1f12-4731-9ae7-64a863aca822">
        <name>"Primeros pasos con github: subir un proyecto al repositorio.</name>
        <level>BASIC</location>
    </course>
</courses>

A continuación se muestra cómo sería el impostor para documentos en formato XML.

{
    "predicates": [
        {
            "equals": {                                         1
                "method": "PUT"                                 1
            }                                                   1
        },
        {
            "equals": {                                         1
                "path": "/courses"                              1
            }                                                   1
        },
        {
            "xpath": {                                          2
                "selector": "//course[last()]/name"             2
            },                                                  2
            "equals": {                                         3
                "body": "Ejb3 Timer Service: Scheduling"        3
            }                                                   3
        }
    ],
    "responses": [
        {
            "is": {                                             4
                "statusCode": 400                               4
            }                                                   4
        }
    ]                   
}

  1. Verifica que es un PUT to /courses
  2. Limita el predicado al valor dado
  3. El valor debe ser igual a «Ejb3 Timer Service: Scheduling.»
  4. Devuelve una solicitud errónea

 

Conclusiones

En este artículo hemos repasado qué son y cómo podemos utilizar los predicados de Mountebank para entrenar a nuestro servidor de tests de una forma mucho más eficiente.

Puedes descargar el proyecto completo aquí.

 

Referencias

http://www.mbtest.org/

https://github.com/bbyars/mountebank

https://github.com/bbyars/mountebank-in-action

https://hub.docker.com/r/bbyars/mountebank

https://goessner.net/articles/JsonPath/

https://www.w3.org/TR/1999/REC-xpath-19991116/

La entrada Mountebank – Jugando con los predicados se publicó primero en Adictos al trabajo.

Mountebank – Respuestas enlatadas

$
0
0

Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro 15′ (2,3 Ghz Intel Core i7, 16GB DDR4).
  • Sistema Operativo: Mac OS Catalina 10.15.5
  • Entorno de desarrollo: JDK 11, IntelliJ, Docker, Postman

 

Introducción

En este artículo vamos a ver el uso de uno de los 3 tipos de respuestas que provee Mountebank, las respuestas de tipo is o enlatadas, espero que os guste y/o sirva de ayuda en vuestros tests.

Este artículo está dividido en 7 partes:

  • ¿Qué son las respuestas?
  • Tipos de respuestas
  • Respuestas de tipo is
  • Respuesta por defecto
  • Respuestas infinitas
  • Impostores HTTPS
  • Conclusiones

 

¿Qué son las respuestas?

Como su propio nombre indica se trata de las respuestas que Mountebank va a devolver cuando la solicitud coincida con todos los predicados del array tal y como vimos en el anterior tutorial. 

Mountebank se encarga de traducir la estructura de respuesta de los datos de JSON al formato de red esperado por el sistema bajo prueba, el cual se indica en el campo protocolo del impostor. 

 

Tipos de respuestas

Mountebank tiene tres tipos de respuesta diferentes

  • Un tipo de respuesta is que devuelve el JSON proporcionado, creando una respuesta que se denomina “enlatada”
  • Un tipo de respuesta proxy que reenvía la solicitud a una dependencia real y convierte su respuesta en una estructura de respuesta JSON.
  • Un tipo de respuesta de inyección que permite definir programáticamente la respuesta JSON utilizando JavaScript. La inyección es la forma en que se puede extender mountebank cuando sus capacidades incorporadas no hacen exactamente lo que se necesita

 

Respuestas de tipo is

El tipo de respuesta is, es el componente fundamental de un stub, se trata de respuestas “estáticas” que devuelven el contenido tal cual se define en el stub

Vamos a ver un ejemplo sencillo de una respuesta enlatada, el típico Hola mundo, donde definimos un impostor con una única respuesta sin predicado, por lo que cualquier llamada que hagamos al servicio virtual siempre nos va a devolver el mismo resultado.

{
    "protocol": "http",
    "port": 80,
    "stubs": [
        {
            "responses": [
                {
                    "is": {
                        "statusCode": 200,
                        "headers": {
                            "Content­Type": "text/plain"
                        },
                        "body": "Hello, world!"
                    }
                }
            ]
        }
    ]
}

La respuesta es casi, pero no del todo, la misma que la respuesta «Hello, world» que se muestra a continuación:

HTTP/1.1 200 OK
Content­Type: text/plain
Connection: close
Date: Wed, 21 Jun 2020 01:42:38 GMT
Transfer­Encoding: chunked
Hello, world!

Tres cabeceras HTTP adicionales de alguna manera se colaron, entender de dónde proceden estas cabeceras nos obliga a revisar otras características de las respuestas, como la respuesta por defecto.

 

Respuesta por defecto

Mountebank utiliza un stub oculto por defecto cuando la solicitud no coincide con ningún predicado. Ese stub predeterminado no contiene predicados, por lo que siempre coincide con la solicitud, y contiene exactamente una respuesta: la respuesta por defecto. Podemos ver esta respuesta predeterminada si creamos un impostor sin stubs y posteriormente enviamos una petición HTTP a ese puerto.

Mountebank fusiona la respuesta predeterminada en cualquier respuesta que proporcionamos, qué quiere decir esto, significa que sólo se necesita especificar los campos que son diferentes de los predeterminados, simplificando la configuración de la respuesta.

Mountebank nos permite cambiar la respuesta predeterminada para que se adapte mejor a nuestras necesidades, como podemos ver en el ejemplo, donde hemos configurado en la respuesta predeterminada además del statusCode los encabezados Connection y ContentLenght, con Connection el servidor le dice al cliente que mantenga abierta la conexión ya que Mountebank lo cierra por defecto

El comportamiento predeterminado del impostor establece el encabezado TransferEncoding: chunked, que divide el cuerpo en una serie de trozos y prefijos, cada uno con el número de bytes que contiene. La ventaja de enviar al cuerpo un trozo cada vez es que el servidor puede comenzar a transmitir datos al cliente antes de que el servidor tenga todos los datos. La estrategia alternativa es calcular la longitud de todo el cuerpo HTTP antes de enviarlo y proporcionar esa información en el encabezado.

{
    ...
    "defaultResponse": {
        "statusCode": 400,
        "headers": {
            "Connection": "Keep­Alive",
            "Content­Length": 0
        }
    },
    "stubs": [
        {
            "predicates": [{...}],
            "responses": [{...}]
        }
    ]
}

 

Respuestas infinitas

Otra característica que provee Mountebank en las respuestas son las respuestas infinitas, vamos a verlas con otro ejemplo, imaginemos que tenemos un escenario de prueba de petición de pedidos donde parte del proceso de envío de pedidos consiste en comprobar que el inventario es suficiente, ya que el inventario no es estático, se vende y se repone, necesitamos que la misma solicitud al servicio de inventario pueda responder un resultado diferente cada vez.

{
    "protocol": "http",
    "port": 3000,
    "stubs": [
        {
            "responses": [
                {
                    {"is": {"body": "54"}},
                    {"is": {"body": "21"}},
                    {"is": {"body": "0"}}
                }
            ]
        }
    ]
}

Creando un impostor como se muestra en el ejemplo, podemos ver que la primera llamada devuelve 54, la segunda devuelve 21, y la tercera devuelve 0. Si se necesitará realizar una cuarta llamada, volverá a devolver 54, luego 21, y 0 y así sucesivamente. Mountebank trata la lista de respuestas como una lista infinita, con la primera y última entrada conectadas como un círculo.

 

Impostores HTTPS

La creación de un impostor HTTPS se parece a la creación de un impostor HTTP. La única diferencia es que el protocolo se establece en https. Esto es ideal para configurar rápidamente un servidor HTTPS, pero utiliza un certificado predeterminado, ese certificado es a la vez inseguro y no confiable. 

Mountebank permite crear el impostor con el certificado y la clave privada, para ello basta con especificar los parámetros key y cert en lo que se conoce como formato PEM

Establecer el indicador mutualAuth en un impostor significa que aceptará los certificados de cliente utilizados para la autenticación.

{
    "protocol": "https",
    "port": 443,
    "key": "­­­­­BEGIN RSA PRIVATE KEY­­­­­nMIIEpAIBAAKC...",
    "cert": "­­­­­BEGIN CERTIFICATE­­­­­nMIIDejCCAmICCQD...",
    "mutualAuth": true                              
}

Mountebank utiliza un lenguaje de plantillas llamado EJS para persistir en la configuración de los impostores y ha incorporado algunas mejoras que nos permiten simplificar y hacer más legibles las plantillas, como veis en el ejemplo definir el certificado de esta forma ni es legible ni fácil ni cómodo.

Mountebank agrega la función stringify al lenguaje de plantillas, que hace el equivalente a una llamada JSON.stringify de JavaScript sobre el contenido del archivo dado, convirtiendo el contenido del archivo multilínea en una cadena JSON haciendo mucho más legible nuestra plantilla EJS.

{
    "port": 80,
    "cert": "<%­ stringify(filename, 'ssl/cert.pem') %>",
    "key": "<%­ stringify(filename, 'ssl/key.pem') %>",
    "stubs": [
        {
            "predicates": [{...}],
            "responses": [{...}]
        }
    ]
}

 

Conclusiones

En este artículo hemos repasado qué son y cómo podemos utilizar los tipos de respuesta is o enlatadas que proporciona Mountebank para devolver nuestras respuestas a las llamadas que hagan nuestros test, también hemos visto que es la respuesta por defecto, como crear respuestas infinitas y por último como configurar rápidamente un servidor HTTPS.

Puedes descargar el proyecto completo aquí.

 

Referencias

http://www.mbtest.org/

https://github.com/bbyars/mountebank

https://github.com/bbyars/mountebank-in-action

https://hub.docker.com/r/bbyars/mountebank

http://www.mbtest.org/docs/api/stubs

 

La entrada Mountebank – Respuestas enlatadas se publicó primero en Adictos al trabajo.

Mountebank – Respuestas proxy

$
0
0

Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro 15′ (2,3 Ghz Intel Core i7, 16GB DDR4).
  • Sistema Operativo: Mac OS Catalina 10.15.5
  • Entorno de desarrollo: JDK 11, IntelliJ, Docker, Postman

 

Introducción

En este artículo vamos a ver el uso de otro de los tipos de respuestas que provee Mountebank, las respuestas de tipo proxy, espero que os guste y/o sirva de ayuda en vuestros tests.

Este artículo está dividido en 6 partes:

  • Respuestas de tipo proxy
  • Creación de predicados con predicateGenerators
  • Capturar múltiples respuestas
  • Proxy “transparente”
  • Proxy “parciales”
  • Conclusiones

 

Respuestas de tipo proxy

Los impostores de tipo proxy se configuran para ir directamente a la fuente real, nos permite poner a un impostor mountebank entre el sistema bajo prueba y un servicio real del que depende, esta disposición permite capturar datos reales que se pueden reproducir en pruebas, en lugar de crearlos manualmente utilizando respuestas enlatadas. 

Mountebank expone el estado actual de cada impostor a través de la API, si enviamos una solicitud GET a http://localhost:2525/imposters/80, se puede ver la respuesta guardada.

Mountebank registra el tiempo que se tarda en llamar al servicio real en el campo proxyResponseTime, que podemos usar para agregar latencia simulada durante la prueba de rendimiento.

El comportamiento por defecto de un proxy (definido por el modo proxyOnce) es llamar al servicio de bajada la primera vez que ve una petición que no reconoce, y desde ese punto en adelante enviar la respuesta guardada de vuelta para futuras peticiones que se vean similares.

{
    "port": 80,
    "protocol": "http",
    "stubs": [
        {
            "responses": [
                {
                    "proxy": {
                        "to": "http://api.autentia.com"
                    },
                    "mode": "proxyOnce"
                }
            ]
        }
    ]
}

 

Creación de predicados con predicateGenerators

Los predicateGenerators son los responsables de crear los predicados sobre las respuestas guardadas. Los predicados generados utilizan deepEquals para la mayoría de los casos. 

El campo predicateGenerators refleja fielmente el campo predicates estándar y acepta todos los mismos parámetros. Cada objeto en la matriz predicateGenerators genera un objeto correspondiente en la matriz de predicados del stub recién creado.

Vamos a verlo con un ejemplo, para ello vamos a definir el siguiente impostor.

{
    ...
    "proxy": {
        "to": "http://api.autentia.com/...",
        "predicateGenerators": [
            {
                "matches": {
                    "query": true
                },
                "caseSensitive": true
            }
        ]
    }
}

El siguiente fragmento de código refleja cómo se vería el predicado generado por Mountbank y almacenado para las siguientes peticiones que se realicen.

...
"stubs": [
    {
        "predicates": [
            {
                "caseSensitive": true,
                "deepEquals": {
                    "query": {
                        "q": "mountebank",
                        "page": "1"
                    }
                }
            }
        ],
        "responses": [
            {
                "is": { ...
                }
            }
        ]
    },
...

 

Capturar múltiples respuestas

Hasta ahora hemos visto la opción por defecto de Mountebank, tal y como hemos comentado anteriormente, proxyOnce, que es llamar al servicio de bajada la primera vez que ve una petición que no reconoce pero ¿cómo haríamos si tuviéramos un caso de prueba que se basará en la volatilidad del inventario a lo largo del tiempo?, necesitaríamos un proxy que nos permitiera capturar un conjunto de datos más rico o más voluminoso para reproducirlo posteriormente, por ejemplo en unas pruebas de carga. 

El modo proxyAlways se asegura que todas las solicitudes lleguen al servicio real, lo que nos permite capturar múltiples respuestas para un único tipo de solicitud. Crear este tipo de proxy es tan simple como establecer el modo proxyAlways.

La diferencia clave entre proxyOnce y proxyAlways, es que proxyOnce genera los nuevos stubs antes del stub que contiene la respuesta de tipo proxy, mientras que proxyAlways genera los stubs después del stub del proxy.

{
    "port": 80,
    "protocol": "http",
    "stubs": [
        {
            "responses": [
                {
                    "proxy": {
                        "to": "http://api.autentia.com/..."
                    },
                    "mode": "proxyAlways",
                    "predicateGenerators": [
                        {
                            "matches": {
                                "path": true
                            }
                        }
                    ]
                }
            ]
        }
    ]
}

 

Proxy “transparente”

Mountebank provee de un modo más de proxy, el modo “proxyTransparent” que al igual que el modo proxyAlways se asegura que todas las solicitudes lleguen al servicio real, pero a diferencia de éste no guarda las respuestas que recibe del servicio real.

 

Proxy “parciales”

Mountebank también proporciona lo que denomina como proxy parcial, donde la mayoría de las llamadas fluyen a través del servicio real, pero unas pocas solicitudes especiales desencadenan respuestas enlatadas. Fijaos que el proxy no tiene predicados, lo que significa que todas las peticiones que no coincidan con los predicados en los stubs anteriores fluirán a través del proxy.

{
    ...
    "stubs": [
        {
            "predicates": [
                {
                    "contains": {
                        "body": "5555555555555555"
                    }
                }
            ],
            "responses": [
                {
                    "is": {
                        "body": "FRAUD ALERT... "
                    }
                }
            ]
        },
        {
            "predicates": [
                {
                    "contains": {
                        "body": "4444444444444444"
                    }
                }
            ],
            "responses": [
                {
                    "is": {
                        "body": "INSUFFICIENT FUNDS..."
                    }
                }
            ]
        },
        {
            "responses": [
                {
                    "proxy": {
                        "to": "http://api.autentia.com/...",
                        "mode": "proxyAlways"
                    }
                }
            ]
        }
    ]
    ...
}

 

Conclusiones

En este artículo hemos repasado qué son y cómo podemos utilizar los tipos de respuesta proxy que proporciona Mountebank para capturar las respuestas reales y guardar sus respuestas para su reproducción futura.

Puedes descargar el proyecto completo aquí.

 

Referencias

http://www.mbtest.org/

https://github.com/bbyars/mountebank

https://github.com/bbyars/mountebank-in-action

https://hub.docker.com/r/bbyars/mountebank

http://www.mbtest.org/docs/api/proxies

La entrada Mountebank – Respuestas proxy se publicó primero en Adictos al trabajo.

Mountebank – Respuestas inject

$
0
0

Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro 15′ (2,3 Ghz Intel Core i7, 16GB DDR4).
  • Sistema Operativo: Mac OS Catalina 10.15.5
  • Entorno de desarrollo: JDK 11, IntelliJ, Docker, Postman

 

Introducción

En este artículo vamos a ver el uso del último tipo de respuesta que nos provee Mountebank, las respuestas de tipo inject, con este tipo de respuesta Mountebank nos permite extender sus funcionalidades por defecto, espero que os guste y/o sirva de ayuda en vuestros tests.

Este artículo está dividido en 5 partes:

  • Respuestas de tipo inject
  • Añadir estado
  • Seguridad
  • Cómo depurar en Mountebank
  • Conclusiones

 

Respuestas de tipo inject

De vez en cuando podemos encontrarnos con servicios que utilizan algo distinto a JSON o XML, por ejemplo CSV. CSV sigue siendo un formato relativamente popular en ciertos tipos de integraciones de servicios, especialmente en aquellos que implican la transmisión de información de tipo bulkstyle, como por ejemplo el servicio meteorológico del sitio weather.com.

rológico del sitio weather.com.
Day,Description,High,Low,Precip,Wind,Humidity
4­Jun,PM Thunderstorms,83,71,50%,E 5 mph,65%
5­Jun,PM Thunderstorms,82,69,60%,NNE 8mph,73%
6­Jun,Sunny,90,67,10%,NNE 11mph,55%
7­Jun,Mostly Sunny,86,65,10%,NE 7 mph,53%
8­Jun,Partly Cloudy,84,68,10%,ESE 4 mph,53%
9­Jun,Partly Cloudy,88,68,0%,SSE 11mph,56%
10­Jun,Partly Cloudy,89,70,10%,S 15 mph,57%
11­Jun,Sunny,90,73,10%,S 16 mph,61%
12­Jun,Partly Cloudy,91,74,10%,S 13 mph,63%
13­Jun,Partly Cloudy,90,74,10%,S 17 mph,64%

Si tuvieses un caso de prueba que nos diera los días con un porcentaje de humedad superior al 60%, tendríamos un problema por que Mountebank no soporta CSV de forma nativa, y por lo tanto no tiene un predicado que busque niveles de humedad superiores al 60%, estamos fuera de sus capacidades incorporadas.

Para todos estos casos donde pensamos que Mountebank no nos puede ayudar vamos a crear nuestros propios predicados usando JavaScript y el predicado de inyección, pero primero tenemos que iniciar mountebank con el indicador de línea de comandos –allowInjection:

mb --­­allowInjection

Todos los predicados que hemos visto hasta ahora funcionan en un solo campo de la petición.  No es así con inject, que nos da control total al pasar todo el objeto de solicitud a una función JavaScript que escribimos. Esa función JavaScript devuelve true si el predicado coincide con la petición y false de otra manera, como se muestra a continuación:

function (request) {
    if (...) {
        return true;
    }
    else {
        return false;
    }
}

Enchufar la función en la matriz de predicados de un stub implica que se use JSONescaping, que reemplaza las nuevas líneas con ‘n’, y escapa de las comillas dobles:

{
    "predicates": [{
        "inject": "function (request) {n if (...) {n return true;n }n else {n return false;n }n}"
    }],
    "responses": [{
        "is": { ... }
    }]
}

También podemos crear nuestra propia respuesta en mountebank. En su forma más simple, la función de inyección de respuesta refleja la de los predicados, aceptando toda la petición como parámetro. Es responsable de devolver un objeto de respuesta que mountebank fusionará con la respuesta predeterminada. Piensa en ello como si estuvieras creando una respuesta de tipo is usando una función JavaScript, de la siguiente manera.

{
    "responses": [{
        "inject": "function (request) { return { statusCode: 400 }; }"
    }]
}

Una función de inyección predicada sólo requiere un parámetro (“config”) que tiene los siguientes campos:

  • request, el objeto de petición específico del protocolo
  • state, un objeto inicialmente vacío al que se le puede agregar estado; será pasado en las llamadas subsecuentes para el mismo impostor.
  • logger, se usa para escribir información de depuración en los registros de mountebank

La función de inyección de respuesta incluye esos campos y añade uno más al parámetro (“config”):

  • callback, una función de devolución de llamada para soportar operaciones asíncronas

Una cosa importante y de la que no podemos perder el foco es que la virtualización de servicios es una estrategia de prueba que nos da determinismo al probar un servicio que tiene dependencias de tiempo de ejecución. No es una forma de reimplementar dependencias de tiempo de ejecución en una plataforma diferente. Aunque mountebank proporciona funcionalidad avanzada para hacer que tus stubs sean más inteligentes cuando necesites que lo sean, lo mejor es no necesitar que sean tan inteligentes. Cuanto más tontos puedan ser sus servicios virtuales, más mantenible será tu arquitectura de pruebas.

 

Añadir estado

Mountebank pasa un parámetro de estado a tus funciones de inyección que puedes usar para recordar información a través de múltiples solicitudes. Inicialmente es un objeto vacío, pero puedes añadirle la información que quieras cada vez que se ejecute la función de inyección.

Vamos a verlo con un ejemplo, empezaremos añadiendo el parámetro a la función e inicializándolo con las variables que desea recordar, en posteriores llamadas utilizaremos el parámetro.

function (request, state) {
    function csvToObjects (csvData) {...}
    // Initialize state arrays
    if (!state.humidities) {
        state.days = [];
        state.humidities = [];
    }
    var rows = csvToObjects(request.body);
    rows.forEach(function (row) {
    if (state.days.indexOf(row.Day) < 0) {   
        state.days.push(row.Day);
        state.humidities.push(row.Humidity.replace('%', ''));
    }
    });
    ...
}

 

Seguridad

La inyección de JavaScript está desactivada de forma predeterminada cuando se inicia mountebank, y por una buena razón. Con la inyección habilitada, mountebank se convierte en un potencial motor de ejecución remota accesible a cualquiera en la red. La inyección es una característica enormemente útil, pero hay que utilizarla teniendo en cuenta las implicaciones de seguridad. 

La primera precaución es no ejecutar mb en su cuenta de usuario. Comenzar mountebank con un usuario sin privilegios, idealmente uno sin credenciales de dominio de red. La siguiente capa de seguridad es restringir qué máquinas pueden acceder a la web de mountebank. Para esto tenemos varias opciones la más sencilla es utilizar el indicador –localOnly, que restringe el acceso a los procesos que se ejecutan en la misma máquina. Esta opción es perfecta cuando tus pruebas se ejecutan en la misma máquina que mountebank, y debería ser la opción por defecto la mayor parte del tiempo. Cuando se requiere pruebas remotas (durante pruebas de carga extensivas, por ejemplo), también podemos restringir qué máquinas pueden acceder al servidor web de mountebank con la bandera –ipWhitelist, que captura un conjunto limitado de direcciones IP. En el siguiente ejemplo vamos a ver como configurar unas únicas direcciones IP remotas que permiten el acceso a mountebank son 10.22.57.137 y 10.22.57.138:

mb --­­allowInjection --­­ipWhitelist "10.22.57.137|10.22.57.138"

 

Cómo depurar en Mountebank

Escribir funciones de inyección tiene la misma complejidad que escribir cualquier código, excepto que es mucho más difícil depurarlas a través de un IDE porque se ejecutan en un proceso remoto aunque siempre podremos utilizar la depuración de impresión. 

Como ya hemos visto mountebank pasa otro parámetro tanto a la inyección predicada como a la de respuesta para hacer la salida un poco más fácil de detectar en los logs, el logger, veamos un ejemplo sencillo de depuración por impresión:

function (request, state, logger) {
    var rows = csvToObjects(request.body), humidities = rows.map(function (row) {
        return parseInt(row.Humidity.replace('%', ''));
    });
    logger.warn(JSON.stringify(humidities));
    return {};
}

 

Conclusiones

En este artículo hemos repasado qué son y cómo podemos utilizar los tipos de respuesta inject que proporciona Mountebank para crear nuestros propios predicados con una función JavaScript que acepta el objeto de petición y devuelve un booleano que representa si el predicado coincide o no, también cómo crear nuestras propias respuestas con una función JavaScript que acepta el objeto de solicitud y devuelve un objeto que representa la respuesta. Hemos repasado los principales problemas de seguridad que se pueden dar sino somos cautelosos, también hemos visto cómo manejar el estado en nuestras funciones javascript y por último cómo depurar dichas funciones.

Puedes descargar el proyecto completo aquí.

 

Referencias

http://www.mbtest.org/

https://github.com/bbyars/mountebank

https://github.com/bbyars/mountebank-in-action

https://hub.docker.com/r/bbyars/mountebank

http://www.mbtest.org/docs/api/injection

La entrada Mountebank – Respuestas inject se publicó primero en Adictos al trabajo.


Mountebank – Comportamientos

$
0
0

Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro 15′ (2,3 Ghz Intel Core i7, 16GB DDR4).
  • Sistema Operativo: Mac OS Catalina 10.15.5
  • Entorno de desarrollo: JDK 11, IntelliJ, Docker, Postman

 

Introducción

En este artículo vamos a ver el uso de los comportamientos en Mountebank, con ellos podemos alterar la respuesta, espero que os guste y/o sirva de ayuda en vuestros tests.

Este artículo está dividido en 10 partes:

  • ¿Qué son los comportamientos?
  • Características de los comportamientos
  • Tipos de comportamientos
  • Uso del comportamiento decorate
  • Uso del comportamiento shellTransform
  • Uso del comportamiento wait
  • Uso del comportamiento repeat
  • Uso del comportamiento copy
  • Uso del comportamiento lookup
  • Conclusiones

 

¿Qué son los comportamientos?

Los ingenieros de software que provienen de la escuela de pensamiento orientada a objetos utilizan el término «decorar» para referirse a interceptar un mensaje sencillo y aumentarlo de alguna manera antes de reenviarlo al destinatario. En Mountebank, los comportamientos representan una forma de decorar las respuestas antes de que el impostor las envíe de vuelta.

 

Características de los comportamientos

Los comportamientos se sitúan junto al tipo de respuesta en la definición del stub, se pueden combinar múltiples comportamientos juntos, pero sólo uno de cada tipo. 

Ningún comportamiento debe depender del orden de ejecución de otros comportamientos. 

Algunos comportamientos requieren que el indicador permitirInyección se establezca al iniciar mb. 

Los comportamientos no tienen acceso a ningún estado controlado por el usuario como la inyección de respuesta y no permiten respuestas asíncronas.

Los comportamientos decorate y shellTransform son los únicos que aceptan el objeto de respuesta como entrada y lo transforman de alguna manera, enviando un nuevo objeto de respuesta como salida.

Los comportamientos son agnósticos al tipo de respuesta a la que se aplican, lo que significa que también pueden decorar una respuesta de tipo proxy. Pero por defecto, la decoración se aplica sólo a la respuesta proxy en sí, no a la respuesta que guarda.

{
    "responses": [{
        "proxy": {
            "to": "http://downstream­service.com",
            "mode": "proxyOnce",
            "addDecorateBehavior": "..."
        }
    }]
}

El uso básico de los comportamientos se puede ver claramente en el siguiente ejemplo. La respuesta is primero fusionará el código de estado 500 con la respuesta predeterminada, y luego pasará el objeto de respuesta generado a los comportamientos de decorate y wait

{
    "responses": [{
        "is": { "statusCode": 500 },
        "_behaviors": {
            "decorate": ...,
            "wait": …
        }
    }]
}

 

Tipos de comportamientos

Comportamiento ¿Funciona con las respuestas proxy guardadas? ¿Necesita soporte de inyección? Descripción
decorate yes yes Utiliza una función JavaScript para post procesar la respuesta.
shellTransform no yes Envía la respuesta a través de un pipeline de línea de comandos para el postprocesamiento.
wait yes no Añade latencia a una respuesta
repeat no no Repite una respuesta varias veces
copy no no Copia un valor de la solicitud en la respuesta
lookup no no Reemplaza los datos de la respuesta con datos de una fuente de datos externa basada en una clave de la solicitud.

 

Uso del comportamiento decorate

A menudo nos encontramos con la necesidad de utilizar campos con valores dinámicos en las respuestas de nuestros impostores. Sin comportamientos, nos veríamos obligados a utilizar una respuesta de inyección si un campo de la respuesta fuera dinámico. Por ejemplo, supongamos que deseamos devolver el siguiente cuerpo de respuesta:

{
    "timestamp": "2017­07­22T14:49:21.485Z",
    "givenName": "Stubby",
    "surname": "McStubble",
    "birthDate": "1980­01­01"
}

Como podemos observar en el ejemplo el campo timestamp podría quedar obsoleto rápidamente si necesitáramos que siempre fuera una fecha futura y no muy lejana en el tiempo. Desafortunadamente, traducir eso a una respuesta de tipo inject oculta la intención, como se muestra en el siguiente ejemplo.

{
    "responses": [{
    "inject": "function () { return { body: { timestamp: new Date(), givenName: 'Stubby', surname: 'McStubble', birthDate:'1980­01­01' } } }"
    }]
}

Para entender lo que la respuesta está haciendo, tienes que extraer la función JavaScript y mirarla fijamente. Si comparamos eso con la combinación de una respuesta de tipo is y un comportamiento decorate, que envía el mismo JSON por red pero sin traducciones incómodas, como puedes ver en el siguiente ejemplo, podemos ver que el comportamiento decorate nos aporta mucha más claridad en nuestros impostores:

"responses": [{
    "is": {
        "body": {
            "givenName": "Stubby",
            "surname": "McStubble",
            "birthDate": "1980­01­01"
        }
    },
    "_behaviors": {
        "decorate": "function (config) { config.response.body.timestamp = new Date(); }"
    }
}]

 

Uso del comportamiento shellTransform

El siguiente comportamiento es tanto el más general como el más poderoso, y ambos aspectos vienen con una complejidad añadida. Al igual que decorate, shellTransform le permite el postprocesamiento programático de la respuesta. Pero no requiere el uso de JavaScript y permite encadenar una serie de transformaciones posteriores.

Para ver cómo funciona, tomemos dos transformaciones (añadiendo un % y activando una excepción de límite de tasa) que convertimos a comportamientos shellTransform. Cada transformación se implementa como una aplicación de línea de comandos que acepta la petición y la respuesta codificadas JSON como parámetros en la entrada estándar y devuelve la respuesta codificada JSON transformada en la salida estándar. Comencemos por la configuración del impostor:

"responses": [{
    "is": {
        "headers": {
            "x­rate­limit­remaining": 3
        },
        "body": {
            "givenName": "Stubby",
            "surname": "McStubble",
            "birthDate": "1980­01­01"
        }
    },
    "_behaviors": {
        "shellTransform": [
            "ruby scripts/applyRateLimit.rb",
            "ruby scripts/addTimestamp.rb"
        ]
    }
}]

En este ejemplo, se ha elegido canalizar las transformaciones a través de scripts Ruby, pero podría haber sido cualquier lenguaje. A continuación veremos el código de ambas funciones javascript

applyRateLimit.rb:
require 'json'
response = JSON.parse(ARGV[1])
headers = response['headers']
current_value = headers['x­rate­limit­remaining'].to_i

if File.exists?('rate­limit.txt')
    current_value = File.read('rate­limit.txt').to_i
end

if current_value <= 0
    response['statusCode'] = 429
    response['body'] = {
        'errors' => [{
            'code' => 88, 'message' => 'Rate limit exceeded'
        }]
    }
    response['headers']['x­rate­limit­remaining'] = 0
Else
    File.write('rate­limit.txt', current_value ­ 25)
    headers['x­rate­limit­remaining'] = current_value ­ 25
end
puts response.to_json

addTimestamp.rb:
require 'json'
response = JSON.parse(ARGV[1])
response['body']['timestamp'] = Time.now.getutc
puts response.to_json

Al encadenar transformaciones, shellTransform actúa como una forma de agregar una tubería de transformación a su manejo de respuestas, permitiéndonos agregar toda la complejidad que necesitemos.

 

Uso del comportamiento wait

A veces es necesario simular latencia en las respuestas, y el comportamiento wait le dice a mountebank que se tome un tiempo antes de devolver la respuesta, se le pasa el número de milisegundos de la siguiente manera. Al igual que el comportamiento de decorate, puede agregar el comportamiento de espera a las respuestas guardadas que generan los proxys.

"responses": [{
    "is": {
        "body": {
            "givenName": "Stubby",
            "surname": "McStubble",
            "birthDate": "1980­01­01"
        }
    },
    "_behaviors": {
        "wait": 3000
    }
}]

 

Uso del comportamiento repeat

A veces es necesario enviar la misma respuesta varias veces antes de pasar a la siguiente respuesta. El comportamiento repeat acepta el número de veces que quieres repetir la respuesta.

Un caso de uso común implicaría desencadenar una respuesta de error después de un número determinado de respuestas de “happy path”. Esto se puede hacer con sólo dos respuestas y un comportamiento repetitivo, como se muestra en el ejemplo:

"responses": [{
    "is": {
        "body": {
            "givenName": "Stubby",
            "surname": "McStubble",
            "birthDate": "1980­01­01"
        }
    },
    "_behaviors": {
        "repeat": 3
    }
},
{
    "is": {
        "body": {
            "givenName": "Jerry",
            "surname": "McCorrey",
            "birthDate": "1998­06­06"
        }
    }
}]

 

Uso del comportamiento copy

Siempre se puede agregar datos dinámicos a una respuesta a través de una respuesta de inject, o a través de los comportamientos decorate y shellTransform. Pero estos dos comportamientos adicionales apoyan la inserción de ciertos tipos de datos dinámicos en la respuesta sin la sobrecarga del control programático.

El comportamiento de copy nos permite capturar alguna parte de la solicitud e insertarla en la respuesta, por ejemplo: copiar el id de la solicitud a la respuesta, el comportamiento de copia acepta una matriz, lo que significa que puede realizar múltiples reemplazos en la respuesta. Cada reemplazo debe usar un token diferente, y cada uno puede seleccionar de una parte diferente de la solicitud. El comportamiento de copia soporta: xpath y jsonpath

"responses": [{
    "is": {
        "statusCode": 200,
        "body": {
            "id": "${ID}[1]",
            "givenName": "Stubby",
            "surname": "McStubble",
            "birthDate": "1980­01­01"
        }
    },
    "_behaviors": {
        "copy": [{
            "from": "path",
            "into": "${ID}",
            "using": {
                "method": "regex",
                "selector": "/copy/(.*)"
            }
        }]
    }
}]

 

Uso del comportamiento lookup

El comportamiento de búsqueda le permite reemplazar los tokens en la respuesta con datos dinámicos que provienen de una fuente de datos externa. La única fuente de datos que mountebank soporta es un archivo CSV. Una búsqueda exitosa requiere tres valores:

  1. Una llave seleccionada de la solicitud (Kip Brady)
  2. La conexión a la fuente de datos externa (data/errors.csv)
  3. La columna clave en la fuente de datos externa (nombre)

name,statusCode,errorCode,errorMessage
Tom Larsen,500,serverError,An unexpected error occurred
Kip Brady,400,duplicateEntry,User already exists
Mary Reynolds,400,tooYoung,You must be 18 years old to register
Harry Smith,503,serverBusy,Server currently unavailable

"responses": [{
    "is": {
        "statusCode": "{row}['statusCode']",
        "body": {
            "code": "{row}['errorCode']",
            "message": "{row}['errorMessage']"
        }
    },
    "_behaviors": {
        "lookup": [{
            "key": {
                "from": "path",
                "using": { "method": "regex", "selector": "/lookup/(.*)$" },
                "index": 1
            },
            "fromDataSource": {
                "csv": {
                "path": "/mountebank/servers/data/errors.csv",
                "keyColumn": "name",
                "delimiter": ","
            }
        },
        "into": "{row}"
    }]
  }
}

 

Conclusiones

En este artículo hemos repasado qué son y cómo podemos utilizar los diferentes tipos de comportamientos que provee Mountebank, para sacar mayor provecho a las respuestas de nuestros impostores.

Puedes descargar el proyecto completo aquí.

 

Referencias

http://www.mbtest.org/

https://github.com/bbyars/mountebank

https://github.com/bbyars/mountebank-in-action

https://hub.docker.com/r/bbyars/mountebank

http://www.mbtest.org/docs/api/behaviors

La entrada Mountebank – Comportamientos se publicó primero en Adictos al trabajo.

A11y Pill – La presión bajo el foco

$
0
0

Índice

1 – Introducción

El foco del sistema permite que los usuarios puedan utilizar nuestras apps y sitios web con el teclado. Sin embargo, muchas veces este está oculto, no se aprecia demasiado bien o no se recibe en los elementos que debería. Incluso es escondido a propósito por motivos estéticos.

En esta píldora vamos a ver cómo manejar el foco y qué alternativas tenemos para representarlo en pantalla.

2 – Elementos enfocables

Por defecto, los elementos enfocables son aquellos que pueden ser activados. Aquí se incluyen enlaces, botones y controles de formulario. Siempre que podamos, deberíamos utilizar estos elementos.

Esto ya nos da una pista de qué elementos deberían recibir el foco y cuáles no. La interactividad es un criterio bastante aproximado. Es cierto que, por diferentes motivos, otro tipo de elementos también podrían requerir ser enfocados. Pero se trata de excepciones.

Recuerda que un elemento no recibirá eventos de teclado si no está enfocado. Cuando hayamos añadido interactividad a un elemento que no la debería tener por defecto, un span, por ejemplo, podemos utilizar el atributo tabindex para definir su comportamiento frente al foco:

  • tabindex=»0″ clasifica el elemento como enfocable y lo incluye en la secuencia del tabulador.
  • tabindex=»-1″ clasifica el elemento como enfocable, pero no lo añade a la secuencia del tabulador.

Así que, la siguiente pregunta es obvia…

3 – Cuándo añadir un elemento a la secuencia del tabulador

Por defecto, deberíamos hacer esto siempre que queramos que un elemento sea interactivo. Sin embargo, hay algunas excepciones que debemos considerar.

Pensemos, por ejemplo, en un combobox. Decidimos no utilizar el elemento select y realizar una implementación personalizada utilizando el patrón de WAI-ARIA para este tipo de componente. El elemento principal de este componente, generalmente un input, deberá estar en la secuencia del tabulador. Sin embargo, cada opción que despliega el combobox no debería estar en ella, pero sí que debería ser enfocable. Así el usuario podrá saltar al siguiente control de formulario o elemento interactivo sin tener que pasar por todas las opciones.

Como regla general, podemos asumir que cuando un componente es compuesto, es decir, sus partes son enfocables, sólo una de ellas deberá estar en la secuencia del tabulador.

Esto también implica que, si optamos por este tipo de implementaciones, debemos proporcionar también el manejo del foco necesario mediante las teclas de flechas y otras. Esta interacción también está descrita en los patrones de WAI-ARIA para ofrecer una experiencia homogénea entre sitios web.

4 – Visibilidad del foco

Todo lo anterior no sirve de nada si el foco no es visible. El usuario debe saber con qué elemento interactúa. Si no, es como jugar a la ruleta rusa con un montón de balas en la recámara. Esto no sólo implica que el foco sea visible, sino que cumpla con otros criterios de accesibilidad, como el contraste o el uso del color.

Se tiene asumido que el foco es un recuadro punteado que engloba el elemento. Esta idea supongo que se ha asentado debido a que es la forma en que los navegadores lo muestran por defecto. No obstante, no tiene por qué ser así. Podemos darle la apariencia que queramos. Incluso podemos adaptar el foco a cada tipo de elemento de nuestra interfaz. Con esto se consigue un resultado muy elegante y una experiencia propia. Lo importante es que el usuario sepa qué elemento está enfocado.

Pero, si no nos queremos complicar tanto la vida y no queremos que ese recuadro punteado esté omnipresente en nuestra página por motivos estéticos, podemos hacer una visualización condicional del foco. Cuando detectemos que el usuario está usando el ratón para navegar por nuestro sitio web, podemos añadir una clase CSS al body que oculte el foco. Para el resto de casos, estará visible.

5 – Conclusiones

La visibilidad y el manejo correcto del foco son dos elementos fundamentales para la accesibilidad de nuestros sitios web y aplicaciones. Su uso no tiene por qué implicar un aspecto más feo, siempre que sepamos trabajarlo de la forma adecuada.

La entrada A11y Pill – La presión bajo el foco se publicó primero en Adictos al trabajo.

Testcontainers – Dockeriza tus tests de integración en Java

$
0
0

Índice de Contenidos

    1. Introducción
    2. Contenedor genérico
    3. Contenedor mysql
    4. Contendor Singleton

1. Introducción

El uso de una base de datos en memoria como H2 en Java tiene algunas desventajas porque los tests podrían depender de características que las bases de datos en memoria no pueden reproducir y algunos tests que han pasado en local pueden fallar en producción. Esto afecta a la fiabilidad de nuestros tests porque no cubriremos al 100% los mismos escenarios que en un entorno real. Testcontainers aparece en nuestro camino para que podamos dockerizar nuestros tests. Es una biblioteca de Java que permite crear cualquier instancia de Docker y manipularla. Claramente los tests van a tardar unos segundos mas que al usar una BBDD en memoria, pero debemos tener en cuenta que los estamos lanzando contra una base de datos igual a la de nuestro entorno de producción.

Los siguientes ejemplos están hechos con JUnit5, pero si estás usando JUnit4, los cambios son mínimos, por lo que no tendrás ningún problema. Comenzamos añadiendo la dependencia en el pom.xml. Podemos añadir la dependencia genérica o una más específica (si queremos un contenedor preconfigurado). También debemos añadir la dependencia del driver de base de datos (Testcontainers no lo añadirá por nosotros). En maven repository puedes ver la lista de contenedores ya preconfigurados. Algunos ejemplos son mongoDB, postgresql, cassandra, elasticsearch, rabbitmq, entre otros.

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>testcontainers</artifactId>
    <version>1.14.3</version>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>mysql</artifactId>
    <version>1.14.3</version>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.20</version>
</dependency>

¿Cómo funciona Testcontainers?

  1. Levanta un contenedor con la imagen de docker específica  (en mi ejemplo estaré usando una imagen mysql).
  2. Otro contenedor llamado Ryuk se levantará y su tarea principal es la de gestionar el arranque y la detención del contenedor.

consola: docker ps, 2 contenedores mysql y Ryuk

Una de las ventajas de esta biblioteca es su integración con JUnit. Para poder usar las siguientes anotaciones, necesitamos añadir esta dependencia.

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>1.14.3</version>
    <scope>test</scope>
</dependency>

De la documentación:

  • @Testcontainers: es una extensión de JUnit Jupiter para activar el inicio automático y la detención de los contenedores utilizados.
  • @Containers: se usa junto con la anotación @Testcontainers para señalar contenedores que deben ser administrados por Testcontainers.

2. Creando un contenedor genérico

Podemos crear un contenedor genérico a partir de cualquier imagen de docker pública o un docker-compose. Como este es un enfoque mas genérico, necesitamos realizar alguna configuración mas que si tuviésemos un contenedor preconfigurado.

@Container
 private GenericContainer container = new GenericContainer("image_name")
            .withExposedPorts(port_number);

withExposedPorts(port),exponemos el puerto interno por defecto (en mysql es 3306) del contenedor, que será mapeado a un puerto aleatorio. Para recuperar ese puerto aleatorio en tiempo de ejecución, podemos usar el método getMappedPort(original_port) o simplemente getFirstMappedPort(). Si no exponemos el puerto, obtendremos el siguiente error ‘Container doesn’t expose any ports’. También podemos añadir variables de entorno al contenedor con .withEnv(), ejecutar comandos dentro de un contenedor (como docker exec), administrar nuestras propias estrategias de espera y arranque, etc. En la documentación podrás encontrar información mas detallada.

3. Creando un contenedor mysql

Como dije al principio, tenemos varios contenedores preconfigurados y listos para ser usados. En este caso, voy a utilizar un contenedor mysql para mis tests.
Podemos decidir si queremos iniciar y detener el contenedor cada vez que se ejecute un test o una única vez antes de cada clase de test (veremos más adelante cómo crear un contenedor singleton).

//Once per test class
 @Container
 private static final MySQLContainer mysql = new MySQLContainer("mysql:latest");

// Once per test method
 @Container
 private MySQLContainer mysql = new MySQLContainer("mysql:latest");

Nota: Si estás usando JUnit4, puedes usar las anotaciones @Rule y @ClassRule.

El siguiente ejemplo levanta un contenedor mysql y luego ejecuta mi HelloEndpointIT. Estoy usando static final en la instancia del contenedor, por lo que este será compartido entre todos los tests de la clase. El contenedor mysql me proporciona ciertos métodos para configurar un nombre de base de datos, un nombre de usuario y contraseña. Si no se especifica, se utilizan valores por defecto (nombre de base de datos: test, contraseña: test, nombre de usuario: test).

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles(profiles = "test")
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@Testcontainers
public class HelloEndpointIT {

    @Autowired
    private TestRestTemplate restTemplate;

    @LocalServerPort
    private int port;

    @Container
    private static final MySQLContainer mysql = new MySQLContainer("mysql:latest")
                .withDatabaseName("demo_db_name")
                .withUsername("any_username")
                .withPassword("any_passw");

    @BeforeAll
    private void initDatabaseProperties() {
        System.setProperty("spring.datasource.url", mysql.getJdbcUrl());
        System.setProperty("spring.datasource.username", mysql.getUsername());
        System.setProperty("spring.datasource.password", mysql.getPassword());
    }

    @Test
    public void hello_endpoint_should_return_hello_world() {
        HttpHeaders headers = new HttpHeaders();
        HttpEntity entity = new HttpEntity(headers);

        ResponseEntity<String> response = this.restTemplate.exchange(createUrlWith("/hello"), HttpMethod.GET, entity, String.class);

        assertThat(response.getStatusCode(), equalTo(HttpStatus.OK));
        assertThat(response.getBody(), equalTo("Hello world"));
    }

    private String createUrlWith(String endpoint) {
        return "http://localhost:" + port + endpoint;
    }
}

Cuando el contenedor ya está levantado, se necesita establecer la configuración del datasource. Podemos obtener la url con el puerto mapeado utilizando el método getJdbcUrl(), así como getUsername() y getPassword(). Se observa en el ejemplo cómo añado estos valores de configuración antes de ejecutar mis tests usando la anotación @BeforeAll proporcionada por JUnit5. En JUnit4 seria @BeforeClass.

mensaje en consola: contenedor docker arrancado

4. Creando un contendor Singleton

Hasta ahora hemos visto cómo levantar nuestro contenedor en una clase de tests, pero me gustaría crear una única instancia para todas mis clases. Veamos cómo levantar un contenedor singleton antes de ejecutar todos nuestros tests de integración.

Vamos a usar static Initializers para instanciar el contenedor solo una vez. Necesitamos hacerlo en una clase abstracta y extender todas nuestras clases de tests. En este caso, necesitamos iniciar manualmente el contenedor en nuestro Initializer blocker y cuando los tests hayan acabado, el contenedor Ryuk se encargará de detenerlo.

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles(profiles = "test")
public abstract class DemoEndpointIT {

    @Autowired
    private TestRestTemplate restTemplate;

    @LocalServerPort
    private int port;

    private static final MySQLContainer mysql;

    static {
        mysql = new MySQLContainer("mysql:latest");
        mysql.start();
        System.setProperty("spring.datasource.url", mysql.getJdbcUrl());
        System.setProperty("spring.datasource.username", mysql.getUsername());
        System.setProperty("spring.datasource.password", mysql.getPassword());
    }

    protected String createUrlWith(String endpoint) {
        return "http://localhost:" + port + endpoint;
    }

    protected TestRestTemplate getRestTemplate() {
        return this.restTemplate;
    }
}

Como hemos visto en los ejemplos, tener un contenedor mysql listo para los tests de integración ha sido bastante sencillo y la configuración ha sido mínima.

La entrada Testcontainers – Dockeriza tus tests de integración en Java se publicó primero en Adictos al trabajo.

Manejo de ventanas en iPadOS

$
0
0
  1. Introducción
  2. Cómo declarar Scene Delegate
  3. Nueva forma de manejar los estados en Swift UI (iOS14)
  4. Adaptación de la app a nueva API de escenas
  5. Los métodos para crear las ventanas
  6. Actualización del contenido de la ventanas
  7. Conclusiones
  8. Referencias

Introducción

Con la introducción de iOS 13 en iPad OS por fin tenemos la posibilidad de crear y usar las ventanas en nuestra app de iOS. En este tutorial lo aprendemos. Históricamente las aplicaciones iOS usaban solo una ventana. Cada ventana puede contener la información completamente diferente o parecida. Para implementarlo no tenemos que hacer mucho esfuerzo. El proceso es bastante sencillo y claro. De momento solo los usuarios de iPad pueden disfrutar de esta funcionalidad, pero seguramente en el futuro podemos verlo en los iPhones también.

Cómo declarar Scene Delegate

Apple ha cambiado significativamente el ciclo de vida de aplicación en iOS 13. Antes cada aplicación tenía solo una ventana y su ciclo de vida fue gestionado por UIApplicationDelegate. Pero en iOS13 cada aplicación puede tener diferentes ventanas (escenas) y hay que gestionar el ciclo de vida de cada una. Ahora UISession controla el ciclo de vida de ventana. También hay nuevos protocolos para manejar el ciclo de vida de la ventana: UISceneSession, UISceneDelegate y UISceneConfiguration.
Estas novedades cambian la forma en que interactúa con UIApplicationDelegate. Muchos de los métodos de UIApplicationDelegate ahora se han trasladado al delegado de escena UIWindowSceneDelegate.

import UIKit
import SwiftUI

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?


    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
        // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
        // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).

        // Create the SwiftUI view that provides the window contents.
        let contentView = ContentView()

        // Use a UIHostingController as window root view controller.
        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            window.rootViewController = UIHostingController(rootView: contentView)
            self.window = window
            window.makeKeyAndVisible()
        }
    }

    func sceneDidDisconnect(_ scene: UIScene) {
        // Called as the scene is being released by the system.
        // This occurs shortly after the scene enters the background, or when its session is discarded.
        // Release any resources associated with this scene that can be re-created the next time the scene connects.
        // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead).
    }

    func sceneDidBecomeActive(_ scene: UIScene) {
        // Called when the scene has moved from an inactive state to an active state.
        // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
    }

    func sceneWillResignActive(_ scene: UIScene) {
        // Called when the scene will move from an active state to an inactive state.
        // This may occur due to temporary interruptions (ex. an incoming phone call).
    }

    func sceneWillEnterForeground(_ scene: UIScene) {
        // Called as the scene transitions from the background to the foreground.
        // Use this method to undo the changes made on entering the background.
    }

    func sceneDidEnterBackground(_ scene: UIScene) {
        // Called as the scene transitions from the foreground to the background.
        // Use this method to save data, release shared resources, and store enough scene-specific state information
        // to restore the scene back to its current state.
    }


}

Como veis son prácticamente iguales a los métodos del protocolo UIApplicationDelegate. En iOS 13 en UIApplicationDelegate tenemos dos nuevos métodos para manejar las sesiones.

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        return true
    }

    // MARK: UISceneSession Lifecycle

    func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
        // Called when a new scene session is being created.
        // Use this method to select a configuration to create the new scene with.
        return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
    }

    func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
        // Called when the user discards a scene session.
        // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
        // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
    }


}

Se usan para configurar las sesiones (el rol de la sesión: la ventana o el documento, la jerarquía de la vistas) y para eliminar todos los recursos de las sesiones descartadas.

Nueva forma de manejar los estados en Swift UI (iOS14)

En las ultimas betas de Xcode 12, hay una nueva opción al crear una aplicación SwiftUI: «SwiftUI App«. Es una de las dos opciones para el ciclo de vida de la aplicación. Ahora podemos controlar el ciclo de la sesión con el código declarativo de SwiftUI. Es una manera mucho más compacta y clara.

@main
struct HelloWorldApp: App {
    @Environment(\.scenePhase) private var scenePhase
    @SceneBuilder var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .onChange(of: scenePhase) { (newScenePhase) in
            switch newScenePhase {
            case .active:
                print("scene is now active!")
            case .inactive:
                print("scene is now inactive!")
            case .background:
                print("scene is now in the background!")
            @unknown default:
                print("Apple must have added something new!")
            }
        }
        Settings {
            SettingsView()
        }
    }
}

Antes, esto se manejaba en AppDelegate y SceneDelegate (dos ficheros distintos), lo que dificultaba la administración del estado de la aplicación.

Tenemos que poner el property wrapper @SceneBuilder a la propiedad body si queremos incluir más que una escena en la app.

Además, ahora los ficheros AppDelegate y SceneDelegate no están en el proyecto. En cambio, un archivo llamado <App Name> App los reemplaza.

Las novedades de nueva estructura

Dentro de este fichero hay algunas novedades:

  • @main le dice a Xcode que la siguiente estructura, HelloWorldApp, será el punto de entrada para la aplicación. Solo se puede marcar una estructura con este atributo.
  • Según la documentación, App es un protocolo que «representa la estructura y el comportamiento de una aplicación». HelloWorldApp se ajusta a esto. Es como la vista base de aplicación. Literalmente, aquí escribes cómo se verá la aplicación.
  • Scene: el body de una vista SwiftUI debe ser del tipo Vista. Del mismo modo, el cuerpo de una aplicación SwiftUI debe ser del tipo UIScene.

Cada escena contiene la vista raíz de una jerarquía de vistas y tiene un ciclo de vida administrado por el sistema. La escena actúa como un contenedor para sus vistas. El sistema decide cuándo y cómo presentar la jerarquía de vistas en la interfaz de usuario de una manera apropiada para la plataforma y que dependa del estado actual de la aplicación.

Y como las plataforma macOS y iPadOS admiten múltiples ventanas, ajustar todas las vistas de aplicación en una escena facilita la reutilización al tiempo que permite «fases de escena» que incluyen estados activos, inactivos y de fondo.
WindowGroup es una escena que envuelve vistas. La vista que queremos presentar, (ContentView) es una Vista, no una escena. WindowGroup nos permite envolverlos en una sola escena que SwiftUI puede reconocer y mostrar.
Primero hacemos una propiedad, scenePhase, que obtiene el estado actual de actividad del sistema. La barra invertida (\) indica que estamos usando un keypath, lo que significa que nos estamos refiriendo a la propiedad en sí y no a su valor. Y cada vez que cambia el valor de la propiedad, se llama al modificador onChange, donde podemos obtener el estado del ciclo de vida.

Adaptación de app a nueva API de escenas

Para añadir el soporte de múltiples ventanas a tu aplicación hay que actualizar el fichero Info.plist. Los siguientes pasos para actualizar el fichero Info.plist:

  • Abre Info.plist
  • Pincha el botón «+» y añade el nodo Application Scene Manifest.
  • Abre el elemento Application Scene Manifest haciendo clic en el botón (▼).
  • Establece el valor de «Enable Multiple Windows» en «YES».
  • Abre el elemento «Scene Configuration», pinchando el botón (+) para añadir la nueva configuración de escena
  • Elige «Application Session Role».
  • Abre el primer elemento «Item 0» que contiene los valores como «Class Name«, «Delegate Class Name«, «Configuration Name» y «Storyboard Name«. Tienes que rellenar «Delegate Class Name» con el nombre de la clase del delegado de tu escena (por ejemplo, $(PRODUCT_MODULE_NAME).SceneDelegate), «Configuration Name» con un nombre único que la aplicación utilizará para identificar la escena internamente, «Storyboard Name» con el nombre del storyboard que contiene la interfaz de usuario inicial de la escena (borrarlo si usas SwiftUI) y «Class Name» con el nombre de la clase de escena que habitualmente es UIWindowScene (normalmente puedes borrarlo también).

Después de haber establecido estos valores, tienes que añadir un delegado de escena.

Añadir un delegado de escena

El nombre de clase de este delegado de escena debe coincidir con el nombre de clase que has puesto en el archivo Info.plist en el paso anterior.
Crea un nuevo archivo Swift llamado SceneDelegate.swift y añade el siguiente código al nuevo archivo:

import UIKit


@available(iOS 13.0, *)
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
  
  var window: UIWindow?

  func scene(
    _ scene: UIScene,
    willConnectTo session: UISceneSession,
    options connectionOptions: UIScene.ConnectionOptions
  ) {
    if let windowScene = scene as? UIWindowScene {
    
      let window = UIWindow(windowScene: windowScene)
      window.rootViewController = UIHostingController(
        rootView: ContentView()
      )
      self.window = window
      window.makeKeyAndVisible()
    }
  }
}

Si necesitas el soporte de iOS 12, añade el modificador @available(iOS 13.0, *). Solo se compilará para iOS 13.
Tienes que añadir la propiedad window.

Es posible que tengas que reproducir parte de la lógica de tu aplicación existente en tu nuevo delegado de escena. En iOS 13, el delegado de escena realiza muchas funciones que realizaba UIApplicationDelegate.
Si solo quieres la compatibilidad con iOS 13 y versiones posteriores, mueve esta lógica. Si es compatible con iOS 13 y sistemas operativos anteriores, deja el código de UIApplicationDelegate tal cual y también añádelo al delegado de escena. Como ves el delegado de escena duplica en parte la funcionalidad de delegado de aplicación (UIApplicationDelegate).

Con eso ya podemos crear nuevas ventanas usando tres diferentes métodos.

Los métodos para crear diferentes ventanas

Un usuario puede crear las nuevas ventanas usando varios métodos diferentes:

Si la aplicación declara compatibilidad con múltiples ventanas, el usuario puede entrar en la vista multitarea deslizando hacia arriba para mostrar el dock y pulsando el icono de su aplicación mientras la aplicación ya está en el primer plano, hasta que aparezca el menú con «Show All Windows». Pulsando este botón se abre la siguiente vista de las ventanas de aplicación, incluido un botón más (+) para crear una nueva ventana:

Con la aplicación en primer plano, deslízate hacia arriba para mostrar el dock nuevamente. Esta vez, arrastra el icono de la aplicación fuera del dock hasta que se convierta en una ventana flotante. Coloca la ventana en el lado derecho o izquierdo de la pantalla. Ahora tienes una segunda ventana ejecutándose en modo deslizante:

Con la ventana deslizante aún ejecutándose, toca y mantén presionado el controlador de arrastre en la parte superior de la ventana. Tira hacia abajo y hacia la derecha hasta que la ventana cambie de forma. Ahora tiene dos ventanas que se ejecutan una al lado de la otra. También puedes mover el controlador en el medio para cambiar el tamaño de estas ventanas:

Agregar soporte para escenas adicionales es bastante simple, pero debes considerar donde tiene sentido agregar este soporte y cómo mantener sus ventanas sincronizadas.

Actualización del contenido de las ventanas

Si has intentado implementar el manejo de las escenas, puedes notar que hay un problema con el estado de la interfaz cuando cambian los datos.
Se necesita una forma de decirle a la interfaz de usuario que actualice su estado actual y vuelva a pedir los datos. Aquí es donde entra en juego UISceneSession. Una sesión de escena puede estar en uno de los siguientes estados:

  1. Primer plano activo: la escena se ejecuta en primer plano y actualmente recibe eventos.
  2. Primer plano inactivo: la escena se está ejecutando en primer plano pero actualmente no recibe eventos.
  3. Fondo: la escena se ejecuta en segundo plano y no está en la pantalla.
  4. Desconectado: la escena no está conectada actualmente a la aplicación.

Las escenas pueden desconectarse en cualquier momento, porque iOS puede desconectarlas para liberar recursos.
Debes manejar las escenas tanto en primer plano como en segundo plano para mantener tus escenas actualizada .

Una manera de hacerlo es utilizar una herramienta familiar: NotificationCenter.
Puedes actualizar cualquier sesión en primer plano escuchando la notificación apropiada y solicitando actualizaciones.

Es posible actualizar las escenas que están en el segundo plano. Para encontrar y actualizar estas escenas, primero debes adjuntarles información de identificación. De esta forma, puedes encontrarlas luego. Para este propósito, se puede usar la propiedad userInfo de la sesión de escena.

Actualiza application(_:configurationForConnecting:options:) en AppDelegate.swift para adjuntar un diccionario userInfo a la sesión de escena. Justo después de crear la configuración de la escena, agrega el siguiente código:

let userInfo = [
  "type": activity.rawValue
]
connectingSceneSession.userInfo = userInfo

Luego puedes actualizar el estado de las escenas de siguiente manera:

func updateListViews() {
  let scenes = UIApplication.shared.connectedScenes
  let filteredScenes = scenes.filter { scene in
    guard 
      let userInfo = scene.session.userInfo,
      let sceneType = userInfo["type"] as? String,
      sceneType == "specialViewId" 
      else {
        return false
    }

    return true
  }
  filteredScenes.forEach { scene in
    UIApplication.shared.requestSceneSessionRefresh(scene.session)
  }
}

 

Conclusiones

Agregar soporte para escenas cambia la forma en que la aplicación iOS responde a los eventos del ciclo de vida. En una aplicación sin escenas, el objeto delegado de la aplicación maneja las transiciones al primer plano o al fondo. Cuando añades el soporte de las escenas a tu aplicación, UIKit transfiere esa responsabilidad a sus objetos delegados de escena. Los ciclos de vida de la escena son independientes entre sí e independientes de la aplicación, por lo que los objetos delegados de la escena deben manejar las transiciones. Las escenas abren el nuevo camino hacia multitarea de otro nivel en iPadOS. Es una novedad muy importante no solo para los usuarios de iPad, pero también para los que tienen iPhone. En el futuro esta funcionalidad sin duda llegará a todos los dispositivos móviles de Apple.

 

Referencias

  1. https://www.raywenderlich.com/5814609-adopting-scenes-in-ipados
  2. https://developer.apple.com/documentation/uikit/app_and_environment/scenes/specifying_the_scenes_your_app_supports
  3. https://developer.apple.com/documentation/uikit/app_and_environment/scenes/supporting_multiple_windows_on_ipad

 

 

 

La entrada Manejo de ventanas en iPadOS se publicó primero en Adictos al trabajo.

Añadir un App Clip en iOS 14

$
0
0



Índice


1. Introducción

Presentados en la WWDC 2020 para iOS 14, los App Clips son la respuesta de Apple a las PWA. Se tratan de Apps ultraligeras, de no más de 10 MB, 100% nativas y que se descargan mediante la lectura de un QR o mediante NFC. Su objetivo es encapsular una mínima funcionalidad de una aplicación, de forma que no sea necesario descargar una aplicación entera para ello (por ejemplo, comprar una bebida o pagar rápidamente por el alquiler de algún servicio).

App Clip Code

Al contrario que una aplicación normal, los App Clips son eliminados por el sistema automáticamente tras detectar que no se usan, borrando todos los datos. En cambio, si estos se usan periódicamente puede mantener los datos del usuario (por ejemplo, imaginemos el caso de un App Clip para comprar un refresco, que se usa casi todos los días para comprar el mismo tipo de refresco, en este caso el App Clip no expirará y además recordará la última elección).

Puedes encontrar el código de ejemplo aquí https://github.com/DaniOtero/DodoAirlines-AppClipDemo


2. Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro 15″ (2.5 GHz Intel Core i7, 16 GB 1600 MHz DDR3)
  • Sistema Operativo: macOS 11 Big Sur Beta 2
  • Xcode 12 Beta 2
  • iOS 14
  • Swift 5.3


3. Añadiendo un App Clip a nuestra aplicación

La aerolínea Dodo Airlines, una vez vista la WWDC 2020, decide que sería una genial idea crear un App Clip para que todos aquellos pasajeros que no tengan la aplicación puedan hacer Check-In rápidamente. Su aplicación actual tiene tres pantallas, metidas en un TabBar, una para buscar y reservar vuelos, otra para hacer Check-In y la típica pantalla de perfil en la que el usuario puede ver sus datos, su nivel, sus millas Nook, etc (nota: todo el código es un ejemplo sencillo, es todo «cartón-piedra», no hace absolutamente nada salvo como mucho navegar entre pantallas).

Pantallas

La pantalla de Check-In, que es la que nos interesa para este ejemplo, pide el apellido del pasajero y el localizador del vuelo. Una vez introducidos nos presenta una pantalla de detalle con los datos del vuelo, y nos ofrece la posibilidad de comprar algún extra y/o para el viaje o hacer Check-In (si no se había hecho aun) y obtener la tarjeta de embarque.

Detalle Check-In

Lo primero vamos a añadir una extensión App Clip a nuestro proyecto, para ello en Xcode seleccionamos el proyecto y pulsando en el icono «+» abajo a la izquierda. La llamaré DodoAirlinesClip por poner un ejemplo.

Añadir App Clip

Me creará en el proyecto un directorio «DodoAirplinesClip». Este contendrá «DodoAirlinesClipApp.swift» y «ContentView.swift» así como el «Info.plist» y el «DodoAirlinesClip.entitlements». Voy a refactorizar «ContentView» para renombrarlo a «MainView», que de momento contiene un «Hello World».

import SwiftUI

struct MainView: View {
    var body: some View {
        Text("Hello World")
    }
}

struct MainView_Previews: PreviewProvider {
    static var previews: some View {
        MainView()
    }
}


4. Modificando la App

Ya tenemos nuestro App Clip, ahora vamos a darle forma. Queremos que el App Clip muestre la pantalla actual de Check-In, pero queremos cambiar su comportamiento. En vez de mostrar la pantalla intermedia de la que hemos hablado antes, queremos que sea rápido y liviano y que directamente nos obtenga la tarjeta de embarque. Para conseguir esta diferencia de comportamiento podemos hacer dos cosas, o bien creamos una vista completamente distinta o bien introducimos un flag de compilación. Para este ejemplo se va a optar por la segunda. Seleccionamos nuestro proyecto, a continuación el target del App Clip, y en la sección «Build Settings» -> «Swift compiler – Custom flags», creamos un nuevo flag, por ejemplo «APPCLIP».
Custom flag

Con esto ya podemos cambiar el comportamiento de la vista en función de si es la aplicación principal o el App Clip con la directiva «#if – #else – #endif

#if APPCLIP
// Do something only if it is the App Clip
#endif

Ahora, añadimos el fichero «CheckinView.swift» y todas sus dependencias al target «DodoAirlinesClip». Seleccionamos los archivos oportunos, y en el inspector los marcamos para dicho target.

Añadir al target

Si nos dejamos alguna dependencia, el compilador se quejará y nos dirá que le falta algo para compilar el target, como es el caso. ¿Qué sucede? Vamos a echar un vistazo a nuestro CheckinView.swift:

//
//  CheckInView.swift
//  DodoAirlines
//
//  Created by Daniel Otero on 08/07/2020.
//

import SwiftUI

#if APPCLIP
import StoreKit
#endif

struct CheckInView: View {
    @State private var surname: String = ""
    @State private var locator: String = ""
    @State private var keyboardOffset: CGFloat = 0
    @State private var showDetail: Bool = false

    var body: some View {
        NavigationView {
            GeometryReader { geometry in
                container(geometry: geometry)
            }
            .colorScheme(.light)
        }
        .accentColor(.white)
        .colorScheme(.dark)
    }

    private func container(geometry: GeometryProxy) -> some View {
        VStack {
            Image("logo")
            VStack(spacing: 8) {
                VStack {
                    TextField("Surname", text: $surname)
                        .padding(.all, 8)
                    TextField("Locator", text: $locator)
                        .padding(.all, 8)
                }
                .background(Color.white)
                .cornerRadius(8)

                NavigationLink(
                    destination: checkinDetailView,
                    isActive: $showDetail,
                    label: {
                        CustomButton(action: checkIn) {
                            Text("Check In")
                        }
                    })
            }
            .padding(.all, 16)
            .background(Color("Box"))
            .cornerRadius(16.0)
            Spacer()
        }
        .frame(height: geometry.size.height - keyboardOffset)
        .animation(.spring())
        .padding(.all, 16)
        .background(Color("Background").edgesIgnoringSafeArea(.all))
        .onAppear {
            NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillShowNotification, object: nil, queue: .main) { notification in
                let rect = notification.userInfo![UIResponder.keyboardFrameBeginUserInfoKey] as! CGRect
                self.keyboardOffset = rect.height
            }

            NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillHideNotification, object: nil, queue: .main) { _ in
                self.keyboardOffset = 0
            }
        }
    }

    private func checkIn() {
        showDetail.toggle()
    }

    var checkinDetailView: some View {
        let flight = Flight(origin: "MAD",
                            dest: "NRT",
                            date: Date(),
                            checked: false,
                            duration: 3600 * 14)
        return CheckInDetailView(flight: flight)
    }
}

Vale, la vista actual contiene un NavigationLink que hace uso de CheckInDetailView y este no está incluido en nuestro target. Esto no es lo que queremos, sino que queremos que directamente devuelva las tarjetas de embarque. Vamos a empezar por declarar dos variables de estado para el App Clip:

#if APPCLIP
    @State private var showCheckInSuccess: Bool = false
    @State private var presentingAppStoreOverlay: Bool = false
#endif

La primera la utilizaremos para simular que hemos obtenido la tarjeta de embarque (simplemente vamos a mostrar un alert a modo de ejemplo). La segunda la vamos a utilizar para mostrar un overlay que invite al usuario a descargar la aplicación principal si le ha gustado la experiencia. Para hacer esto, iOS nos lo da casi hecho. Modificamos ligeramente el cuerpo de la vista:

var body: some View {
    NavigationView {
        GeometryReader { geometry in
                #if APPCLIP
                container(geometry: geometry)
                    .appStoreOverlay(isPresented: $presentingAppStoreOverlay) {
                        SKOverlay.AppClipConfiguration(position: .bottom)
                    }
                #else
                container(geometry: geometry)
                #endif
        }
        .colorScheme(.light)
    }
    .accentColor(.white)
    .colorScheme(.dark)
}

E incluimos el StoreKit en la parte superior:

import SwiftUI

#if APPCLIP
import StoreKit
#endif

Ahora viene lo que realmente nos interesa, localizamos nuestro botón de «Check-In», que ahora mismo es una NavigationLink. Vamos a duplicarlo para que haga cosas distintas en función de si es la aplicación principal o el App Clip:

#if APPCLIP
CustomButton(action: checkIn) {
    Text("Check In")
}
.alert(isPresented: $showCheckInSuccess) {
    Alert(title: Text("Success"),
            message: Text("Your flight is checked in. If this was a real App this should download your boarding passes 🙂"))

}
#else
NavigationLink(
    destination: checkinDetailView(),
    isActive: $showDetail,
    label: {
        CustomButton(action: checkIn) {
            Text("Check In")
        }
    })
#endif

También tenemos que modificar la función «checkIn()» para que haga cosas distintas:

#if APPCLIP
showCheckInSuccess.toggle()
presentingAppStoreOverlay.toggle()
#else
showDetail.toggle()
#endif

Y en este punto aún se nos quejará, eso es porque aún está intentado usar CheckInDetailView. Simplemente lo excluimos de la compilación del App Clip:

#if !APPCLIP
var checkinDetailView: some View {
    let flight = Flight(origin: "MAD",
                        dest: "NRT",
                        date: Date(),
                        checked: false,
                        duration: 3600 * 14)
    return CheckInDetailView(flight: flight)
}
#endif

Y con esto ya sería suficiente, si ahora ejecutamos el target del App Clip, podemos ver que, ademas de no aparecer el TabBar, cuando pulsamos el botón «Check In» nos muestra un alert en vez de navegar a otra pantalla.

Ejecutar App Clip

App Clip


5. Opciones avanzadas

Todos los App Clips llevan asociada una URL que hay que registrar en App Store Connect, que es la que leen los dispositivos por NFC o mediante código QR. Esta URL se puede parsear para cambiar el comportamiento de la App, en función de sus query params. Imaginemos que en el ejemplo anterior, a parte del hacer Check-In, Dodo Airlines quiere que con el App Clip se pueda hacer Check In y comprar una maleta a la vez, pero solo para ciertas ubicaciones por temas legales. En ese caso, tan solo tendría que hacer que en el «DodoAirlinesClipApp», añadir algo como:

import SwiftUI

@main
struct DodoAirlinesClipApp: App {
    @StateObject private var model = DodoAirlinesModel()


    var body: some Scene {
        WindowGroup {
            MainView()
        }
        .environmentObject(model)
        .onContinueUserActivity(NSUserActivityTypeBrowsingWeb, perform: handleUserActivity)
    }

    func handleUserActivity(_ userActivity: NSUserActivity) {
        guard let incomingURL = userActivity.webpageURL,
              let components = NSURLComponents(url: incomingURL, resolvingAgainstBaseURL: true),
              let queryItems = components.queryItems,
              queryItems.contains(where: { $0.name == "purchaseBaggage" }) else {
            return
        }

        guard let payload = userActivity.appClipActivationPayload,
              let latitudeValue = queryItems.first(where: { $0.name == "latitude" })?.value,
              let longitudeValue = queryItems.first(where: { $0.name == "longitude" })?.value,
              let latitude = Double(latitudeValue), let longitude = Double(longitudeValue) else {
            return
        }

        let region = CLCircularRegion(center: CLLocationCoordinate2D(latitude: latitude,
                            longitude: longitude), radius: 100, identifier: "dodo_location")

        payload.confirmAcquired(in: region) { inRegion, error in
            if let error = error {
                print(error.localizedDescription)
                return
            }
            DispatchQueue.main.async {
                model.purchaseBaggageAllowed = inRegion
            }
        }
    }
}

El método confirmAcquired nos permite saber si el App Clip se ha ejecutado en la ubicación leída de los query params (aunque lo suyo sería que esta ubicación o ubicaciones se le devuelva de algún servicio o configuración, pero para propósitos de prueba nos vale).


6. Conclusiones

Aunque es muy fácil crear un App Clip, la principal barrera de entrada es que es obligatorio que esté escrita en Swift UI, con lo cual es difícil que aplicaciones existentes, sobre todo aquellas que aún estén dando soporte a versiones anteriores a iOS 13.

En estos casos crear un App Clip supondría reescribir dicha funcionalidad de una App en Swift UI, y si aun después de la salida de iOS 14 continúa soportando versiones anteriores a iOS 13 tendrá que mantener tanto la parte en UIKit como la parte en Swift UI de forma independiente.

Por contra, para aplicaciones nuevas resulta muy fácil. Tendiendo vistas con alta cohesión y bajo acoplamiento (SRP de SOLID), debería ser bastante fácil poder reutilizar esa vista para el propósito que se desee.

Si además la App cuenta con integración “Sign-In with Apple” y Apple Pay, la cosa aun se vuelve más interesante, porque los usuarios pueden identificarse y pagar fácilmente con solo un par de taps.

La entrada Añadir un App Clip en iOS 14 se publicó primero en Adictos al trabajo.

Viewing all 989 articles
Browse latest View live