7.2 Unit Testing para Plugins

Escribe tests automatizados con xUnit, Moq y FakeXrmEasy

Los plugins ejecutan lógica de negocio crítica. Un bug puede causar datos corruptos, pérdida de dinero, o frustración de usuarios. Los unit tests te dan confianza de que tu código funciona correctamente, y siguen funcionando cuando haces cambios.

Objetivos de aprendizaje

  • Configurar un proyecto de tests para plugins
  • Escribir tests con xUnit y Moq
  • Usar FakeXrmEasy para simular Dataverse completo
  • Entender qué testear y qué no

El reto de testear plugins

Testear plugins es complicado porque dependen de IServiceProvider, IOrganizationService, y otros servicios de Dataverse. No puedes simplemente instanciar tu plugin y llamar Execute.

Tienes dos enfoques:

Mocking manual: Usando Moq o similar, creas mocks de cada servicio. Más control, más código de setup.

FakeXrmEasy: Una librería que simula toda la plataforma Dataverse en memoria. Menos configuración, comportamiento más realista.


Configurar el proyecto de tests


<!-- MiEmpresa.Plugins.Tests.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net462</TargetFramework>
    <IsPackable>false</IsPackable>
  </PropertyGroup>
  
  <ItemGroup>
    <PackageReference Include="xunit" Version="2.4.2" />
    <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5" />
    <PackageReference Include="Moq" Version="4.18.4" />
    <PackageReference Include="FakeXrmEasy.v9" Version="3.0.0" />
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
  </ItemGroup>
  
  <ItemGroup>
    <ProjectReference Include="..\MiEmpresa.Plugins\MiEmpresa.Plugins.csproj" />
  </ItemGroup>
</Project>

Testing con Moq: control total

Moq te permite crear mocks de interfaces y definir exactamente qué devuelven:


public class DiscountCalculatorTests
{
    [Fact]
    public void Calculate_ClienteGold_Devuelve10Porciento()
    {
        // Arrange
        var mockService = new Mock<IOrganizationService>();
        
        var cliente = new Entity("account");
        cliente["customerlevel"] = new OptionSetValue(3); // Gold
        
        var calculator = new DiscountCalculator(mockService.Object);
        
        // Act
        decimal resultado = calculator.Calculate(cliente, 5000m);
        
        // Assert
        Assert.Equal(10m, resultado);
    }
    
    [Fact]
    public void Calculate_ClientePlatinumConVolumen_Devuelve20Porciento()
    {
        // Arrange
        var mockService = new Mock<IOrganizationService>();
        
        var cliente = new Entity("account");
        cliente["customerlevel"] = new OptionSetValue(4); // Platinum
        
        var calculator = new DiscountCalculator(mockService.Object);
        
        // Act: monto > 10000 agrega 5% adicional
        decimal resultado = calculator.Calculate(cliente, 15000m);
        
        // Assert: 15% base + 5% volumen = 20%
        Assert.Equal(20m, resultado);
    }
    
    [Fact]
    public void Calculate_NuncaExcede25Porciento()
    {
        // Arrange
        var mockService = new Mock<IOrganizationService>();
        
        var cliente = new Entity("account");
        cliente["customerlevel"] = new OptionSetValue(4); // Platinum (15%)
        
        var calculator = new DiscountCalculator(mockService.Object);
        
        // Act: monto muy alto que agregaría 10% adicional
        decimal resultado = calculator.Calculate(cliente, 100000m);
        
        // Assert: capped at 25%
        Assert.Equal(25m, resultado);
    }
}

Nota que testeamos el servicio de cálculo, no el plugin directamente. Esto es intencional: la lógica de negocio está en el servicio, que es fácil de testear.


Testing con FakeXrmEasy: comportamiento realista

FakeXrmEasy simula Dataverse en memoria. Puedes crear registros, ejecutar plugins, y verificar resultados como si estuvieras contra el sistema real:


public class AccountPreCreateTests
{
    [Fact]
    public void Execute_AsignaNumeroSecuencial()
    {
        // Arrange
        var context = new XrmFakedContext();
        
        // Pre-poblar con una cuenta existente
        var cuentaExistente = new Entity("account", Guid.NewGuid());
        cuentaExistente["accountnumber"] = "ACC-1000";
        context.Initialize(new[] { cuentaExistente });
        
        // Nueva cuenta a crear
        var nuevaCuenta = new Entity("account");
        nuevaCuenta["name"] = "Contoso Corp";
        
        // Configurar contexto del plugin
        var pluginContext = context.GetDefaultPluginContext();
        pluginContext.MessageName = "Create";
        pluginContext.Stage = 20; // Pre-operation
        pluginContext.PrimaryEntityName = "account";
        pluginContext.InputParameters["Target"] = nuevaCuenta;
        
        // Act
        context.ExecutePluginWith<AccountPreCreate>(pluginContext);
        
        // Assert
        Assert.NotNull(nuevaCuenta["accountnumber"]);
        Assert.StartsWith("ACC-", nuevaCuenta["accountnumber"].ToString());
    }
    
    [Fact]
    public void Execute_NombreVacio_LanzaExcepcion()
    {
        // Arrange
        var context = new XrmFakedContext();
        
        var cuenta = new Entity("account");
        cuenta["name"] = ""; // Nombre vacío
        
        var pluginContext = context.GetDefaultPluginContext();
        pluginContext.MessageName = "Create";
        pluginContext.Stage = 10; // Pre-validation
        pluginContext.PrimaryEntityName = "account";
        pluginContext.InputParameters["Target"] = cuenta;
        
        // Act & Assert
        var exception = Assert.Throws<InvalidPluginExecutionException>(() =>
            context.ExecutePluginWith<AccountPreValidation>(pluginContext));
        
        Assert.Contains("nombre", exception.Message.ToLower());
    }
}

FakeXrmEasy también simula Retrieve, RetrieveMultiple, Create, Update, y Delete. Puedes verificar que tu plugin creó los registros esperados consultando el contexto después de la ejecución.


Qué testear y qué no

Testear:

  • Lógica de cálculo y validación
  • Flujos de decisión (if/else basados en datos)
  • Transformaciones de datos
  • Manejo de casos límite (nulls, vacíos, valores extremos)

No testear:

  • El framework de Dataverse (ya está testeado por Microsoft)
  • Funcionalidad básica de Entity o QueryExpression
  • Llamadas HTTP a servicios externos (usar mocks)

Ejecutar tests en CI/CD

En tu pipeline de Azure DevOps:


- task: VSTest@2
  inputs:
    testSelector: 'testAssemblies'
    testAssemblyVer2: '**\*.Tests.dll'
    searchFolder: '$(Build.SourcesDirectory)'
    resultsFolder: '$(Build.ArtifactStagingDirectory)/TestResults'
    codeCoverageEnabled: true

Los tests se ejecutan automáticamente en cada commit. Si alguno falla, el pipeline no avanza.


Puntos clave

  • Separa la lógica de negocio en servicios testeables independientemente
  • Moq para tests unitarios con control fino
  • FakeXrmEasy para tests de integración con Dataverse simulado
  • Testea cálculos, validaciones, y casos límite
  • Integra tests en tu pipeline de CI/CD

Para profundizar

Inicia sesión e inscríbete para guardar tu progreso.
En este curso
¿Te ha resultado útil?