0

Mocks en EntityFramework

Existen varios caminos para probar una aplicación que tenga como base Entity Framework Core. El camino más utilizado es consumir directamente la base de datos garantizando que en producción va funcionar correctamente. Por otro lado, no es una buena manera de probar consumiendo directamente de la base de datos, un mejor camino es probar con datos falsos las funcionalidades.

Lo primero que debemos implementar es el Patrón repositorio que nos facilitara la tarea de utilizar datos falsos para realizar las pruebas y similar la capa de acceso a datos. Implementando el patrón, también tendremos una separación clara entre la capa de datos y la lógica de negocios de la aplicación simplificando las pruebas a realizar.

Otro camino, si no deseamos implementar el patrón repositorio en nuestra aplicación, podemos usar un proveedor de EF fake para SQLite, In-Memory u otros para suplantar el DbContext. Para suplantar el DbContext existen 2 librerías que son las más utilizadas y soportadas por la comunidad: Moq.EntityFrameworkCore y MockQueryable. Las utilizaremos en nuestro proyecto.

En mi caso, estoy utilizando para todos los demás la base de datos AventureWorks para Microsoft SQL Server. Esta base de datos está completa lista para usar con registros completos y puede descargarla desde esta url. También utilizó Scaldfolding para crear mi DbContext y todas las entidades con el siguiente comando:

dotnet ef dbcontext scaffold "[cnnConnectionsStrings]}" Microsoft.EntityFrameworkCore.SqlServer -o Models

Luego de crear los modelos en nuestro proyecto, puedes dirigirte a la carpeta modelos y ver la clase Products que será la que utilizaremos en este ejemplo:

using System;
using System.Collections.Generic;

namespace DemoMobile.api.Models;

/// <summary>
/// Products sold or used in the manfacturing of sold products.
/// </summary>
public partial class Product
{
    /// <summary>
    /// Primary key for Product records.
    /// </summary>
    public int ProductId { get; set; }

..... (el código es bastante extenso)

Ahora es el momento de crear nuestro controlador que utiliza Products para servir los datos de la base de datos. Será un controller bastante simple:

public class ProductsController : ControllerBase
{
    private readonly ProductDBContext _context;
    public ProductsController(AdventureWorksContext context)
    {
        _context = context;
    }
    // GET: api/Products
    [HttpGet]
    public async Task<ActionResult<IEnumerable<Product>>> GetProducts()
    {
        return await _context.Products.ToListAsync();
    }
    // GET: api/Products/5
    [HttpGet("{id}")]
    public async Task<ActionResult<Product>> GetProductById(int id)
    {
        var Product = await _context.Products.FindAsync(id);
        if (Product is null)
        {
            return NotFound();
        }
        return Product;
    }
}

En nuestro controlador tenemos 2 métodos: GetProducts(), que retorna todos los productos, y GetProductById() que retorna un producto a partir de su Id.

Para poder hacer un fake de DbContext utilizaremos la librería Moq.EntityFrameworkCore. La instalaremos por medio de la consola de administración de paquetes que está incluida en Visual Studio 2022.

Install-Package Moq.EntityFrameworkCore

Momento de crear el los test, primero el test para GetProducts:

[Fact]
public async Task GetProducts_WhenCalled_ReturnsProductListAsync()
{
    // Arrange
    var ProductContextMock = new Mock<AdventureWorksContext>();
    ProductContextMock.Setup<DbSet<Product>>(x => x.Products)
        .ReturnsDbSet(TestDataHelper.GetFakeProductList());
    //Act
    ProductsController ProductsController = new(ProductContextMock.Object);
    var Products = (await ProductsController.GetProducts()).Value;
    //Assert
    Assert.NotNull(Products);
    Assert.Equal(2, Products.Count());
}

Para poder hacer un fake correcto, debemos armar un método que nos devuelva una lista. Veamos como será:

private static List<Product> GetFakeProductList()
{
    return new List<Product>()
    {
        new Product
        {
            ProductId = 1,
            Name = "Producto 1",
            ProductNumber = "P0001",
            Color = "Red"
        },
        new Product
        {
            ProductId = 2,
            Name = "Producto 2",
            ProductNumber = "P0002",
            Color = "Blue"
        }
    };
}

Lo siguiente a probar es el método GetProductById(). Utilizaremos el método FindAsync() para obtener un registro y sortear el método.

[Fact]
public async Task GetProductById_WhenCalled_ReturnsProductAsync()
{
    // Arrange            
    var ProductContextMock = new Mock<AdventureWorksContext>();
    ProductContextMock.Setup(x => x.Products.FindAsync(1).Result)
        .Returns(TestDataHelper.GetFakeProductList().Find(e => e.Id == 1) ?? new Product());

    //Act
    ProductsController ProductsController = new(ProductContextMock.Object);
    var Product = (await ProductsController.GetProductById(1)).Value;

    //Assert
    Assert.NotNull(Product);
    Assert.Equal(1, Product.Id);
}

Tos listo para poder hacer fakes de las pruebas tanto para el método GetProducts() y GetProductByID(). Es el momento de agregar la librería MockQueryable que no ayudará a hacer un fake de ToListAsync(), FindAsync() y todos los demás. Para instalarlo nuevamente debemos ir a la venta de administración de paquetes y ejecutaremos el siguiente script:

Install-Package MockQueryable.Moq

Modificaremos nuestros test para agregar el soporte de la nueva librería. Perimteo GetProdcuts.

[Fact]
public async Task GetProducts_WhenCalled_ReturnsProductListAsync()
{
    // Arrange
    var mock = TestDataHelper.GetFakeProductList().BuildMock().BuildMockDbSet();
    var ProductContextMock = new Mock<AdventureWorksContext>();
    ProductContextMock.Setup(x => x.Products).Returns(mock.Object);
    //Act
    ProductsController ProductsController = new(ProductContextMock.Object);
    var Products = (await ProductsController.GetProducts()).Value;
    //Assert
    Assert.NotNull(Products);
    Assert.Equal(2, Products.Count());
}

Ahora modificaremos GetProductById:

[Fact]
public async Task GetProductById_WhenCalled_ReturnsProductAsync()
{
    // Arrange
    var mock = TestDataHelper.GetFakeProductList().BuildMock().BuildMockDbSet();
    mock.Setup(x => x.FindAsync(1)).ReturnsAsync(
        TestDataHelper.GetFakeProductList().Find(e => e.Id == 1));

    var ProductContextMock = new Mock<AdventureWorksContext>();
    ProductContextMock.Setup(x => x.Products)
        .Returns(mock.Object);

    //Act
    ProductsController ProductsController = new(ProductContextMock.Object);
    var Product = (await ProductsController.GetProductById(1)).Value;

    //Assert
    Assert.NotNull(Product);
    Assert.Equal(1, Product.Id);
}

Conclusiones

En el post usamos Moq.EntityFramworkCore para hacer un fake del contexto de la base de datos en una primera etapa para poder hackear DBSet, DBQuery desde el contexto. MockQueryable nos proporcionó la capacidad de hacer fácil a métodos de EF como FinsAsync, ToListAsync(), etc.  Es posible utilizarlo con otras librerías como: Moq, NSubstitute y FakeItEasy. Espero que les sea de utilidad.

Fernando Sonego

Deja una respuesta

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