0

Como hacer un mock en Minimal API

Básicamente Mock, o Mockear como decimos en Argentina, es hacer que un método o servicio responda con datos falsos que no permitan probar nuestra lógica de negocio en las pruebas.

Por otro lado, también podemos hacerlas para que otros equipos puedan seguir desarrollándose sin problema. Supongamos que el equipo que tiene que realizar las API no está disponible porque se encuentra realizando otra tarea. Fácilmente podremos simular el comportamiento para construir lo que necesitamos sin esperar que esté lista.

¡Comencemos!

Vamos a crear un archivo en JSON, que contenga una estructura con los datos de respuesta. Más o menos tendremos la siguiente forma:

{
    "Customers":[
        {
            "Id": 1,
            "Name":  "Homero Simpsons"
        },
        {
            "Id": 2,
            "Name":  "Barny Gomez"
        },
        {
            "Id": 3,
            "Name":  "Moe Sislack"
        }
    ]
}

Implementaremos GET, POST, PUT y DELETE cumpliendo todo lo relacionado con RESTful. Este es el punto de partida en alto nivel de lo que necesitamos y cómo será el comportamiento de las cosas.

Ahora vamos a crear en Visual Studio 2022 nuestro proyecto 

recuerda seleccionar la opción “Usar Controladores (desactivar para usar API mínimas”.

Si abrimos nuestro programs.cs veremos que esta el app.MapGet que no indica para una cierta ruta a la cual el verbo le llega un GET responderá. Existen los demás tipos de llamadas MapPost(), MapPut() o MapDelete().

Para que quede todo desacoplado vamos a crear un Middleware. Crearemos una extensión para detectar los routers lo llamaremos RouteMiddlewareExtensions. Vamos a crear un archivo nuevo de clase y lo completamos con el siguiente código.

using System.Text.Json;
using System.Text.Json.Nodes;

   public static class RouteMiddlewareExtensions
   {
      public static WebApplication UseExtraRoutes(this WebApplication app)
      { 
      }
   }

Por otro lado, vamos a agregar un archivo que se llame fake.json con la estructura de datos que planteamos al inicio.

Lo primero que haremos es agregar el Método Get. Para mapear las respuestas debemos recorrer el objeto app que es de tipo WebApplication. Esto hará que las rutas de configuraciones se relacionen con los tipos de respuestas esperados. Agregamos el siguiente código para el Get.

var writableDoc = JsonNode.Parse(File.ReadAllText("fake.json"));

 foreach(var elem in writableDoc?.Root.AsObject().AsEnumerable()) {
      var arr = elem.Value.AsArray();
      app.MapGet(string.Format("/{0}", elem.Key), () => elem.Value.ToString());
   }

Luego iremos a nuestro program.cs y agregaremos:

app.UseExtraRoutes();

Lo ejecutamos presionando y veremos que tenemos un método a partir de la estructura definida y que el resultado será el siguiente:

Ahora vamos un paso más, vamos a agregar un Get que pueda filtrar por medio del Id. Vamos nuevamente a nuestro archivo RouteMiddlewareExtensions.cs y agregaremos el siguiente código debajo del anterior Get:

 app.MapGet(string.Format("/{0}", elem.Key) + "/{id}", (int id) =>
      {
        var matchedItem = arr.SingleOrDefault(row => row
          .AsObject()
          .Any(o => o.Key.ToLower() == "id" && int.Parse(o.Value.ToString()) == id)
        );
        return matchedItem;
      });

Veamos los resultados de la ejecución:

El siguiente paso es agregar un post. Para esto vamos a necesitar escribir en nuestro archivo fake.json con lo que deseamos guardar y podés mantener. Para esto debemos escribir en el archivo utilizando System.IO para leer la estructura y volver a escribir el archivo. Veamos el código:

app.MapPost(string.Format("/{0}", elem.Key), async (HttpRequest request) => {
        string content = string.Empty;
        using(StreamReader reader = new StreamReader(request.Body))
        {
          content = await reader.ReadToEndAsync();
        }
        var newNode = JsonNode.Parse(content);
        var array = elem.Value.AsArray();
        newNode.AsObject().Add("Id", array.Count() + 1);
        array.Add(newNode);

        File.WriteAllText("mock.json", writableDoc.ToString());
        return content;
      });

Nuevamente vamos a nuestro archivo RouteMiddlewareExtensions.cs y agregaremos el siguiente código anterior debajo de Get por el id.

Próximo a implementar Delete, que es una actualización. Muy similar al anterior, pero tendremos Map Delete. Veamos el código que debemos agregar debajo del MapPost;

    app.MapDelete(string.Format("/{0}", elem.Key) + "/{id}", (int id) => {

        var matchedItem = arr
         .Select((value, index) => new{ value, index})
         .SingleOrDefault(row => row.value
          .AsObject()
          .Any(o => o.Key.ToLower() == "id" && int.Parse(o.Value.ToString()) == id)
        );
        if (matchedItem != null) {
          arr.RemoveAt(matchedItem.index);
          File.WriteAllText("mock.json", writableDoc.ToString());
        }

        return "OK";
      });

Ahora les dejo como tarea para el hogar la creación de PUT. No debería ser tan difícil con los ejemplos que vimos hasta ahora. Truco, es muy muy similar al POST. Les dejo el código completo:

using System.Text.Json;
using System.Text.Json.Nodes;

namespace mock;
public static class RouteMiddlewareExtensions
{   
    public static WebApplication UseExtraRoutes(this WebApplication app)
    {
        var writableDoc = JsonNode.Parse(File.ReadAllText("fake.json"));

        foreach (var elem in writableDoc?.Root.AsObject().AsEnumerable())
        {
            var arr = elem.Value.AsArray();

            app.MapGet(string.Format("/{0}", elem.Key), () => elem.Value.ToString());

            app.MapGet(string.Format("/{0}", elem.Key) + "/{id}", (int id) =>
            {
                var matchedItem = arr.SingleOrDefault(row => row
                  .AsObject()
                  .Any(o => o.Key.ToLower() == "id" && int.Parse(o.Value.ToString()) == id)
                );
                return matchedItem;
            });

            app.MapPost(string.Format("/{0}", elem.Key), async (HttpRequest request) => {
                string content = string.Empty;
                using (StreamReader reader = new StreamReader(request.Body))
                {
                    content = await reader.ReadToEndAsync();
                }
                var newNode = JsonNode.Parse(content);
                var array = elem.Value.AsArray();
                newNode.AsObject().Add("Id", array.Count() + 1);
                array.Add(newNode);

                File.WriteAllText("mock.json", writableDoc.ToString());
                return content;
            });

            app.MapDelete(string.Format("/{0}", elem.Key) + "/{id}", (int id) => {

                var matchedItem = arr
                 .Select((value, index) => new { value, index })
                 .SingleOrDefault(row => row.value
                  .AsObject()
                  .Any(o => o.Key.ToLower() == "id" && int.Parse(o.Value.ToString()) == id)
                );
                if (matchedItem != null)
                {
                    arr.RemoveAt(matchedItem.index);
                    File.WriteAllText("mock.json", writableDoc.ToString());
                }

                return "OK";
            });

        }

        return app;
    }
}

Conclusiones

Esta opción es una buena oportunidad de crear API sin la necesidad de que implementen la lógica de negocio completa. Por ende, sería muy útil para no esperar a ningún equipo que lo tenga que desarrollar si sabemos que estructuras vamos a recuperar. Espero que les sea útil. Nos vemos en próximos posts.

Fernando Sonego

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *