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