LINKS
ACERCA DE TURING

info@turingchallenge.com

Paseo de la Castellana, 95, Planta 29

Madrid

Spain

SOCIAL
  • Black LinkedIn Icon
  • Black Twitter Icon

© 2017 por Turing Challenge. 

  • miguel.veloso

Explorando Microsoft.Bot.Builder SDK v4 con TDD en ASP.NET Core 2.1

En este artículo vamos a desarrollar una pequeña aplicación tipo TO-DO list, muy sencilla, en ASP\.NET Core 2.1 con el Bot Builder v4, aplicando el proceso de desarrollo TDD.


Este proyecto también nos hará más fácil comenzar a explorar la nueva arquitectura y funcionalidad del SDK v4.


Introducción


Con el SDK v4 se reescribió por completo el Bot Builder y hay algunos cambios importantes de arquitectura, que están muy bien recopilados y explicados en el blog post Bot Framework V4: What I learnt in 4 days in July 2018 de Martin Kearn, donde se pueden revisar en detalle con links a la documentación relevante y que luego se pueden explorar paso a paso con la aplicación que vamos a desarrollar aquí.


Al comenzar el tutorial del Echo Bot con v4 me llamó la atención lo sencillo que resulta ahora, porque con esta versión, un Bot es simplemente un servicio más que se registra en ConfigureServices en Startup.cs en una aplicación ASP\.NET Core:

Esto significa que podemos usar la inyección de dependencias estándar porque, al final, un Bot sigue siendo una aplicación ASP\.NET Core, solo que ahora no es necesario usar un controlador para manejar las peticiones.


Lo que convierte la aplicación en un bot son básicamente dos cosas:


1. Una línea en Configure:

2. Una clase que implementa la interfaz IBot, donde toda la lógica (del bot) se incluye en el método OnTurn:

Uno de los aspectos que me parece más interesante de v4, y en el que se basa este artículo, es el BotAdapter, que abstrae todos servicios de soporte necesarios para que funcione el bot y permite la creación del TestAdapter, que está incluido en el SDK y es la base de las pruebas que haremos.


TDD


También vamos a aprovechar este artículo para hacer una breve introducción práctica al proceso de desarrollo con TDD, para quien no se haya animado todavía a trabajar así, y prefiera usar (perder) tiempo en el debugger 😉.


En forma muy resumida, el proceso es:

  1. Desarrollar primero una prueba que verifique el funcionamiento de una característica y demostrar que la prueba falla porque todavía no está implementada. Esto es importante porque así "probamos la prueba".

  2. Implementar la característica, con el código más sencillo posible, de forma que la prueba pase con éxito.

  3. Refactorizar el código para mejorar claridad, mantenibilidad o reutilización, asegurando que todas las pruebas desarrolladas hasta el momento se completen con éxito.

  4. Repetir hasta que se completen todas las características requeridas.

Además, es importante que las características se desarrollen progresivamente, comenzando con lo más sencillo posible y agregando en cada iteración solo un pequeño incremento de la funcionalidad.


Por eso, para nuestro primer escenario de prueba, queremos que el bot salude al usuario por su nombre, cuando éste le salude.


Versiones utilizadas

  • Visual Studio 2017 v15.8.4

  • Microsoft.Bot.Builder.Core (4.0.1-preview)

  • Microsoft.Bot.Builder.Core.Extensions (4.0.1-preview)

  • Microsoft.Bot.Builder.Integration.AspNet.Core (4.0.1-preview)

  • FluentAssertions (5.4.1)

  • xunit (2.3.1)

  • xunit.runner.visualstudio (2.3.1)


Vista general de la aplicación TO-DO


La "aplicación" que vamos a desarrollar es muy sencilla con toda la intención y solo tendrá una entidad con tres propiedades: nombre, estatus y fecha límite.


La solución tendrá dos proyectos principales:

  • TodoApp: El back-end para manejar eventualmente la persistencia en una base de datos (en un artículo futuro).

  • TodoApp\.Bot: La interfaz de usuario tipo Bot.

Esto facilitaría la extensión de nuestra aplicación para, eventualmente, incluir una TodoApp.Mvc o TodoApp.Api que trabajen sobre la misma base de datos.


Proyecto TodoApp\.Bot


Entonces vamos a comenzar por construir el bot, siguiendo el tutorial del EchoBot, pero con un par de ajustes:

  1. Crear la carpeta src, tanto en la solución con en el filesystem y

  2. Crear el proyecto como TodoApp\.Bot

Notas:

  • Pueder ser conveniente agregar un .editorconfig al proyecto para estandarizar algunos aspectos del código (como poner en indentado en espacios 😉).

  • El proyecto se debe crear manualmente en la carpeta src del filesystem, además de crearlo en el solution folder src en VS.

También vamos a actualizar el TargetFramework a netcoreapp2.1 en el TodoApp.Bot.csproj:

Así como las versiones de los packages instalados por el template del EchoBot.


Al final nuestra solución debería resultar en algo como esto:

Y al abrir el fichero TodoApp.Bot.bot con el BotEmulator (V4 PREVIEW), debería funcionar como se explica en el tutorial:


Este es un buen momento para hacer commit (aunque seguro que no hace falta recordarlo 😉).


Proyecto TodoApp


Ahora vamos a comenzar con la aplicación de backend, creando un ClassLibrary .NET Standard que, por ahora, sólo va a contener nuestro modelo de dominio y debe resultar en algo como esto:

Crearemos TodoApp como un proyecto único, pero el primer nivel de la estructura de las carpetas sería adecuado para dividirlo en varios proyectos cuando el tamaño lo amerite. Es decir, eventualmente podríamos tener los proyectos TodoApp.Domain, TodoApp.Infrastructure, etc.


Proyecto TodoApp.Bot.UnitTests


Ahora vamos comenzar a desarrollar nuestra aplicación siguiendo un proceso Test Driven Development TDD y para esto nos vamos a apoyar en las clases de soporte para pruebas incluidas en el SDK v4.


Estas clases solo está documentadas en forma básica en el .NET API Browser, pero como el SDK es open source y está publicado en GitHub, pues simplemente vemos cómo se usan, en especial en las pruebas de diálogos.


En las pruebas del SDK podemos ver que las dos clases clave para probar un bot, desde el punto de vista que nos interesa, que es la interacción con el usuario, son TestAdapter y TestFlow. Esta clases nos permiten probar una conversación o escenario con el usuario, y por eso crearemos nuestras pruebas en la carpeta Scenarios, como veremos en un momento.


Primero vamos a renombrar las clases principales en el proyecto ToDoApp\.Bot:

  1. Renombrar la clase EchoBot a TodoBot

  2. Renombrar la clase EchoState a TodoState

Luego creamos el proyecto de pruebas .NET Core con xUnit TodoApp.Bot.UnitTests en la carpeta test y luego vamos a:

  1. Eliminar la clase de prueba inicial

  2. Crear la carpeta Scenarios

  3. Crear la clase de pruebas Bot_Should.cs y

  4. Agregar el método GreetBackTheUser_WhenUserSendsHi, para probar la primera característica que mencionamos al comienzo.

Al terminar, nuestra solución debería quedar así:

Esta forma de organizar este simplifica la identificación de las pruebas con la opción "Show Tests Hierarchy" del Test Explorer:

Ahora vamos a completar nuestra primera prueba y asegurarnos que falle, porque todavía no vamos a implementar el código en el bot.


Característica #1: El bot debe saludar al usuario


Este es el código de la primera prueba, lo más sencillo posible:

Ahora entramos en los detalles, línea por línea:

  1. En la línea 14 creamos conversationState para manejar el estado de la conversación, mantenido como una instancia de TodoState y guardado en memoria (MemoryStorage).

  2. En la línea 16 creamos el TestAdapter, usando el conversationState.

  3. En la línea 18 instanciamos nuestro bot.

  4. En la línea 20 preparamos el escenario de prueba con la clase TestFlow, que corresponde una conversación completa, con toda la secuencia de interacciones que vamos a probar. Usamos el testAdapter y pasamos el método a probar (en forma de "method group", que es equivalente a pasar una expresión lambda). En las líneas siguientes vamos a detallar las interacciones.

  5. En la línea 21 simulamos al usuario escribiendo "Hi".

  6. En la línea 22 verificamos que el bot responda "Hi User1 (#1)". User1 es el nombre genérico de usuario en el TestAdapter, así como en el Bot Emulator es "User" el "#1" lo vamos a tomar del TodoState, así como se hizo en el EchoBot original, solo para usar el conversationState.

  7. En la línea 25 ejecutamos el escenario completo.

Al ejecutar la prueba debemos obtener este resultado, que es lo esperado ahora:

Así verificamos que la prueba está funcionando correctamente, es decir, que falla cuando no se obtiene lo que esperamos.


Ahora nos toca implementar el código más sencillo posible:

Y verificar que la prueba se complete con éxito:


Característica #2: El bot debe mostrar la ayuda después del saludo


Ahora queremos que el bot muestre un texto de ayuda inmediatamente después de saludar, así que desarrollamos primero la prueba:

Y verificamos que falle:

Ahora implementamos el mínimo código necesario (después de haber definido la variable _helpText del bot):

Y verificamos que se ejecute correctamente.

Ahora vamos refactorizar las líneas comunes de las dos pruebas, para llegar a esto:

Y, por supuesto, verificamos que todas las pruebas se sigan ejecutando correctamente.


Si ahora probamos nuestro bot con el Bot Emulator, deberíamos ver esto:

Nota: Recuerde que el nombre del usuario en el Bot Emulator es User.


Característica #3: El bot debe mostrar la ayuda cuando se le pida


Nuestra prueba:

Que, por supuesto, va a fallar (no hace falta mostrarlo ahora) y entonces implementamos el mínimo código necesario para pasarla:


Característica #4: El bot debe alertar cuando no entienda un comando


Ahora lo haremos más rápido, la prueba:

En esta ocasión usamos el atributo Theory, que permite parametrizar las pruebas definiendo valores en el atributo InLineData.


El parámetro test se usa solo para facilitar la identificación del caso de prueba en el navegador:

No vamos a mostrar la implementación porque es trivial y puede ver los detalles en el repositorio si lo desea.


Característica #5: El bot debe guardar una tarea con el comando /add


Ahora necesitamos a guardar una lista de tareas y en este caso vamos a hacerlo, para simplificar el ejercicio, en el mismo estado de la conversación, para lo que vamos a modificar la clase que guarda el estado, agregando la propiedad Tasks:

En la práctica deberíamos mantener la lista de tareas en una base de datos independiente, a través del proyecto TodoApp, pero dejaremos eso para otro artículo.


Antes de comenzar con la próxima prueba, necesitamos una forma de inspeccionar el estado del bot, para verificar si las tareas se están guardando correctamente.


Como tenemos acceso al storage en el método que crea el testFlow, lo convertimos en una variable de la clase, para poder consultarla en los métodos de prueba:

Y entonces desarrollamos la primera prueba:

Llegar hasta este caso de prueba no fue trivial, así que lo explicaremos a continuación.

  1. Línea 78, usamos un overload con un lambda, que recibe la activity como parámetro, así que podemos verificar casi cualquier cosa.

  2. Línea 80, verificamos que el bot genere la respuesta indicada. Aquí usamos el paquete FluentAssertions, así que hay que instalarlo en el proyecto de pruebas.

  3. Línea 82, buscamos el estado del bot en la variable _storage. Aquí aprovechamos para aclarar que logramos llegar hasta este punto explorando el API y corriendo la prueba con el debugger. El método Read de _storage retorna un diccionario de KeyValuePair, pero para encontrar el valor de la clave a buscar fue necesario el debugger. Esto no está documentado en ninguna parte, así que podría fallar en una próxima versión. También es importante aclarar que no se pudo declarar el lambda como async, para usar await en vez de .GetAwaiter().GetResult(), porque al hacerlo, las excepciones de los assert (el .Shoud()) no llegaban hasta el método de prueba y la prueba parecía exitosa cuando no lo era. En principio esto parece un bug del BotBuilder. Esto también muestra una ventaja de verificar que la prueba falle cuando los resultados no sean los esperados.

  4. Líneas 88 y 89, creo que son suficientemente claras y no requieren mayor explicación.

  5. En principio las pruebas no se deberían basar en verificar el estado interno, porque entonces pueden fallar cuando cambien detalles de implementación pero, en este caso, también estamos explorando el BotBuilder y nos interesa conocerlo un poco más internamente 😉.

Finalmente, una implementación que hace pasar la prueba (y todas las anteriores):


Característica #6: El bot debe mostrar la lista de tareas guardadas con el comando /list


Ahora vamos a terminar la última característica que tenemos prevista para este artículo y, como siempre, comenzamos con la prueba.


En este caso será más sencilla porque no vamos a verificar el estado, porque ya sabemos que funciona, así que solo trabajaremos con las respuestas del bot:

Y ahora la nueva implementación:

Con la que pasan todas las pruebas (aunque hay que mejorarla un poco, pero eso queda como tarea 😉):

Y ahora sí podemos probarlo con el BotEmulator:


Resumen


En este artículo preparamos una estructura básica para probar las interacciones con el BotBuilder SDK v4 usando TDD, con la doble intención de explorar los aspectos básicos del SDK v4 y repasar el proceso de desarrollo TDD.


No entramos en profundidad en los detalles del SDK v4, pero el proyecto que desarrollamos nos da una buena base para explorarlos, con la tranquilidad que da trabajar con TDD.


Comentarios

Sería bueno tener aquí una sección de comentarios!

9 vistas