En este post, más que una librería como tal, lo que voy a hacer es comparar múltiples librerías que nos permiten hacer mock dentro de C#.
Ya vimos en otro post cual es la diferencia entre mock, doble y fake, así, que si no estás familiarizado con los términos te recomiendo que empieces con este post.
Nota, debido a los recientes cambios (versión 4.20) en la librería moq, no vamos a cubrirla en este post. De todas formas tienes un post que la cubre de marzo de 2020;
En este post vamos a cubrir mock en .net con las librerias NSubstitute
y FakeitEasy
, La idea de este post es que nos sirva como introducción a dichas librerías, o si tu jefe os esta haciendo migrar de moq, que podais ver la sintaxis de los ejemplos más comunes.
Índice
Recuerda que todo el código está disponible en GitHub.
1 - Qué es mock y por qué utilizarlo?
En esta publicación explicaremos una característica muy común dentro de las pruebas unitarias o los unit test. Se trata de simular interfaces que se conectan con datos externos o servicios de terceros. Este proceso nos permitirá realizar pruebas exhaustivas en nuestros procedimientos.
El término "mock", en términos técnicos, tiene una connotación de imitación. Por lo tanto, en este artículo abordaremos la manera de imitar estas interfaces que facilitan el acceso a datos, posibilitando así la evaluación detallada del flujo del programa.
El uso de mock nos facilita la vida a la hora de cubrir todos los posibles escenarios dentro de nuestra aplicación.
Para este post, vamos a ver todos los ejemplos utilizando tanto la librería NSubstitute como la librería FakeItEasy.
2 - Caso de uso y librerías populares para hacer mock
Para ver el uso de mock en c# vamos a ver el siguiente caso de uso, una servicio que tiene dos dependencias, nos da igual lo que hagan esas dependencias ya que vamos a estar imitando su funcionamiento.
public class ArticleService
{
private readonly IAuthorRepository _authorRepository;
private readonly IArticleRepository _articleRepository;
public ArticleService(IAuthorRepository authorRepository, IArticleRepository articleRepository)
{
_authorRepository = authorRepository;
_articleRepository = articleRepository;
}
public Article InsertArticle(string content, string title, int authorId)
{
if (!_authorRepository.IsValid(authorId))
{
throw new Exception("Author not valid");
}
int aritcleId = _articleRepository.Insert(content, title, authorId);
return GetArticle(aritcleId);
}
public Article GetArticle(int id)
{
return _articleRepository.Get(id);
}
}
public interface IAuthorRepository
{
Author Get(int id);
bool IsValid(int id);
}
public interface IArticleRepository
{
int Insert(string content, string title, int authorId);
Article Get(int id);
}
public record Article(int Id, string Content, string Title, DateOnly Date, int AuthorId);
public record Author(int Id, string Name);
Y para imitar el funcionamiento instalamos la librería, Nsubstitute o FakeItEasy, como he dicho antes, tengo un post dedicado a la librería moq.
3 - Uso de mock con NSubstitute y FaketItEasy
El principal caso de uso de las librerías de mock siempre es simular el funcionamiento de las interfaces que abstraen las dependencias.
En el caso de uso del código de ejemplo son los interfaces que hacen referencia a dos repositorios.
Para el primer ejemplo vamos a imitar y sobreescribir el funcionamiento del método Get
dentro de IArticlerepository
;
Ejemplo en FakeItEasy:
[Fact]
public void WhenArticleExist_ThenReturnArticle() //FakeIt easy
{
Article article = new Article(1, "content", "title", DateOnly.FromDateTime(DateTime.UtcNow), 1);
IAuthorRepository authorRepository = A.Fake<IAuthorRepository>();
IArticleRepository articleRepository = A.Fake<IArticleRepository>();
A.CallTo(() => articleRepository.Get(article.Id)).Returns(article);
ArticleService service = new ArticleService(authorRepository, articleRepository);
Article result = service.GetArticle(article.Id);
Assert.Equal(article.Content, result.Content);
}
Como vemos es sencillo, simplemente utilizamos A
como tipo (no se muy bien porque lo hicieron con A en vez de con Fake) y ya dentro de A
tenemos acceso a todo lo que necesitamos.
Finalmente únicamente tenemos que pasar nuestra interfaz al constructor.
El uso de A.CallTo
nos permite especificar el funcionamiento del método que queremos imitar.
- Nota: si fuera un método asíncrono, el return cambia por
A.CallTo(() => articleRepository.Get(article.Id)).Returns(Task.FromResult(article));
.
Ejemplo en Nsubstitute:
[Fact]
public void WhenArticleExist_ThenReturnArticle() //Nsubstitute
{
Article article = new Article(1, "content", "title", DateOnly.FromDateTime(DateTime.UtcNow), 1);
IAuthorRepository authorRepository = Substitute.For<IAuthorRepository>();
IArticleRepository articleRepository = Substitute.For<IArticleRepository>();
articleRepository.Get(article.Id).Returns(article);
ArticleService service = new ArticleService(authorRepository, articleRepository);
Article result = service.GetArticle(article.Id);
Assert.Equal(article.Content, result.Content);
}
Muy similar al caso anterior, únicamente debemos de cambiar un poquitin la sintaxis y ya estamos ahí.
Algo que me gusta mucho de NSubstitute es que cuando haces el mock del método, únicamente necesitas pasar los valores correspondientes a dicho método, no tienes que incluir sintaxis adicional.
3.1 - Ejemplo completo mock con Nsubstitue y FakeItEasy
Ahora vamos a pasar a un ejemplo algo más completo, o donde tenemos más información que comprobar. En este caso vamos a testear método InsertArticle
, como vemos tiene dos salidas, así que tenemos que hacer dos tests, uno para cada una de las salidas.
Por norma general a mi me gusta empezar por el happy path así que por ahí vamos a ir.
Este es el ejemplo con FakeItEasy:
[Fact]
public void WhenAuthorIsValid_ThenArticleIsInserted()
{
Article article = new Article(1, "content", "title", DateOnly.FromDateTime(DateTime.UtcNow), 10);
IAuthorRepository authorRepository = A.Fake<IAuthorRepository>();
IArticleRepository articleRepository = A.Fake<IArticleRepository>();
A.CallTo(() => authorRepository.IsValid(A<int>._)).Returns(true);
A.CallTo(() => articleRepository.Insert(article.Content, article.Title, article.AuthorId)).Returns(article.Id);
A.CallTo(() => articleRepository.Get(article.Id)).Returns(article);
ArticleService service = new ArticleService(authorRepository, articleRepository);
Article result = service.InsertArticle(article.Content, article.Title, article.AuthorId);
A.CallTo(() => articleRepository.Insert(article.Content, article.Title, article.AuthorId))
.MustHaveHappened(1, Times.Exactly);
Assert.Equal(article.Content, result.Content);
}
En este ejemplo podemos ver como simplemente hemos incluido algo más de configuración para nuestro test, pero además hemos verificado que estamos haciendo la llamada en el assertion. Para ello utilizamos el método MustHaveHappened
con el número de veces que queremos asegurarnos de que algo pasa.
Ejemplo con Nsubstitute:
[Fact]
public void WhenAuthorIsValid_ThenArticleIsInserted()
{
Article article = new Article(1, "content", "title", DateOnly.FromDateTime(DateTime.UtcNow), 10);
IAuthorRepository authorRepository = Substitute.For<IAuthorRepository>();
IArticleRepository articleRepository = Substitute.For<IArticleRepository>();
articleRepository.Get(article.Id).Returns(article);
authorRepository.IsValid(Arg.Any<int>()).Returns(true);
articleRepository.Insert(article.Content, article.Title, article.AuthorId).Returns(article.Id);
ArticleService service = new ArticleService(authorRepository, articleRepository);
Article result = service.InsertArticle(article.Content, article.Title, article.AuthorId);
articleRepository.Received(1).Insert(article.Content, article.Title, article.AuthorId);
Assert.Equal(article.Content, result.Content);
}
Igual que en el anterior, el cambio es mínimo, esta vez configuramos el número de veces que hemos recibido una llamada con Received(times)
;
Es muy importante comprobar el número de veces que un método ocurre para asegurarnos de que no tenemos bugs y de que funciona como esperamos.
Ah, una cosa a tener en cuenta, cuando queremos que un método devuelva algo, pero nos da igual el input, podemos configurar el mock para que así sea, si te fijas en los ejemplos anteriores ya hemos visto esta funcionalidad. En FakeItEasy lo hacemos con A<T>._
mientras que en NSubstitute lo hacemos con Arg.Any<T>()
.
Para comprobar el path que falla con la excepción lo tenemos que hacer ya bien sea con la propia librería de tests o como mostré en el post anterior con FluentAssertions.