Skip to content

🔄 Proceso Paso a Paso

Objetivo: Implementar paso a paso la metodología de testing, desde la creación del proyecto hasta patrones avanzados, siguiendo las prácticas validadas del equipo.


🏗️ Fase 1: Setup del Proyecto de Testing

Section titled “🏗️ Fase 1: Setup del Proyecto de Testing”
1. Right-click en Solution → Add → New Project
2. Buscar "xUnit Test Project"
3. Nombre: TuProyecto.Tests
4. Target Framework: Multiple (net8.0;net48)
5. Right-click en Dependencies → Add Project Reference
6. Seleccionar TuProyecto.Core
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- Multi-targeting para compatibilidad legacy y modern -->
<TargetFrameworks>net48;net8.0</TargetFrameworks>
<LangVersion>latest</LangVersion>
<!-- Calidad de código estricta -->
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<NoWarn>NU1803, CS1591, SA1600</NoWarn>
<IsPackable>false</IsPackable>
</PropertyGroup>
<!-- Generación automática de documentación XML -->
<PropertyGroup>
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<!-- StyleCop configuration -->
<AdditionalFiles Include="../../stylecop.json" />
<!-- Core testing packages -->
<PackageReference Include="coverlet.msbuild" Version="6.0.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<!-- Code quality -->
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<!-- Conditional packages -->
<PackageReference Include="Moq.Dapper" Version="1.0.3" Condition="'$(UseDapper)' == 'true'" />
</ItemGroup>
</Project>

Paso 1.3: Estructura de Carpetas Estándar

Section titled “Paso 1.3: Estructura de Carpetas Estándar”
📁 TuProyecto.Tests/
├── 📁 Services/ # Business logic testing
│ ├── 📄 CacheServiceTests.cs
│ ├── 📄 UserServiceTests.cs
│ └── 📄 PaymentServiceTests.cs
├── 📁 Repositories/ # Data access testing
│ ├── 📄 UserRepositoryTests.cs
│ └── 📄 ProductRepositoryTests.cs
├── 📁 Controllers/ # API endpoint testing
│ ├── 📄 UsersControllerTests.cs
│ └── 📄 ProductsControllerTests.cs
├── 📁 Helpers/ # Utilities testing
│ ├── 📄 ValidationHelperTests.cs
│ └── 📄 StringExtensionsTests.cs
├── 📁 TestData/ # Test data builders
│ ├── 📄 UserTestData.cs
│ ├── 📄 ProductTestData.cs
│ └── 📄 CacheTestData.cs
├── 📁 Extensions/ # Testing utilities
│ ├── 📄 MockLoggerExtensions.cs
│ └── 📄 MockSetupHelpers.cs
├── 📄 GlobalUsings.cs # Global using statements
└── 📄 TuProyecto.Tests.csproj
GlobalUsings.cs
global using Xunit;
global using Moq;
global using System;
global using System.Collections.Generic;
global using System.Linq;
global using System.Threading;
global using System.Threading.Tasks;
global using Microsoft.Extensions.Logging;
// Project-specific usings (opcional)
global using TuProyecto.Core.Services;
global using TuProyecto.Core.Repositories;
global using TuProyecto.Core.Models;

Estándar observado en código real del equipo:

public class CacheServiceTests
{
// 1. Mocks como readonly fields - inmutables
private readonly Mock<ICacheBackendResolver> resolverMock = new Mock<ICacheBackendResolver>();
private readonly Mock<ILogger<CacheService>> loggerMock = new Mock<ILogger<CacheService>>();
private readonly CancellationToken cancellationToken = CancellationToken.None;
// 2. SUT (System Under Test) y configuración como fields
private CacheService service;
private CacheOptions defaultOptions;
public CacheServiceTests()
{
// 3. Setup en constructor - ejecutado antes de cada test
this.defaultOptions = new CacheOptions
{
CacheType = CacheType.InMemory,
SerializerType = SerializerType.None,
ThrowOnError = true,
Expiration = new CacheExpirationOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10)
},
};
// 4. Instanciar SUT con dependencies inyectadas
this.service = new CacheService(
this.resolverMock.Object,
this.loggerMock.Object,
this.defaultOptions
);
}
}

¿Por qué este patrón funciona?

  • Estado limpio en cada test (nuevo constructor por test)
  • Configuración centralizada evita duplicación
  • Mocks inmutables previenen interferencia entre tests
  • CancellationToken reutilizable para consistency
[Fact]
public async Task GetAsync_WithValidKey_ReturnsValue()
{
// Arrange
const string key = "test-key";
const string expectedValue = "cached-value";
var backend = new Mock<ICacheBackend<object>>();
backend.Setup(x => x.GetAsync(key, this.cancellationToken))
.ReturnsAsync(expectedValue);
this.resolverMock
.Setup(x => x.GetBackend<object>(CacheType.InMemory))
.Returns(backend.Object);
// Act
var result = await this.service.GetOrSetAsync(key, () => Task.FromResult(factoryValue));
// Assert
Assert.Equal(factoryValue, result);
backend.Verify(x => x.SetAsync(
key,
factoryValue,
It.IsAny<CacheExpirationOptions>(),
It.IsAny<string[]>(),
It.IsAny<CancellationToken>()),
Times.Once);
}

Elementos clave:

  • Arrange: Setup específico, no genérico
  • Act: Una sola acción siendo probada
  • Assert: Verificación del resultado Y del comportamiento
public class UserRepositoryTests
{
private readonly Mock<IDbConnection> connectionMock = new Mock<IDbConnection>();
private readonly UserRepository repository;
public UserRepositoryTests()
{
this.repository = new UserRepository(this.connectionMock.Object);
}
[Fact]
public async Task GetByIdAsync_WithValidId_ReturnsUser()
{
// Arrange
const int userId = 1;
var expectedUser = CreateTestUser(userId);
const string expectedSql = @"
SELECT Id, Email, FirstName, LastName, IsActive, CreatedAt
FROM Users
WHERE Id = @Id";
this.connectionMock
.SetupDapperAsync(c => c.QueryFirstOrDefaultAsync<User>(
It.Is<string>(sql => NormalizeSql(sql) == NormalizeSql(expectedSql)),
It.Is<object>(p => ((dynamic)p).Id == userId),
null, null, null))
.ReturnsAsync(expectedUser);
// Act
var result = await this.repository.GetByIdAsync(userId);
// Assert
Assert.NotNull(result);
Assert.Equal(expectedUser.Id, result.Id);
Assert.Equal(expectedUser.Email, result.Email);
}
private static string NormalizeSql(string sql)
{
return sql?.Trim().Replace("\r\n", " ").Replace("\n", " ") ?? string.Empty;
}
private static User CreateTestUser(int id) => new User
{
Id = id,
Email = $"user{id}@test.com",
FirstName = $"FirstName{id}",
LastName = $"LastName{id}",
IsActive = true,
CreatedAt = DateTime.UtcNow
};
}

Elementos específicos para Dapper:

  • SetupDapperAsync para métodos Dapper
  • SQL normalization para comparación reliable
  • Parameter verification específica
  • Mock.Dapper package requerido

Patrón estándar observado en el equipo:

[MethodName]_[Scenario]_[ExpectedResult]

Ejemplos correctos:

GetAsync_WithValidKey_ReturnsValue()
GetAsync_WithInvalidKey_ThrowsArgumentException()
GetAsync_TypeMismatch_LogsWarning_ReturnsDefault()
GetAsync_BackendThrows_ThrowOnErrorFalse_LogsAndReturnsDefault()
SetAsync_NullValue_ThrowsArgumentNullException()
GetOrSetAsync_ValueInCache_ReturnsCached()
GetOrSetAsync_CacheMiss_UsesFactoryAndSets()

Elementos del naming:

  • MethodName: Método siendo probado
  • Scenario: Condición específica o input
  • ExpectedResult: Comportamiento esperado
  • Proyecto creado con xUnit template
  • .csproj configurado con multi-targeting y packages
  • Estructura de carpetas estándar creada
  • GlobalUsings.cs configurado
  • Extensions y helpers implementados
  • Test class creada siguiendo naming convention
  • Constructor setup con mocks readonly
  • Test data builders disponibles
  • Happy path cubierto
  • Edge cases identificados y probados
  • Parameter validation con Theory tests
  • Error handling ambos modos (throw/no-throw)
  • Async patterns correctamente implementados
  • Naming convention consistente
  • Arrange/Act/Assert claramente separados
  • Mock verification específica, no genérica
  • Assertions específicas sobre el resultado
  • Logging verification donde aplique
  • Todas las pruebas pasan localmente
  • Coverage mínimo alcanzado (90% líneas, 85% branches)
  • StyleCop sin warnings
  • Build exitoso en ambos frameworks

Con el proceso de implementación completado, continuamos con:

➡️ Herramientas y Scripts - Automatización completa para desarrollo y CI/CD