Lógica del controlador de pruebas unitarias en ASP.NET Core
Por Steve Smith
Las pruebas unitarias implican probar una parte de una aplicación de forma aislada con respecto a su infraestructura y dependencias. Cuando se realizan pruebas unitarias de la lógica de controlador, solo se comprueba el contenido de una única acción, no el comportamiento de sus dependencias o del marco en sí.
Controladores de pruebas unitarias
Configure pruebas unitarias de las acciones del controlador para centrarse en el comportamiento del controlador. Una prueba unitaria del controlador evita escenarios como filtros, enrutamiento y enlace de modelos. Las pruebas que abarcan las interacciones entre los componentes que colectivamente responden a una solicitud se controlan mediante pruebas de integración. Para más información sobre pruebas de integración, vea Pruebas de integración en ASP.NET Core.
Si va a escribir filtros personalizados y rutas, realice pruebas unitarias en ellos de forma aislada, no como parte de las pruebas de una acción de controlador concreta.
Para demostrar las pruebas unitarias del controlador, revise el siguiente controlador en la aplicación de ejemplo.
Vea o descargue el código de ejemplo (cómo descargarlo)
El controlador Home muestra una lista de sesiones de lluvia de ideas y permite crear nuevas sesiones de lluvia de ideas con una solicitud POST:
public class HomeController : Controller
{
private readonly IBrainstormSessionRepository _sessionRepository;
public HomeController(IBrainstormSessionRepository sessionRepository)
{
_sessionRepository = sessionRepository;
}
public async Task<IActionResult> Index()
{
var sessionList = await _sessionRepository.ListAsync();
var model = sessionList.Select(session => new StormSessionViewModel()
{
Id = session.Id,
DateCreated = session.DateCreated,
Name = session.Name,
IdeaCount = session.Ideas.Count
});
return View(model);
}
public class NewSessionModel
{
[Required]
public string SessionName { get; set; }
}
[HttpPost]
public async Task<IActionResult> Index(NewSessionModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
else
{
await _sessionRepository.AddAsync(new BrainstormSession()
{
DateCreated = DateTimeOffset.Now,
Name = model.SessionName
});
}
return RedirectToAction(actionName: nameof(Index));
}
}
El controlador anterior:
- Sigue el Principio de dependencias explícitas.
- Espera la inserción de dependencias (DI) para ofrecer una instancia de
IBrainstormSessionRepository
. - Se puede probar con un servicio
IBrainstormSessionRepository
ficticio con el uso de un marco de objeto ficticio, como Moq. Un objeto ficticio es un objeto fabricado con un conjunto predeterminado de comportamientos de propiedades y métodos utilizados para las pruebas. Para más información, vea Introducción a las pruebas de integración.
El método HTTP GET Index
no tiene bucles ni bifurcaciones y solamente llama a un método. La prueba unitaria para esta acción:
- Realice el simulacro del servicio
IBrainstormSessionRepository
mediante el métodoGetTestSessions
.GetTestSessions
crea dos sesiones de lluvia de ideas ficticias con fechas y nombres de sesión. - Ejecuta el método
Index
. - Realiza aserciones sobre el resultado devuelto por el método:
- Se devuelve ViewResult.
- ViewDataDictionary.Model es
StormSessionViewModel
. - Hay dos sesiones de lluvia de ideas almacenadas en
ViewDataDictionary.Model
.
[Fact]
public async Task Index_ReturnsAViewResult_WithAListOfBrainstormSessions()
{
// Arrange
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.ListAsync())
.ReturnsAsync(GetTestSessions());
var controller = new HomeController(mockRepo.Object);
// Act
var result = await controller.Index();
// Assert
var viewResult = Assert.IsType<ViewResult>(result);
var model = Assert.IsAssignableFrom<IEnumerable<StormSessionViewModel>>(
viewResult.ViewData.Model);
Assert.Equal(2, model.Count());
}
private List<BrainstormSession> GetTestSessions()
{
var sessions = new List<BrainstormSession>();
sessions.Add(new BrainstormSession()
{
DateCreated = new DateTime(2016, 7, 2),
Id = 1,
Name = "Test One"
});
sessions.Add(new BrainstormSession()
{
DateCreated = new DateTime(2016, 7, 1),
Id = 2,
Name = "Test Two"
});
return sessions;
}
Las pruebas del método HTTP POST Index
del controlador de Home verifican que:
- Cuando ModelState.IsValid es
false
, el método de acción devuelve un solicitud incorrecta 400 ViewResult con los datos adecuados. - Cuando
ModelState.IsValid
estrue
:- Se llama al método
Add
del repositorio. - Se devuelve RedirectToActionResult con los argumentos correctos.
- Se llama al método
Un estado de modelo no válido se comprueba mediante la adición de errores con AddModelError, como se muestra en la primera prueba de abajo:
[Fact]
public async Task IndexPost_ReturnsBadRequestResult_WhenModelStateIsInvalid()
{
// Arrange
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.ListAsync())
.ReturnsAsync(GetTestSessions());
var controller = new HomeController(mockRepo.Object);
controller.ModelState.AddModelError("SessionName", "Required");
var newSession = new HomeController.NewSessionModel();
// Act
var result = await controller.Index(newSession);
// Assert
var badRequestResult = Assert.IsType<BadRequestObjectResult>(result);
Assert.IsType<SerializableError>(badRequestResult.Value);
}
[Fact]
public async Task IndexPost_ReturnsARedirectAndAddsSession_WhenModelStateIsValid()
{
// Arrange
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.AddAsync(It.IsAny<BrainstormSession>()))
.Returns(Task.CompletedTask)
.Verifiable();
var controller = new HomeController(mockRepo.Object);
var newSession = new HomeController.NewSessionModel()
{
SessionName = "Test Name"
};
// Act
var result = await controller.Index(newSession);
// Assert
var redirectToActionResult = Assert.IsType<RedirectToActionResult>(result);
Assert.Null(redirectToActionResult.ControllerName);
Assert.Equal("Index", redirectToActionResult.ActionName);
mockRepo.Verify();
}
Si ModelState no es válido, se devuelve el mismo elemento ViewResult
que para una solicitud GET. La prueba no intenta pasar un modelo no válido. Pasar un modelo no válido no es un enfoque válido, ya que el enlace de modelos no está en ejecución (aunque una prueba de integración usa el enlace de modelos). En este caso, no se comprueba el enlace de modelos. Estas pruebas unitarias solo comprueban el código del método de acción.
La segunda prueba verifica que, cuando ModelState
es válido:
- Se agrega un nuevo elemento
BrainstormSession
(mediante el repositorio). - El método devuelve un elemento
RedirectToActionResult
con las propiedades esperadas.
Las llamadas ficticias que no se efectúan se suelen ignorar, aunque llamar a Verifiable
al final de la llamada de configuración permite realizar una validación ficticia de la prueba. Esto se realiza con una llamada a mockRepo.Verify
, que producirá un error en la prueba si no se ha llamado al método esperado.
Nota:
La biblioteca Moq usada en este ejemplo permite mezclar fácilmente objetos ficticios comprobables o "estrictos" con objetos ficticios no comprobables (también denominados "flexibles" o stub). Obtenga más información sobre cómo personalizar el comportamiento de objetos ficticios con Moq.
SessionController en la aplicación de ejemplo muestra información relacionada con una sesión de lluvia de ideas determinada. El controlador incluye lógica para tratar valores id
no válidos (hay dos escenarios return
en el ejemplo siguiente para abarcar estos escenarios). La instrucción return
final devuelve un nuevo elemento StormSessionViewModel
en la vista (Controllers/SessionController.cs
):
public class SessionController : Controller
{
private readonly IBrainstormSessionRepository _sessionRepository;
public SessionController(IBrainstormSessionRepository sessionRepository)
{
_sessionRepository = sessionRepository;
}
public async Task<IActionResult> Index(int? id)
{
if (!id.HasValue)
{
return RedirectToAction(actionName: nameof(Index),
controllerName: "Home");
}
var session = await _sessionRepository.GetByIdAsync(id.Value);
if (session == null)
{
return Content("Session not found.");
}
var viewModel = new StormSessionViewModel()
{
DateCreated = session.DateCreated,
Name = session.Name,
Id = session.Id
};
return View(viewModel);
}
}
Las pruebas unitarias incluyen una prueba de cada escenario return
en la acción Index
del controlador de sesión:
[Fact]
public async Task IndexReturnsARedirectToIndexHomeWhenIdIsNull()
{
// Arrange
var controller = new SessionController(sessionRepository: null);
// Act
var result = await controller.Index(id: null);
// Assert
var redirectToActionResult =
Assert.IsType<RedirectToActionResult>(result);
Assert.Equal("Home", redirectToActionResult.ControllerName);
Assert.Equal("Index", redirectToActionResult.ActionName);
}
[Fact]
public async Task IndexReturnsContentWithSessionNotFoundWhenSessionNotFound()
{
// Arrange
int testSessionId = 1;
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync((BrainstormSession)null);
var controller = new SessionController(mockRepo.Object);
// Act
var result = await controller.Index(testSessionId);
// Assert
var contentResult = Assert.IsType<ContentResult>(result);
Assert.Equal("Session not found.", contentResult.Content);
}
[Fact]
public async Task IndexReturnsViewResultWithStormSessionViewModel()
{
// Arrange
int testSessionId = 1;
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync(GetTestSessions().FirstOrDefault(
s => s.Id == testSessionId));
var controller = new SessionController(mockRepo.Object);
// Act
var result = await controller.Index(testSessionId);
// Assert
var viewResult = Assert.IsType<ViewResult>(result);
var model = Assert.IsType<StormSessionViewModel>(
viewResult.ViewData.Model);
Assert.Equal("Test One", model.Name);
Assert.Equal(2, model.DateCreated.Day);
Assert.Equal(testSessionId, model.Id);
}
Al pasar al controlador de ideas, la aplicación expone la funcionalidad como una API web en la ruta api/ideas
:
- El método
ForSession
devuelve una lista de ideas (IdeaDTO
) asociadas con una sesión de lluvia de ideas. - El método
Create
agrega nuevas ideas a una sesión.
[HttpGet("forsession/{sessionId}")]
public async Task<IActionResult> ForSession(int sessionId)
{
var session = await _sessionRepository.GetByIdAsync(sessionId);
if (session == null)
{
return NotFound(sessionId);
}
var result = session.Ideas.Select(idea => new IdeaDTO()
{
Id = idea.Id,
Name = idea.Name,
Description = idea.Description,
DateCreated = idea.DateCreated
}).ToList();
return Ok(result);
}
[HttpPost("create")]
public async Task<IActionResult> Create([FromBody]NewIdeaModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var session = await _sessionRepository.GetByIdAsync(model.SessionId);
if (session == null)
{
return NotFound(model.SessionId);
}
var idea = new Idea()
{
DateCreated = DateTimeOffset.Now,
Description = model.Description,
Name = model.Name
};
session.AddIdea(idea);
await _sessionRepository.UpdateAsync(session);
return Ok(session);
}
Evite devolver entidades de dominio de negocio directamente mediante llamadas API. Entidades de dominio:
- Suelen incluir más datos de los que necesita el cliente.
- Innecesariamente acoplan el modelo de dominio interno de la aplicación con la API expuesta públicamente.
La asignación entre las entidades de dominio y los tipos devueltos al cliente se puede realizar:
- Manualmente, con una operación
Select
de LINQ, como la aplicación de ejemplo usa. Para más información, vea LINQ (Language Integrated Query). - Automáticamente, con una biblioteca, como AutoMapper.
A continuación, la aplicación de ejemplo muestra las pruebas unitarias para los métodos de API Create
y ForSession
del controlador de ideas.
La aplicación de ejemplo contiene dos pruebas ForSession
. La primera prueba determina si ForSession
devuelve NotFoundObjectResult (HTTP no encontrado) para una sesión no válida:
[Fact]
public async Task ForSession_ReturnsHttpNotFound_ForInvalidSession()
{
// Arrange
int testSessionId = 123;
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync((BrainstormSession)null);
var controller = new IdeasController(mockRepo.Object);
// Act
var result = await controller.ForSession(testSessionId);
// Assert
var notFoundObjectResult = Assert.IsType<NotFoundObjectResult>(result);
Assert.Equal(testSessionId, notFoundObjectResult.Value);
}
La segunda prueba ForSession
determina si ForSession
devuelve una lista de ideas de sesión (<List<IdeaDTO>>
) para una sesión válida. Las comprobaciones también examinan la primera idea para confirmar si su propiedad Name
es correcta:
[Fact]
public async Task ForSession_ReturnsIdeasForSession()
{
// Arrange
int testSessionId = 123;
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync(GetTestSession());
var controller = new IdeasController(mockRepo.Object);
// Act
var result = await controller.ForSession(testSessionId);
// Assert
var okResult = Assert.IsType<OkObjectResult>(result);
var returnValue = Assert.IsType<List<IdeaDTO>>(okResult.Value);
var idea = returnValue.FirstOrDefault();
Assert.Equal("One", idea.Name);
}
Para comprobar el comportamiento del método Create
cuando ModelState
no es válido, la aplicación de ejemplo agrega un error de modelo al controlador como parte de la prueba. No intente probar la validación del modelo o el enlace de modelos en las pruebas unitarias; céntrese en probar el comportamiento del método de acción al confrontarlo con un valor de ModelState
no válido:
[Fact]
public async Task Create_ReturnsBadRequest_GivenInvalidModel()
{
// Arrange & Act
var mockRepo = new Mock<IBrainstormSessionRepository>();
var controller = new IdeasController(mockRepo.Object);
controller.ModelState.AddModelError("error", "some error");
// Act
var result = await controller.Create(model: null);
// Assert
Assert.IsType<BadRequestObjectResult>(result);
}
La segunda prueba de Create
depende de que el repositorio devuelva null
, por lo que el repositorio ficticio está configurado para devolver null
. No es necesario crear una base de datos de prueba (en memoria o de cualquier otro modo) ni crear una consulta que devuelva este resultado. La prueba puede realizarse en una sola instrucción, como se muestra en el código de ejemplo:
[Fact]
public async Task Create_ReturnsHttpNotFound_ForInvalidSession()
{
// Arrange
int testSessionId = 123;
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync((BrainstormSession)null);
var controller = new IdeasController(mockRepo.Object);
// Act
var result = await controller.Create(new NewIdeaModel());
// Assert
Assert.IsType<NotFoundObjectResult>(result);
}
La tercera prueba de Create
, Create_ReturnsNewlyCreatedIdeaForSession
, verifica que se llama al método UpdateAsync
del repositorio. Se llama al objeto ficticio con Verifiable
y, después, se llama al método Verify
del repositorio ficticio para confirmar que se ejecutó el método Verifiable. La prueba unitaria no es responsable de garantizar que el método UpdateAsync
guarda los datos; esto se puede realizar con una prueba de integración.
[Fact]
public async Task Create_ReturnsNewlyCreatedIdeaForSession()
{
// Arrange
int testSessionId = 123;
string testName = "test name";
string testDescription = "test description";
var testSession = GetTestSession();
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync(testSession);
var controller = new IdeasController(mockRepo.Object);
var newIdea = new NewIdeaModel()
{
Description = testDescription,
Name = testName,
SessionId = testSessionId
};
mockRepo.Setup(repo => repo.UpdateAsync(testSession))
.Returns(Task.CompletedTask)
.Verifiable();
// Act
var result = await controller.Create(newIdea);
// Assert
var okResult = Assert.IsType<OkObjectResult>(result);
var returnSession = Assert.IsType<BrainstormSession>(okResult.Value);
mockRepo.Verify();
Assert.Equal(2, returnSession.Ideas.Count());
Assert.Equal(testName, returnSession.Ideas.LastOrDefault().Name);
Assert.Equal(testDescription, returnSession.Ideas.LastOrDefault().Description);
}
Probar ActionResult<T>
ActionResult<T> (ActionResult<TValue>) puede devolver un tipo que se deriva de ActionResult
o bien un tipo específico.
La aplicación de ejemplo incluye un método que devuelve un resultado List<IdeaDTO>
para una sesión determinada id
. Si la sesión id
no existe, el controlador devuelve NotFound:
[HttpGet("forsessionactionresult/{sessionId}")]
[ProducesResponseType(200)]
[ProducesResponseType(404)]
public async Task<ActionResult<List<IdeaDTO>>> ForSessionActionResult(int sessionId)
{
var session = await _sessionRepository.GetByIdAsync(sessionId);
if (session == null)
{
return NotFound(sessionId);
}
var result = session.Ideas.Select(idea => new IdeaDTO()
{
Id = idea.Id,
Name = idea.Name,
Description = idea.Description,
DateCreated = idea.DateCreated
}).ToList();
return result;
}
Dos pruebas del controlador ForSessionActionResult
se incluyen en ApiIdeasControllerTests
.
La primera prueba confirma que el controlador devuelve ActionResult
, pero no una lista inexistente de ideas para una sesión inexistente id
:
- El tipo
ActionResult
esActionResult<List<IdeaDTO>>
. - Result es NotFoundObjectResult.
[Fact]
public async Task ForSessionActionResult_ReturnsNotFoundObjectResultForNonexistentSession()
{
// Arrange
var mockRepo = new Mock<IBrainstormSessionRepository>();
var controller = new IdeasController(mockRepo.Object);
var nonExistentSessionId = 999;
// Act
var result = await controller.ForSessionActionResult(nonExistentSessionId);
// Assert
var actionResult = Assert.IsType<ActionResult<List<IdeaDTO>>>(result);
Assert.IsType<NotFoundObjectResult>(actionResult.Result);
}
Para obtener un elemento id
de sesión válido, la segunda prueba confirma que el método devuelve:
- Un
ActionResult
con un tipoList<IdeaDTO>
. - ActionResult<T>.Value es de un tipo
List<IdeaDTO>
. - El primer elemento de la lista es una idea válida que coincide con la idea almacenada en la sesión ficticia (obtenido mediante una llamada a
GetTestSession
).
[Fact]
public async Task ForSessionActionResult_ReturnsIdeasForSession()
{
// Arrange
int testSessionId = 123;
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync(GetTestSession());
var controller = new IdeasController(mockRepo.Object);
// Act
var result = await controller.ForSessionActionResult(testSessionId);
// Assert
var actionResult = Assert.IsType<ActionResult<List<IdeaDTO>>>(result);
var returnValue = Assert.IsType<List<IdeaDTO>>(actionResult.Value);
var idea = returnValue.FirstOrDefault();
Assert.Equal("One", idea.Name);
}
La aplicación de ejemplo también incluye un método para crear un nuevo elemento Idea
para una sesión determinada. El controlador devuelve:
- BadRequest para un modelo no válido.
- NotFound si no existe la sesión.
- CreatedAtAction cuando se actualiza la sesión con la idea nueva.
[HttpPost("createactionresult")]
[ProducesResponseType(201)]
[ProducesResponseType(400)]
[ProducesResponseType(404)]
public async Task<ActionResult<BrainstormSession>> CreateActionResult([FromBody]NewIdeaModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var session = await _sessionRepository.GetByIdAsync(model.SessionId);
if (session == null)
{
return NotFound(model.SessionId);
}
var idea = new Idea()
{
DateCreated = DateTimeOffset.Now,
Description = model.Description,
Name = model.Name
};
session.AddIdea(idea);
await _sessionRepository.UpdateAsync(session);
return CreatedAtAction(nameof(CreateActionResult), new { id = session.Id }, session);
}
Tres pruebas de CreateActionResult
se incluyen en ApiIdeasControllerTests
.
El primer texto confirma que se devuelve BadRequest para un modelo no válido.
[Fact]
public async Task CreateActionResult_ReturnsBadRequest_GivenInvalidModel()
{
// Arrange & Act
var mockRepo = new Mock<IBrainstormSessionRepository>();
var controller = new IdeasController(mockRepo.Object);
controller.ModelState.AddModelError("error", "some error");
// Act
var result = await controller.CreateActionResult(model: null);
// Assert
var actionResult = Assert.IsType<ActionResult<BrainstormSession>>(result);
Assert.IsType<BadRequestObjectResult>(actionResult.Result);
}
La segunda prueba comprueba que se devuelve NotFound si la sesión no existe.
[Fact]
public async Task CreateActionResult_ReturnsNotFoundObjectResultForNonexistentSession()
{
// Arrange
var nonExistentSessionId = 999;
string testName = "test name";
string testDescription = "test description";
var mockRepo = new Mock<IBrainstormSessionRepository>();
var controller = new IdeasController(mockRepo.Object);
var newIdea = new NewIdeaModel()
{
Description = testDescription,
Name = testName,
SessionId = nonExistentSessionId
};
// Act
var result = await controller.CreateActionResult(newIdea);
// Assert
var actionResult = Assert.IsType<ActionResult<BrainstormSession>>(result);
Assert.IsType<NotFoundObjectResult>(actionResult.Result);
}
Para una sesión válida id
, la prueba final confirma que:
- El método devuelve
ActionResult
con un tipoBrainstormSession
. - ActionResult<T>.Result es CreatedAtActionResult.
CreatedAtActionResult
es similar a una respuesta 201 Creado con un encabezadoLocation
. - ActionResult<T>.Value es de un tipo
BrainstormSession
. - La llamada ficticia para actualizar la sesión,
UpdateAsync(testSession)
, se ha invocado. La llamada al métodoVerifiable
se comprueba mediante la ejecución demockRepo.Verify()
en las aserciones. - Se devuelven dos objetos
Idea
para la sesión. - El último elemento (el elemento
Idea
agregado por la llamada ficticia aUpdateAsync
) coincide con el elementonewIdea
agregado a la sesión en la prueba.
[Fact]
public async Task CreateActionResult_ReturnsNewlyCreatedIdeaForSession()
{
// Arrange
int testSessionId = 123;
string testName = "test name";
string testDescription = "test description";
var testSession = GetTestSession();
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync(testSession);
var controller = new IdeasController(mockRepo.Object);
var newIdea = new NewIdeaModel()
{
Description = testDescription,
Name = testName,
SessionId = testSessionId
};
mockRepo.Setup(repo => repo.UpdateAsync(testSession))
.Returns(Task.CompletedTask)
.Verifiable();
// Act
var result = await controller.CreateActionResult(newIdea);
// Assert
var actionResult = Assert.IsType<ActionResult<BrainstormSession>>(result);
var createdAtActionResult = Assert.IsType<CreatedAtActionResult>(actionResult.Result);
var returnValue = Assert.IsType<BrainstormSession>(createdAtActionResult.Value);
mockRepo.Verify();
Assert.Equal(2, returnValue.Ideas.Count());
Assert.Equal(testName, returnValue.Ideas.LastOrDefault().Name);
Assert.Equal(testDescription, returnValue.Ideas.LastOrDefault().Description);
}
Los controles desempeñan un rol fundamental en cualquier aplicación de ASP.NET Core MVC. Por tanto, debe tener la seguridad de que los controladores se comportan según lo previsto. Las pruebas automatizadas pueden detectar errores antes de que la aplicación se implemente en un entorno de producción.
Vea o descargue el código de ejemplo (cómo descargarlo)
Pruebas unitarias de la lógica del controlador
Las pruebas unitarias implican probar una parte de una aplicación de forma aislada con respecto a su infraestructura y dependencias. Cuando se realizan pruebas unitarias de la lógica de controlador, solo se comprueba el contenido de una única acción, no el comportamiento de sus dependencias o del marco en sí.
Configure pruebas unitarias de las acciones del controlador para centrarse en el comportamiento del controlador. Una prueba unitaria del controlador evita escenarios como filtros, enrutamiento y enlace de modelos. Las pruebas que abarcan las interacciones entre los componentes que colectivamente responden a una solicitud se controlan mediante pruebas de integración. Para más información sobre pruebas de integración, vea Pruebas de integración en ASP.NET Core.
Si va a escribir filtros personalizados y rutas, realice pruebas unitarias en ellos de forma aislada, no como parte de las pruebas de una acción de controlador concreta.
Para demostrar las pruebas unitarias del controlador, revise el siguiente controlador en la aplicación de ejemplo. El controlador Home muestra una lista de sesiones de lluvia de ideas y permite crear nuevas sesiones de lluvia de ideas con una solicitud POST:
public class HomeController : Controller
{
private readonly IBrainstormSessionRepository _sessionRepository;
public HomeController(IBrainstormSessionRepository sessionRepository)
{
_sessionRepository = sessionRepository;
}
public async Task<IActionResult> Index()
{
var sessionList = await _sessionRepository.ListAsync();
var model = sessionList.Select(session => new StormSessionViewModel()
{
Id = session.Id,
DateCreated = session.DateCreated,
Name = session.Name,
IdeaCount = session.Ideas.Count
});
return View(model);
}
public class NewSessionModel
{
[Required]
public string SessionName { get; set; }
}
[HttpPost]
public async Task<IActionResult> Index(NewSessionModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
else
{
await _sessionRepository.AddAsync(new BrainstormSession()
{
DateCreated = DateTimeOffset.Now,
Name = model.SessionName
});
}
return RedirectToAction(actionName: nameof(Index));
}
}
El controlador anterior:
- Sigue el Principio de dependencias explícitas.
- Espera la inserción de dependencias (DI) para ofrecer una instancia de
IBrainstormSessionRepository
. - Se puede probar con un servicio
IBrainstormSessionRepository
ficticio con el uso de un marco de objeto ficticio, como Moq. Un objeto ficticio es un objeto fabricado con un conjunto predeterminado de comportamientos de propiedades y métodos utilizados para las pruebas. Para más información, vea Introducción a las pruebas de integración.
El método HTTP GET Index
no tiene bucles ni bifurcaciones y solamente llama a un método. La prueba unitaria para esta acción:
- Realice el simulacro del servicio
IBrainstormSessionRepository
mediante el métodoGetTestSessions
.GetTestSessions
crea dos sesiones de lluvia de ideas ficticias con fechas y nombres de sesión. - Ejecuta el método
Index
. - Realiza aserciones sobre el resultado devuelto por el método:
- Se devuelve ViewResult.
- ViewDataDictionary.Model es
StormSessionViewModel
. - Hay dos sesiones de lluvia de ideas almacenadas en
ViewDataDictionary.Model
.
[Fact]
public async Task Index_ReturnsAViewResult_WithAListOfBrainstormSessions()
{
// Arrange
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.ListAsync())
.ReturnsAsync(GetTestSessions());
var controller = new HomeController(mockRepo.Object);
// Act
var result = await controller.Index();
// Assert
var viewResult = Assert.IsType<ViewResult>(result);
var model = Assert.IsAssignableFrom<IEnumerable<StormSessionViewModel>>(
viewResult.ViewData.Model);
Assert.Equal(2, model.Count());
}
private List<BrainstormSession> GetTestSessions()
{
var sessions = new List<BrainstormSession>();
sessions.Add(new BrainstormSession()
{
DateCreated = new DateTime(2016, 7, 2),
Id = 1,
Name = "Test One"
});
sessions.Add(new BrainstormSession()
{
DateCreated = new DateTime(2016, 7, 1),
Id = 2,
Name = "Test Two"
});
return sessions;
}
Las pruebas del método HTTP POST Index
del controlador de Home verifican que:
- Cuando ModelState.IsValid es
false
, el método de acción devuelve un solicitud incorrecta 400 ViewResult con los datos adecuados. - Cuando
ModelState.IsValid
estrue
:- Se llama al método
Add
del repositorio. - Se devuelve RedirectToActionResult con los argumentos correctos.
- Se llama al método
Un estado de modelo no válido se comprueba mediante la adición de errores con AddModelError, como se muestra en la primera prueba de abajo:
[Fact]
public async Task IndexPost_ReturnsBadRequestResult_WhenModelStateIsInvalid()
{
// Arrange
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.ListAsync())
.ReturnsAsync(GetTestSessions());
var controller = new HomeController(mockRepo.Object);
controller.ModelState.AddModelError("SessionName", "Required");
var newSession = new HomeController.NewSessionModel();
// Act
var result = await controller.Index(newSession);
// Assert
var badRequestResult = Assert.IsType<BadRequestObjectResult>(result);
Assert.IsType<SerializableError>(badRequestResult.Value);
}
[Fact]
public async Task IndexPost_ReturnsARedirectAndAddsSession_WhenModelStateIsValid()
{
// Arrange
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.AddAsync(It.IsAny<BrainstormSession>()))
.Returns(Task.CompletedTask)
.Verifiable();
var controller = new HomeController(mockRepo.Object);
var newSession = new HomeController.NewSessionModel()
{
SessionName = "Test Name"
};
// Act
var result = await controller.Index(newSession);
// Assert
var redirectToActionResult = Assert.IsType<RedirectToActionResult>(result);
Assert.Null(redirectToActionResult.ControllerName);
Assert.Equal("Index", redirectToActionResult.ActionName);
mockRepo.Verify();
}
Si ModelState no es válido, se devuelve el mismo elemento ViewResult
que para una solicitud GET. La prueba no intenta pasar un modelo no válido. Pasar un modelo no válido no es un enfoque válido, ya que el enlace de modelos no está en ejecución (aunque una prueba de integración usa el enlace de modelos). En este caso, no se comprueba el enlace de modelos. Estas pruebas unitarias solo comprueban el código del método de acción.
La segunda prueba verifica que, cuando ModelState
es válido:
- Se agrega un nuevo elemento
BrainstormSession
(mediante el repositorio). - El método devuelve un elemento
RedirectToActionResult
con las propiedades esperadas.
Las llamadas ficticias que no se efectúan se suelen ignorar, aunque llamar a Verifiable
al final de la llamada de configuración permite realizar una validación ficticia de la prueba. Esto se realiza con una llamada a mockRepo.Verify
, que producirá un error en la prueba si no se ha llamado al método esperado.
Nota:
La biblioteca Moq usada en este ejemplo permite mezclar fácilmente objetos ficticios comprobables o "estrictos" con objetos ficticios no comprobables (también denominados "flexibles" o stub). Obtenga más información sobre cómo personalizar el comportamiento de objetos ficticios con Moq.
SessionController en la aplicación de ejemplo muestra información relacionada con una sesión de lluvia de ideas determinada. El controlador incluye lógica para tratar valores id
no válidos (hay dos escenarios return
en el ejemplo siguiente para abarcar estos escenarios). La instrucción return
final devuelve un nuevo elemento StormSessionViewModel
en la vista (Controllers/SessionController.cs
):
public class SessionController : Controller
{
private readonly IBrainstormSessionRepository _sessionRepository;
public SessionController(IBrainstormSessionRepository sessionRepository)
{
_sessionRepository = sessionRepository;
}
public async Task<IActionResult> Index(int? id)
{
if (!id.HasValue)
{
return RedirectToAction(actionName: nameof(Index),
controllerName: "Home");
}
var session = await _sessionRepository.GetByIdAsync(id.Value);
if (session == null)
{
return Content("Session not found.");
}
var viewModel = new StormSessionViewModel()
{
DateCreated = session.DateCreated,
Name = session.Name,
Id = session.Id
};
return View(viewModel);
}
}
Las pruebas unitarias incluyen una prueba de cada escenario return
en la acción Index
del controlador de sesión:
[Fact]
public async Task IndexReturnsARedirectToIndexHomeWhenIdIsNull()
{
// Arrange
var controller = new SessionController(sessionRepository: null);
// Act
var result = await controller.Index(id: null);
// Assert
var redirectToActionResult =
Assert.IsType<RedirectToActionResult>(result);
Assert.Equal("Home", redirectToActionResult.ControllerName);
Assert.Equal("Index", redirectToActionResult.ActionName);
}
[Fact]
public async Task IndexReturnsContentWithSessionNotFoundWhenSessionNotFound()
{
// Arrange
int testSessionId = 1;
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync((BrainstormSession)null);
var controller = new SessionController(mockRepo.Object);
// Act
var result = await controller.Index(testSessionId);
// Assert
var contentResult = Assert.IsType<ContentResult>(result);
Assert.Equal("Session not found.", contentResult.Content);
}
[Fact]
public async Task IndexReturnsViewResultWithStormSessionViewModel()
{
// Arrange
int testSessionId = 1;
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync(GetTestSessions().FirstOrDefault(
s => s.Id == testSessionId));
var controller = new SessionController(mockRepo.Object);
// Act
var result = await controller.Index(testSessionId);
// Assert
var viewResult = Assert.IsType<ViewResult>(result);
var model = Assert.IsType<StormSessionViewModel>(
viewResult.ViewData.Model);
Assert.Equal("Test One", model.Name);
Assert.Equal(2, model.DateCreated.Day);
Assert.Equal(testSessionId, model.Id);
}
Al pasar al controlador de ideas, la aplicación expone la funcionalidad como una API web en la ruta api/ideas
:
- El método
ForSession
devuelve una lista de ideas (IdeaDTO
) asociadas con una sesión de lluvia de ideas. - El método
Create
agrega nuevas ideas a una sesión.
[HttpGet("forsession/{sessionId}")]
public async Task<IActionResult> ForSession(int sessionId)
{
var session = await _sessionRepository.GetByIdAsync(sessionId);
if (session == null)
{
return NotFound(sessionId);
}
var result = session.Ideas.Select(idea => new IdeaDTO()
{
Id = idea.Id,
Name = idea.Name,
Description = idea.Description,
DateCreated = idea.DateCreated
}).ToList();
return Ok(result);
}
[HttpPost("create")]
public async Task<IActionResult> Create([FromBody]NewIdeaModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var session = await _sessionRepository.GetByIdAsync(model.SessionId);
if (session == null)
{
return NotFound(model.SessionId);
}
var idea = new Idea()
{
DateCreated = DateTimeOffset.Now,
Description = model.Description,
Name = model.Name
};
session.AddIdea(idea);
await _sessionRepository.UpdateAsync(session);
return Ok(session);
}
Evite devolver entidades de dominio de negocio directamente mediante llamadas API. Entidades de dominio:
- Suelen incluir más datos de los que necesita el cliente.
- Innecesariamente acoplan el modelo de dominio interno de la aplicación con la API expuesta públicamente.
La asignación entre las entidades de dominio y los tipos devueltos al cliente se puede realizar:
- Manualmente, con una operación
Select
de LINQ, como la aplicación de ejemplo usa. Para más información, vea LINQ (Language Integrated Query). - Automáticamente, con una biblioteca, como AutoMapper.
A continuación, la aplicación de ejemplo muestra las pruebas unitarias para los métodos de API Create
y ForSession
del controlador de ideas.
La aplicación de ejemplo contiene dos pruebas ForSession
. La primera prueba determina si ForSession
devuelve NotFoundObjectResult (HTTP no encontrado) para una sesión no válida:
[Fact]
public async Task ForSession_ReturnsHttpNotFound_ForInvalidSession()
{
// Arrange
int testSessionId = 123;
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync((BrainstormSession)null);
var controller = new IdeasController(mockRepo.Object);
// Act
var result = await controller.ForSession(testSessionId);
// Assert
var notFoundObjectResult = Assert.IsType<NotFoundObjectResult>(result);
Assert.Equal(testSessionId, notFoundObjectResult.Value);
}
La segunda prueba ForSession
determina si ForSession
devuelve una lista de ideas de sesión (<List<IdeaDTO>>
) para una sesión válida. Las comprobaciones también examinan la primera idea para confirmar si su propiedad Name
es correcta:
[Fact]
public async Task ForSession_ReturnsIdeasForSession()
{
// Arrange
int testSessionId = 123;
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync(GetTestSession());
var controller = new IdeasController(mockRepo.Object);
// Act
var result = await controller.ForSession(testSessionId);
// Assert
var okResult = Assert.IsType<OkObjectResult>(result);
var returnValue = Assert.IsType<List<IdeaDTO>>(okResult.Value);
var idea = returnValue.FirstOrDefault();
Assert.Equal("One", idea.Name);
}
Para comprobar el comportamiento del método Create
cuando ModelState
no es válido, la aplicación de ejemplo agrega un error de modelo al controlador como parte de la prueba. No intente probar la validación del modelo o el enlace de modelos en las pruebas unitarias; céntrese en probar el comportamiento del método de acción al confrontarlo con un valor de ModelState
no válido:
[Fact]
public async Task Create_ReturnsBadRequest_GivenInvalidModel()
{
// Arrange & Act
var mockRepo = new Mock<IBrainstormSessionRepository>();
var controller = new IdeasController(mockRepo.Object);
controller.ModelState.AddModelError("error", "some error");
// Act
var result = await controller.Create(model: null);
// Assert
Assert.IsType<BadRequestObjectResult>(result);
}
La segunda prueba de Create
depende de que el repositorio devuelva null
, por lo que el repositorio ficticio está configurado para devolver null
. No es necesario crear una base de datos de prueba (en memoria o de cualquier otro modo) ni crear una consulta que devuelva este resultado. La prueba puede realizarse en una sola instrucción, como se muestra en el código de ejemplo:
[Fact]
public async Task Create_ReturnsHttpNotFound_ForInvalidSession()
{
// Arrange
int testSessionId = 123;
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync((BrainstormSession)null);
var controller = new IdeasController(mockRepo.Object);
// Act
var result = await controller.Create(new NewIdeaModel());
// Assert
Assert.IsType<NotFoundObjectResult>(result);
}
La tercera prueba de Create
, Create_ReturnsNewlyCreatedIdeaForSession
, verifica que se llama al método UpdateAsync
del repositorio. Se llama al objeto ficticio con Verifiable
y, después, se llama al método Verify
del repositorio ficticio para confirmar que se ejecutó el método Verifiable. La prueba unitaria no es responsable de garantizar que el método UpdateAsync
guarda los datos; esto se puede realizar con una prueba de integración.
[Fact]
public async Task Create_ReturnsNewlyCreatedIdeaForSession()
{
// Arrange
int testSessionId = 123;
string testName = "test name";
string testDescription = "test description";
var testSession = GetTestSession();
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync(testSession);
var controller = new IdeasController(mockRepo.Object);
var newIdea = new NewIdeaModel()
{
Description = testDescription,
Name = testName,
SessionId = testSessionId
};
mockRepo.Setup(repo => repo.UpdateAsync(testSession))
.Returns(Task.CompletedTask)
.Verifiable();
// Act
var result = await controller.Create(newIdea);
// Assert
var okResult = Assert.IsType<OkObjectResult>(result);
var returnSession = Assert.IsType<BrainstormSession>(okResult.Value);
mockRepo.Verify();
Assert.Equal(2, returnSession.Ideas.Count());
Assert.Equal(testName, returnSession.Ideas.LastOrDefault().Name);
Assert.Equal(testDescription, returnSession.Ideas.LastOrDefault().Description);
}
Probar ActionResult<T>
En ASP.NET Core 2.1 o posterior, ActionResult<T> (ActionResult<TValue>) permite devolver un tipo que se deriva de ActionResult
o bien un tipo específico.
La aplicación de ejemplo incluye un método que devuelve un resultado List<IdeaDTO>
para una sesión determinada id
. Si la sesión id
no existe, el controlador devuelve NotFound:
[HttpGet("forsessionactionresult/{sessionId}")]
[ProducesResponseType(200)]
[ProducesResponseType(404)]
public async Task<ActionResult<List<IdeaDTO>>> ForSessionActionResult(int sessionId)
{
var session = await _sessionRepository.GetByIdAsync(sessionId);
if (session == null)
{
return NotFound(sessionId);
}
var result = session.Ideas.Select(idea => new IdeaDTO()
{
Id = idea.Id,
Name = idea.Name,
Description = idea.Description,
DateCreated = idea.DateCreated
}).ToList();
return result;
}
Dos pruebas del controlador ForSessionActionResult
se incluyen en ApiIdeasControllerTests
.
La primera prueba confirma que el controlador devuelve ActionResult
, pero no una lista inexistente de ideas para una sesión inexistente id
:
- El tipo
ActionResult
esActionResult<List<IdeaDTO>>
. - Result es NotFoundObjectResult.
[Fact]
public async Task ForSessionActionResult_ReturnsNotFoundObjectResultForNonexistentSession()
{
// Arrange
var mockRepo = new Mock<IBrainstormSessionRepository>();
var controller = new IdeasController(mockRepo.Object);
var nonExistentSessionId = 999;
// Act
var result = await controller.ForSessionActionResult(nonExistentSessionId);
// Assert
var actionResult = Assert.IsType<ActionResult<List<IdeaDTO>>>(result);
Assert.IsType<NotFoundObjectResult>(actionResult.Result);
}
Para obtener un elemento id
de sesión válido, la segunda prueba confirma que el método devuelve:
- Un
ActionResult
con un tipoList<IdeaDTO>
. - ActionResult<T>.Value es de un tipo
List<IdeaDTO>
. - El primer elemento de la lista es una idea válida que coincide con la idea almacenada en la sesión ficticia (obtenido mediante una llamada a
GetTestSession
).
[Fact]
public async Task ForSessionActionResult_ReturnsIdeasForSession()
{
// Arrange
int testSessionId = 123;
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync(GetTestSession());
var controller = new IdeasController(mockRepo.Object);
// Act
var result = await controller.ForSessionActionResult(testSessionId);
// Assert
var actionResult = Assert.IsType<ActionResult<List<IdeaDTO>>>(result);
var returnValue = Assert.IsType<List<IdeaDTO>>(actionResult.Value);
var idea = returnValue.FirstOrDefault();
Assert.Equal("One", idea.Name);
}
La aplicación de ejemplo también incluye un método para crear un nuevo elemento Idea
para una sesión determinada. El controlador devuelve:
- BadRequest para un modelo no válido.
- NotFound si no existe la sesión.
- CreatedAtAction cuando se actualiza la sesión con la idea nueva.
[HttpPost("createactionresult")]
[ProducesResponseType(201)]
[ProducesResponseType(400)]
[ProducesResponseType(404)]
public async Task<ActionResult<BrainstormSession>> CreateActionResult([FromBody]NewIdeaModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var session = await _sessionRepository.GetByIdAsync(model.SessionId);
if (session == null)
{
return NotFound(model.SessionId);
}
var idea = new Idea()
{
DateCreated = DateTimeOffset.Now,
Description = model.Description,
Name = model.Name
};
session.AddIdea(idea);
await _sessionRepository.UpdateAsync(session);
return CreatedAtAction(nameof(CreateActionResult), new { id = session.Id }, session);
}
Tres pruebas de CreateActionResult
se incluyen en ApiIdeasControllerTests
.
El primer texto confirma que se devuelve BadRequest para un modelo no válido.
[Fact]
public async Task CreateActionResult_ReturnsBadRequest_GivenInvalidModel()
{
// Arrange & Act
var mockRepo = new Mock<IBrainstormSessionRepository>();
var controller = new IdeasController(mockRepo.Object);
controller.ModelState.AddModelError("error", "some error");
// Act
var result = await controller.CreateActionResult(model: null);
// Assert
var actionResult = Assert.IsType<ActionResult<BrainstormSession>>(result);
Assert.IsType<BadRequestObjectResult>(actionResult.Result);
}
La segunda prueba comprueba que se devuelve NotFound si la sesión no existe.
[Fact]
public async Task CreateActionResult_ReturnsNotFoundObjectResultForNonexistentSession()
{
// Arrange
var nonExistentSessionId = 999;
string testName = "test name";
string testDescription = "test description";
var mockRepo = new Mock<IBrainstormSessionRepository>();
var controller = new IdeasController(mockRepo.Object);
var newIdea = new NewIdeaModel()
{
Description = testDescription,
Name = testName,
SessionId = nonExistentSessionId
};
// Act
var result = await controller.CreateActionResult(newIdea);
// Assert
var actionResult = Assert.IsType<ActionResult<BrainstormSession>>(result);
Assert.IsType<NotFoundObjectResult>(actionResult.Result);
}
Para una sesión válida id
, la prueba final confirma que:
- El método devuelve
ActionResult
con un tipoBrainstormSession
. - ActionResult<T>.Result es CreatedAtActionResult.
CreatedAtActionResult
es similar a una respuesta 201 Creado con un encabezadoLocation
. - ActionResult<T>.Value es de un tipo
BrainstormSession
. - La llamada ficticia para actualizar la sesión,
UpdateAsync(testSession)
, se ha invocado. La llamada al métodoVerifiable
se comprueba mediante la ejecución demockRepo.Verify()
en las aserciones. - Se devuelven dos objetos
Idea
para la sesión. - El último elemento (el elemento
Idea
agregado por la llamada ficticia aUpdateAsync
) coincide con el elementonewIdea
agregado a la sesión en la prueba.
[Fact]
public async Task CreateActionResult_ReturnsNewlyCreatedIdeaForSession()
{
// Arrange
int testSessionId = 123;
string testName = "test name";
string testDescription = "test description";
var testSession = GetTestSession();
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync(testSession);
var controller = new IdeasController(mockRepo.Object);
var newIdea = new NewIdeaModel()
{
Description = testDescription,
Name = testName,
SessionId = testSessionId
};
mockRepo.Setup(repo => repo.UpdateAsync(testSession))
.Returns(Task.CompletedTask)
.Verifiable();
// Act
var result = await controller.CreateActionResult(newIdea);
// Assert
var actionResult = Assert.IsType<ActionResult<BrainstormSession>>(result);
var createdAtActionResult = Assert.IsType<CreatedAtActionResult>(actionResult.Result);
var returnValue = Assert.IsType<BrainstormSession>(createdAtActionResult.Value);
mockRepo.Verify();
Assert.Equal(2, returnValue.Ideas.Count());
Assert.Equal(testName, returnValue.Ideas.LastOrDefault().Name);
Assert.Equal(testDescription, returnValue.Ideas.LastOrDefault().Description);
}
Recursos adicionales
- Pruebas de integración en ASP.NET Core
- Creación y ejecución de pruebas unitarias con Visual Studio
- MyTested.AspNetCore.Mvc: biblioteca fluida de pruebas para ASP.NET Core MVC: Biblioteca de pruebas fuertemente tipada, que ofrece una interfaz fluida para la prueba de aplicaciones MVC y de API web. (Microsoft no realiza su mantenimiento ni su soporte técnico. )
- JustMockLite: marco ficticio para desarrolladores de .NET. (Microsoft no realiza su mantenimiento ni su soporte técnico. )