Las ventajas que te ofrece Microsoft Azure y el mundo.NET

Testeando nuestras Azure Functions

Azure Functions pone a nuestra disposición una forma maravillosa de ejecutar tareas de forma manual o desatendida en un entorno cloud y serverless. Podemos programar tareas de todo tipo en respuesta a multitud de eventos, ya sea una llamada Http, un temporizador o la subida de un documento a un storage, por poner algunos ejemplos. 

Desde el momento en el que estamos generando código para nuestras Functionsnos interesa acompañar este código de unos tests que nos aseguren que lo que estamos desarrollando funciona correctamente y que nos protejan de futuras regresiones a la hora de ampliar alguna funcionalidad. En este artículo os mostraremos cómo testear nuestras Azure Functions usando xUnit Moq en un proyecto de Functions, realizando acciones sobre una cuenta de almacenamiento con blobs en Azure. 

Tendréis todo el código subido en este repositorio de GitHub, separado en ramas para que sea más cómodo de seguir según vamos avanzando en el artículo. 

 Nuestro punto de partida 

Desde la rama “step-0-start” tenemos un proyecto de Functions listo para empezar a hacer tests sobre Azure Functions. 

Por una parte, tenemos un BlobStorageService que nos ayudará a realizar acciones con nuestra cuenta de almacenamiento y nuestros blobs. 

 BlobStorageService

BlobStorageService

Y tenemos configurado este servicio en nuestra inyección de dependencias. Esto es importante (al margen de ser una buena práctica) para poder mockear este servicio usando la librería Moq en nuestros tests. 

Por otra parte, tenemos un proyecto de Azure Functions con dos funciones: 

  • Una HttpTrigger function llamada “GetBlobsList”, que lee todos los blobs que hay en nuestro blob container y devuelve sus nombres en forma de array. 
  • Una TimeTrigger function que lee el contenido del blob “doc1.txt”, y traza por consola el contenido del archivo. 

proyecto de Azure Functions con dos funciones

Proyecto de Azure Functions con dos funciones

 ¡Empezamos! 

Vamos a añadir una nueva carpeta dentro de la solución llamada “04-Tests” y ahí añadiremos un nuevo proyecto de tests xUnit que llamaremos “TestingAzureFunctions.Tests. 

 tests xUnit

 

Después, añadiremos sobre este proyecto el paquete NuGet de Moq 

Añadimos el paquete NuGet de Moq

Añadimos el paquete NuGet de Moq

Y añadimos las referencias a nuestros otros proyectos para poder usarlos en nuestros tests. 

Añadimos referencias

Añadimos referencias

Ahora podemos eliminar la clase por defecto que nos viene en el proyecto y añadir dos nuevas clases para testear nuestras dos Functions: “GetBlobTests” y “GetBlobsListTests. 

 

“GetBlobsListTests”. 

Añadimos dos nuevas clases

 

Y, por último, vamos a añadir una factoría de tests, para poder hacer comprobaciones sobre los logs que arrojan nuestras Functions y mockear requests (tenéis el paso a paso en la documentación oficial de Microsoft y el código ya añadido en el repositorio de ejemplo del artículo).  

Factoría de test

Factoría de test

Con todo esto ya estamos listos para escribir nuestros tests y pasar a la rama “step-1-writtingTests”. 

 HttpTrigger function tests 

using Azure.Storage.Blobs;
using Microsoft.AspNetCore.Mvc;
using Moq;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Linq;
using TestingAzureFunctions.Fnt.Functions;
using TestingAzureFunctions.Services.Abstract;
using Xunit;

namespace TestingAzureFunctions.Tests
{

public class GetBlobsListTests
{

[Fact]
public async Task GetBlobsList_withData_returnArrayOfBlobNames()
{

var listLogger = (ListLogger)TestFactory.CreateLogger(LoggerTypes.List);
var req = TestFactory.CreateHttpRequest(«mockRequest», «mockRequest»);
var mockBlobService = GetMockBlobService(true);
var fnt = new GetBlobsList(mockBlobService);

var response = (OkObjectResult)await fnt.Run(req, listLogger);

string[] blobNames = response.Value as string[];
Assert.NotNull(response);
Assert.Equal(200, response.StatusCode);
Assert.NotEmpty(blobNames);
Assert.Equal(5, blobNames.Length);
Assert.Contains(«Blob list retieved with 5 blob names», listLogger.Logs[2]);

}

[Fact]
public async Task GetBlobsList_withoutData_returnEmptyArray()
{

var listLogger = (ListLogger)TestFactory.CreateLogger(LoggerTypes.List);
var req = TestFactory.CreateHttpRequest(«mockRequest», «mockRequest»);
var mockBlobService = GetMockBlobService(false);
var fnt = new GetBlobsList(mockBlobService);

var response = (OkObjectResult)await fnt.Run(req, listLogger);

string[] blobNames = response.Value as string[];
Assert.NotNull(response);
Assert.Equal(200, response.StatusCode);
Assert.Empty(blobNames);
Assert.Contains(«There are no blobs in this container», listLogger.Logs[2]);

}

[Fact]
public async Task GetBlobsList_raiseException_returnExceptionResult()
{

var listLogger = (ListLogger)TestFactory.CreateLogger(LoggerTypes.List);
var req = TestFactory.CreateHttpRequest(«mockRequest», «mockRequest»);
var mockBlobService = new Mock<IBlobStorageService>();
mockBlobService.Setup(x => x.GetBlobsListAsync(It.IsAny<BlobContainerClient>())).Throws(new Exception());
var fnt = new GetBlobsList(mockBlobService.Object);

var response = await fnt.Run(req, listLogger);

Assert.Contains(«Error getting the blob list», listLogger.Logs.Last());

}

#region Private methods

private IBlobStorageService GetMockBlobService(bool withData)
{

var mockBlobService = new Mock<IBlobStorageService>();
mockBlobService.Setup(x => x.GetBlobsListAsync(It.IsAny<BlobContainerClient>())).Returns(Task.FromResult(GetMockBlobList(withData)));
return mockBlobService.Object;

}

private List<string> GetMockBlobList(bool withData)
{

List<string> blobList = new List<string>();
if (withData)
{

blobList.Add(«doc1.txt»);
blobList.Add(«doc2.txt»);
blobList.Add(«doc3.txt»);
blobList.Add(«doc4.txt»);
blobList.Add(«doc5.txt»);

}

return blobList;

}

#endregion

}

} 

Para hacer nuestros test necesitamos un método que nos devuelve una lista de nombres de blobs “GetMockBlobList”, y un método que nos prepare un mock de nuestro IBlobStorageService “GetMockBlobService”, para poder probar todos los casos de nuestros tests. 

Ahora ya podemos escribir los tests de nuestra function “GetBlobsList y probar tres casos:

  1. Cuando nos devuelve un array de datos (GetBlobsList_withData_returnArrayOfBlobNames) 

Nos devuelcve un array de datos (GetBlobsList_withData_returnArrayOfBlobNames) 

Nos devuelve un array de datos (GetBlobsList_withData_returnArrayOfBlobNames)

2.Cuando no nos devuelve datos (GetBlobsList_withoutData_returnEmptyArray) 

No nos devuelve datos (GetBlobsList_withoutData_returnEmptyArray) 

No nos devuelve datos (GetBlobsList_withoutData_returnEmptyArray)

3. Cuando nos salta una excepción (GetBlobsList_raiseException_returnExceptionResult)

Nos salta una excepción (GetBlobsList_raiseException_returnExceptionResult)

Nos salta una excepción (GetBlobsList_raiseException_returnExceptionResult)

Para los nombres de los tests estamos siguiendo esta notación:

QueEstoyProbando_casoDePrueba_resultadoEsperado

 

TimeTrigger function tests

using Azure.Storage.Blobs.Models;
using Moq;
using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using TestingAzureFunctions.Fnt.Functions;
using TestingAzureFunctions.Services.Abstract;
using Xunit;

namespace TestingAzureFunctions.Tests
{

public class GetBlobTests
{

[Fact]
public async Task GetBlob_withData_logBlobContent()
{

var listLogger = (ListLogger)TestFactory.CreateLogger(LoggerTypes.List);
var mockBlobService = new Mock();
mockBlobService.Setup(x => x.DownloadBlobAsync()).Returns(Task.FromResult(GetMockBlob()));
var fnt = new GetBlob(mockBlobService.Object);

await fnt.Run(null, listLogger);

Assert.Contains(«Blob content: doc», listLogger.Logs[3]);
Assert.Contains(«blob content readed successfully», listLogger.Logs.Last());

}

[Fact]
public async Task GetBlob_throwException_logError()
{

var listLogger = (ListLogger)TestFactory.CreateLogger(LoggerTypes.List);
var mockBlobService = new Mock();
mockBlobService.Setup(x => x.DownloadBlobAsync()).Throws(new Exception());
var fnt = new GetBlob(mockBlobService.Object);

await fnt.Run(null, listLogger);

Assert.Contains(«Error getting the blob», listLogger.Logs.Last());

}

#region Private methods

private BlobDownloadInfo GetMockBlob()
{

byte[] firstString = Encoding.UTF8.GetBytes(«doc»);
MemoryStream str = new MemoryStream(firstString);

BlobDownloadInfo blob = BlobsModelFactory.BlobDownloadInfo(
DateTime.UtcNow, 0, BlobType.Block, null, null, null, null, null, null,
CopyStatus.Pending, null, LeaseDurationType.Infinite, null, LeaseState.Available,
null, LeaseStatus.Locked, null, null, default, 0, null, false, null, null,
null, 0, null, null, null, str);
return blob;

}

#endregion

}

}

Para hacer los tests de la function “GetBlob” nos vamos a servir de otro método “GetMockBlob” que nos devuelva un objeto “BlobDownloadInfo” para alimentar a nuestro “IBlobStorageService” mockeado.

Y podemos probar los siguientes casos:

  1. Cuando se tracea correctamente el contenido del blob (GetBlob_withData_logBlobContent)

Cuando se tracea correctamente el contenido del blob

Cuando se tracea correctamente el contenido del blob

2. Cuando nos salta una excepción (GetBlob_throwException_logError)

Cuando nos salta una excepción (GetBlob_throwException_logError)

Cuando nos salta una excepción (GetBlob_throwException_logError)

Conclusiones

Ya hemos visto cómo podemos hacer test unitarios para asegurar que nuestro código está protegido contra regresiones y funciona correctamente.

Hemos añadido una factoría de tests que nos ayuda a la hora de escribir nuestras pruebas.

Nos hemos valido de Moq para emular los casos específicos por los que pasa nuestro flujo de ejecución.

Y hemos comprobado que, en cada caso que se puede dar a lo largo de la ejecución de nuestras Azure Functions, el código se comporta exactamente como deseamos.

El código final está en la rama “step-2-finish” del repositorio, para que podáis consultar el resultado final, y ahora solo nos queda echar el respaldo de la silla para atrás y disfrutar de la maravillosa vista de todos nuestros test pasando con toda la lista en verde 😉

Maravillosa vista de ver todos nuestros test pasando con toda la lista en verde

Maravillosa vista de todos nuestros test pasando con toda la lista en verde

mm

Sobre Andrés Sánchez Robleño

Técnico superior en desarrollo de aplicaciones web y Microsoft Certified Solutions Developer. Apasionado de las tecnologías y de como usarlas para dar soluciones a problemas complejos. Disfruta de aprender cada día algo nuevo y de aportar todo su valor en su actual puesto como Cloud Solutions Developer en ENCAMINA. Dando pasos de bebé para luego poder dar pasos de gigante.
Esta entrada ha sido publicada en Azure Functions y etiquetada como , , . Enlace permanente .
Suscríbete a Piensa en Sofware desarrolla en Colores

Suscríbete a Piensa en Sofware desarrolla en Colores

Recibe todas las actualizaciones semanalmente de nuestro blog

You have Successfully Subscribed!

ENCAMINA, piensa en colores