Para simular un método asíncrono con Moq, configúralo con ReturnsAsync() en lugar de Returns(). Para un método que devuelve Task<T>, ReturnsAsync(value) devuelve una tarea completada que envuelve ese valor, así que el código que se prueba puede esperarla con await de la forma habitual.

Para un método que devuelve un Task no genérico, sin resultado, no hay nada que envolver, así que la configuración usa Returns(Task.CompletedTask). Con esas dos líneas resolvemos casi todos los mocks asíncronos que lleguemos a escribir, y el resto del artículo muestra ambas sobre un repositorio real.

Para descargar el código fuente de este artículo, puedes visitar nuestro repositorio de GitHub.

¿Cómo usar ReturnsAsync en Moq?

ReturnsAsync() es el método de configuración de Moq para cualquier método simulado cuyo tipo de valor devuelto sea Task<T>. Recibe el valor que queremos que obtenga quien llama, y Moq lo envuelve en una tarea ya completada.

var mock = new Mock<IArticleRepository>();

mock.Setup(x => x.GetArticleAsync(It.IsAny<int>()))
    .ReturnsAsync(new ArticleDbModel { Id = 1, Title = "First Article" });

Al esperar GetArticleAsync(1) con await en el código que se prueba, ahora se obtiene ese artículo de forma síncrona y sin cambiar de hilo (thread), porque la tarea ya está completada antes de llegar al await.

El error que conviene conocer aquí es usar Returns() a secas. Returns(article) no compila, porque la configuración espera un Task<ArticleDbModel>, mientras que Returns(Task.FromResult(article)) sí compila. ReturnsAsync() es la forma abreviada de eso, y así es el código de Moq más antiguo.

Cuando el valor tiene que depender de los argumentos, ReturnsAsync también acepta una expresión lambda que los recibe, de modo que ReturnsAsync((int id) => _articles.First(a => a.Id == id)) devuelve un artículo distinto en cada llamada.

SetupSequence() resuelve el caso en que llamadas sucesivas deben devolver valores distintos: una prueba de reintento que falla una vez y después tiene éxito es el motivo habitual. Cada ReturnsAsync() encadenado a la secuencia corresponde a una llamada, en orden.

La misma idea de usar los argumentos está detrás de devolver un valor que se pasó a un método simulado.

¿Cómo simular un método que devuelve un Task no genérico?

Cuando el método devuelve Task sin resultado, no hay ningún valor que envolver, así que ReturnsAsync() no se aplica. La configuración devuelve directamente una tarea completada.

mock.Setup(x => x.SaveArticleAsync(It.IsAny<ArticleDbModel>()))
    .Returns(Task.CompletedTask);

Parece que un método asíncrono sin resultado, el equivalente asíncrono de un método void, debería usar el método de configuración asíncrono, pero ReturnsAsync() no compila con él, y la solución es Returns(), que parece más antiguo.

Omitir la configuración por completo falla de una forma más extraña. Un mock flexible (loose) devuelve una tarea ya completada para un método sin configurar que devuelve Task<T>, así que el await tiene éxito y devuelve default(T), o una secuencia vacía cuando T es un array, IEnumerable o IQueryable. Nuestro propio código lanza una excepción por ese null unas líneas más adelante.

Conviene reconocer ese fallo a simple vista, porque el stack trace apunta a nuestro propio código de producción y no a la prueba que olvidó una configuración. Un mock estricto (new Mock<T>(MockBehavior.Strict)) lo convierte en un error explícito de “no hay configuración”.

Un caso relacionado sigue la misma regla: usa Returns(Task.FromException(ex)) para que la tarea falle, o ThrowsAsync(ex), que se lee mejor y hace lo mismo.

El proyecto de repositorio que vamos a probar

Este proyecto, MockAsynchronousMethods.Repository, representa un patrón de repositorio que conecta nuestro código fuente con una base de datos falsa, con la misma estructura de repositorio que usaríamos al hacer pruebas unitarias de controladores con Moq.

Como crear el repositorio no es el objetivo principal de este artículo, no vamos a mostrar todas las clases e interfaces, sino la clase principal que vamos a probar. Dicho esto, veamos la clase ArticleRepository:

public class ArticleRepository : IArticleRepository
{
    private readonly IFakeDbArticles _fakeDbArticle;

    public ArticleRepository(IFakeDbArticles fakeDbArticle)
    {
        _fakeDbArticle = fakeDbArticle;
    }

    public async Task<ArticleDbModel?> GetArticleAsync(int id)
    {
        return await _fakeDbArticle.GetByIdAsync(id);
    }

    public async Task<IEnumerable<ArticleDbModel>> GetAllArticlesAsync()
    {
        return await _fakeDbArticle.GetAsync();
    }
}

Esta clase implementa la interfaz IArticleRepository. Ten en cuenta que todos estos métodos son asíncronos y simplemente obtienen datos de la clase FakeDbArticles:

public class FakeDbArticles : List<ArticleDbModel>, IFakeDbArticles
{
    private readonly static List<ArticleDbModel> _articles = Populate();

    private static List<ArticleDbModel> Populate()
    {
        var result = new List<ArticleDbModel>()
        {
            new ArticleDbModel
            {
                Id = 1,
                Title = "First Article",
                LastUpdate = DateTime.Now
            },
           ...
        };

        return result;
    }

    public async Task<IEnumerable<ArticleDbModel>> GetAsync()
    {
        return await Task.FromResult(_articles);
    }

    public async Task<ArticleDbModel?> GetByIdAsync(int id)
    {
        return await Task.FromResult(_articles.FirstOrDefault(x => x.Id == id));
    }
}

Para más detalles, puedes consultar la implementación completa de este repositorio en el código fuente de este artículo.

The Web API Production Checklist, ebook gratuito en inglés

Ebook gratis

¿Tu Web API está lista para producción?

33 puntos que comprobar antes de desplegarla, con la solución de cada uno. Un PDF gratuito de 76 páginas para .NET 10.

El ebook está en inglés.

Descarga la checklist gratis

PDF gratuito. Un solo correo para enviártelo. Puedes darte de baja cuando quieras.

Proyecto de pruebas xUnit para simular métodos asíncronos

Ahora que el repositorio está listo, vamos a crear el proyecto de pruebas xUnit MockAsynchronousMethods.Tests. Consulta el artículo sobre pruebas unitarias con xUnit si el propio framework es nuevo para ti.

Una vez listo el proyecto, agreguemos MockAsynchronousMethods.Repository como referencia haciendo clic con el botón derecho en las dependencias y después en Add Project Reference.

A continuación, tenemos que instalar el framework Moq:

Install-Package Moq

Una vez hecho esto, vamos a crear una carpeta Mock y, dentro de ella, una clase FakeDbArticleMock que herede de Mock<IFakeDbArticles>.

using MockAsynchronousMethods.Repository.Interfaces;
using Moq;

public class FakeDbArticleMock : Mock<IFakeDbArticles>
{
}

Y una clase FakeDb para guardar una lista con algunos artículos:

using MockAsynchronousMethods.Repository.DbModels;

public static class FakeDb
{
    public static List<ArticleDbModel> Articles = new List<ArticleDbModel>()
    {
        new ArticleDbModel
        {
            Id = 1,
            Title = "First Article",
            LastUpdate = DateTime.Now
        },
        new ArticleDbModel
        {
            Id = 2,
            Title = "Second title",
            LastUpdate = DateTime.Now
        },
        new ArticleDbModel
        {
            Id = 3,
            Title = "Third title",
            LastUpdate = DateTime.Now
        }
    };
}

Ahora, empecemos a configurar los mocks:

public FakeDbArticleMock GetByIdAsync()
{
    Setup(x => x.GetByIdAsync(It.IsAny<int>()))
        .ReturnsAsync(FakeDb.Articles.First());

    return this;
}

En este método, configuramos una llamada simulada asíncrona al método GetByIdAsync de la interfaz IFakeDbArticles. Este método devuelve el primer artículo de la lista que creamos dentro de la clase FakeDb.

Cuando simulamos un método asíncrono que devuelve un valor, en lugar de usar el método estándar Returns(...), usamos el método ReturnsAsync(...), que hace que el mock devuelva una tarea completada.

Puedes consultar los demás métodos de mock en la implementación completa de la clase FakeDbArticleMock aquí.

Ahora que todos los mocks están listos, es momento de probar nuestro código.

The Web API Production Checklist, ebook gratuito en inglés

Ebook gratis

¿Tu Web API está lista para producción?

33 puntos que comprobar antes de desplegarla, con la solución de cada uno. Un PDF gratuito de 76 páginas para .NET 10.

El ebook está en inglés.

Descarga la checklist gratis

PDF gratuito. Un solo correo para enviártelo. Puedes darte de baja cuando quieras.

En la raíz del proyecto de pruebas, vamos a crear una clase ArticleRepositoryTests, que contendrá todos los métodos de prueba de este artículo. Su archivo empieza con directivas using para MockAsynchronousMethods.Repository, MockAsynchronousMethods.Tests.Mock y Moq.

Y después, creemos un método de prueba:

[Fact]
public async Task GivenAMockedDatabase_WhenRequestingAnExistingArticleAsynchronously_ThenReturnASingleArticle()
{
    var mockArticleRepository = new FakeDbArticleMock()
        .GetByIdAsync();

    var articleRepository = new ArticleRepository(mockArticleRepository.Object);
    var result = await articleRepository.GetArticleAsync(1);

    Assert.NotNull(result);
    Assert.Equal(FakeDb.Articles.First(), result);
}

Primero, creamos una instancia de la clase FakeDbArticleMock e indicamos qué configuración queremos usar en esta prueba. Después, hay que crear una instancia del repositorio que queremos probar e inyectarle la instancia del mock.

Por último, llamamos al método que estamos probando y comprobamos los resultados con aserciones.

Puedes consultar los demás métodos de prueba en nuestro código fuente.

¿Cómo verificar que se llamó a un método asíncrono?

La verificación no es asíncrona. Verify() inspecciona las llamadas que Moq ya ha registrado, así que no hay nada que esperar con await ni ningún VerifyAsync() que buscar.

mockArticleRepository.Verify(x => x.GetByIdAsync(1), Times.Once);

El problema está en el orden. La llamada solo se registra cuando el código que se prueba la ha invocado de verdad, así que la llamada esperada con await debe completarse antes de verificar. Eso se cumple siempre que el propio método de prueba sea async Task y que todas las llamadas que contiene se esperen con await.

Es en una prueba async void donde esto se vuelve frágil. xUnit.net 2.9 todavía espera a que termine, pero xUnit.net v3 ya no admite pruebas async void. La guía de programación asíncrona de Microsoft recomienda “devolver ‘async void’ solo desde manejadores de eventos”, y cita la dificultad para probar ese código entre sus motivos. Los métodos de prueba devuelven Task, siempre.

It.IsAny<int>() coincide con cualquier argumento cuando el valor no importa, y Times.Once después de dos llamadas es la aserción de una prueba de almacenamiento en caché: demostrar que la segunda llamada no llegó al repositorio suele ser el objetivo.

Para ver con más detalle cómo comprobar el número de llamadas, consulta el artículo sobre cómo usar Moq para saber si se llamó a un método.

El método devuelveConfiguraciónNotas
Task<T>.ReturnsAsync(value)Tarea completada que envuelve el valor
Task<T>, un valor por llamada.ReturnsAsync((int id) => Find(id))La expresión lambda recibe los argumentos de la llamada
Task<T>, secuencia de valores.SetupSequence(...).ReturnsAsync(a).ReturnsAsync(b)Un valor por cada llamada sucesiva
Task (sin resultado).Returns(Task.CompletedTask)ReturnsAsync() no se aplica
ValueTask<T>.ReturnsAsync(value)Se admite de la misma forma
IAsyncEnumerable.Returns(items.ToAsyncEnumerable())No es un tipo que se pueda esperar con await. No hay ReturnsAsync; ToAsyncEnumerable() está integrado en .NET 10 y no hace falta ningún paquete
Lanzar una excepción de forma asíncrona.ThrowsAsync(new NotFoundException())Hace fallar la tarea en lugar de lanzar la excepción en la llamada
Retraso antes de devolver.ReturnsAsync(value, TimeSpan.FromMilliseconds(50))Para pruebas de tiempo de espera y de cancelación
Verificar que se llamómock.Verify(x => x.GetByIdAsync(1), Times.Once)Igual que en código síncrono: Verify no es asíncrono

Algo que conviene saber antes de elegir Moq para un proyecto nuevo: SponsorLink llegó con Moq 4.20.0 (agosto de 2023) y se eliminó en la 4.20.2, así que la versión actual, la 4.21.0, está limpia. Moq no publicó nada entre la 4.20.72, de septiembre de 2024, y la 4.21.0, de septiembre de 2026, un dato que vale la pena sopesar al elegir una biblioteca de mocks. Nuestra guía sobre cómo crear mocks eficaces con NSubstitute trata lo mismo con una biblioteca que sigue manteniéndose activamente.

Conclusión

En este artículo, hemos aprendido a simular métodos asíncronos con el framework Moq, usando ReturnsAsync() para los métodos que devuelven Task<T> y Returns(Task.CompletedTask) para los que devuelven un Task no genérico. También hemos visto cómo verificar que de verdad se llamó a un método asíncrono y la trampa de hacerlo desde una prueba async void.

Probado con .NET 10.0.10.