web-dev-qa-db-fra.com

Affirmation d'exception de test asynchrone Nunit

J'ai un contrôleur UserController avec cette action

// GET /blah
public Task<User> Get(string domainUserName)
{
        if (string.IsNullOrEmpty(domainUserName))
        {
            throw new ArgumentException("No username specified.");
        }

        return Task.Factory.StartNew(
            () =>
                {
                    var user = userRepository.GetByUserName(domainUserName);
                    if (user != null)
                    {
                        return user;
                    }

                    throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.NotFound, string.Format("{0} - username does not exist", domainUserName)));
                });
}

J'essaie d'écrire un test pour le cas où je lève une exception 404. 

Voici ce que j'ai essayé, avec la sortie - 

1) 

[Test]
public void someTest()
{
        var mockUserRepository = new Mock<IUserRepository>();
        mockUserRepository.Setup(x => x.GetByUserName(It.IsAny<string>())).Returns(default(User));
    var userController = new UserController(mockUserRepository.Object) { Request = new HttpRequestMessage() };

    Assert.That(async () => await userController.Get("foo"), Throws.InstanceOf<HttpResponseException>());
}

Résultat Le test a échoué

  Expected: instance of <System.Web.Http.HttpResponseException>
  But was:  no exception thrown

2) 

[Test]
public void someTest()
{
        var mockUserRepository = new Mock<IUserRepository>();
        mockUserRepository.Setup(x => x.GetByUserName(It.IsAny<string>())).Returns(default(User));
    var userController = new UserController(mockUserRepository.Object) { Request = new HttpRequestMessage() };

    var httpResponseException = Assert.Throws<HttpResponseException>(() => userController.Get("foo").Wait());
    Assert.That(httpResponseException.Response.StatusCode, Is.EqualTo(HttpStatusCode.NotFound));
}

Résultat Le test a échoué

  Expected: <System.Web.Http.HttpResponseException>
  But was:  <System.AggregateException> (One or more errors occurred.)

3)

[Test]
public void someTest()
{
        var mockUserRepository = new Mock<IUserRepository>();
        mockUserRepository.Setup(x => x.GetByUserName(It.IsAny<string>())).Returns(default(User));
    var userController = new UserController(mockUserRepository.Object) { Request = new HttpRequestMessage() };

    var httpResponseException = Assert.Throws<HttpResponseException>(async () => await userController.Get("foo"));
    Assert.That(httpResponseException.Response.StatusCode, Is.EqualTo(HttpStatusCode.NotFound));
}

Résultat Le test a échoué

  Expected: <System.Web.Http.HttpResponseException>
  But was:  null

4)

[Test]
[ExpectedException(typeof(HttpResponseException))]
public async void ShouldThrow404WhenNotFound()
{            var mockUserRepository = new Mock<IUserRepository>();
        mockUserRepository.Setup(x => x.GetByUserName(It.IsAny<string>())).Returns(default(User));

    var userController = new UserController(mockUserRepository.Object) { Request = new HttpRequestMessage() };

    var task = await userController.Get("foo");
}

Résultat Test réussi

Des questions -

  1. Pourquoi Assert.Throws ne gère-t-il pas HttpResponseException, contrairement à ExpectedException?
  2. Je ne veux pas simplement tester que l'exception est levée. Je veux affirmer sur le code d'état de la réponse. Quel est le moyen de faire ça?

Toute comparaison entre ces comportements et leurs causes serait géniale!

36

Vous voyez des problèmes dus à async void.

En particulier:

1) async () => await userController.Get("foo") est converti en TestDelegate, ce qui retourne void; votre expression lambda est donc traitée comme async void. Le testeur commencera donc à exécuter le lambda mais n'attendra pas qu'il soit terminé. Le lambda est renvoyé avant la fin de Get (car il s'agit de async) et le lanceur de test voit qu'il est retourné sans exception.

2) Wait encapsule toutes les exceptions dans une AggregateException.

3) Encore une fois, la async lambda est traitée comme async void, de sorte que le programme d’essai n’attend pas son achèvement.

4) Je vous recommande de créer ce async Task plutôt que async void, mais dans ce cas, le programme d'exécution de test attend la fin et voit donc l'exception.

Selon ce rapport de bogue , il existe un correctif pour cela dans la prochaine version de NUnit. En attendant, vous pouvez créer votre propre méthode ThrowsAsync; un exemple de pour xUnit est ici .

41
Stephen Cleary

Je ne sais pas quand cela a été ajouté, mais la version actuelle de Nunit (3.4.1 au moment de l'écriture) inclut une méthode ThrowsAsync.

voir https://github.com/nunit/docs/wiki/Assert.ThrowsAsync

Exemple:

[Test]
public void ShouldThrow404WhenNotFound()
{
    var mockUserRepository = new Mock<IUserRepository>();
    mockUserRepository.Setup(x => x.GetByUserName(It.IsAny<string>())).Returns(default(User));
    var userController = new UserController(mockUserRepository.Object) { Request = new HttpRequestMessage() };

    var exception = Assert.ThrowsAsync<HttpResponseException>(() => userController.Get("foo"));

    Assert.That(exception.Response.StatusCode, Is.EqualTo(HttpStatusCode.NotFound));
}
27
James Ross

Ce blog parle de problèmes similaires au mien.

J'ai suivi la recommandation proposée et fais un test comme celui-ci - 

    [Test]
    public void ShouldThrow404WhenNotFound()
    {
        var mockUserRepository = new Mock<IUserRepository>();
        mockUserRepository.Setup(x => x.GetByUserName(It.IsAny<string>())).Returns(default(User));
        var userController = new UserController(mockUserRepository.Object) { Request = new HttpRequestMessage() };

        var aggregateException = Assert.Throws<AggregateException>(() => userController.Get("foo").Wait());
        var httpResponseException = aggregateException.InnerExceptions
            .FirstOrDefault(x => x.GetType() == typeof(HttpResponseException)) as HttpResponseException;

        Assert.That(httpResponseException, Is.Not.Null);
        Assert.That(httpResponseException.Response.StatusCode, Is.EqualTo(HttpStatusCode.NotFound));
    }

Je ne suis pas trop content, mais ça marche.

EDIT 1

Inspiré par @StephenCleary, j'ai ajouté une classe d'assistance statique qui effectue les assertions recherchées. Ça ressemble à ça - 

public static class AssertEx
{
    public static async Task ThrowsAsync<TException>(Func<Task> func) where TException : class
    {
        await ThrowsAsync<TException>(func, exception => { });
    } 

    public static async Task ThrowsAsync<TException>(Func<Task> func, Action<TException> action) where TException : class
    {
        var exception = default(TException);
        var expected = typeof(TException);
        Type actual = null;
        try
        {
            await func();
        }
        catch (Exception e)
        {
            exception = e as TException;
            actual = e.GetType();
        }

        Assert.AreEqual(expected, actual);
        action(exception);
    }
}

Je peux maintenant avoir un test comme - 

    [Test]
    public async void ShouldThrow404WhenNotFound()
    {
        var mockUserRepository = new Mock<IUserRepository>();
        mockUserRepository.Setup(x => x.GetByUserName(It.IsAny<string>())).Returns(default(User));
        var userController = new UserController(mockUserRepository.Object) { Request = new HttpRequestMessage() };

        Action<HttpResponseException> asserts = exception => Assert.That(exception.Response.StatusCode, Is.EqualTo(HttpStatusCode.NotFound));
        await AssertEx.ThrowsAsync(() => userController.Get("foo"), asserts);
    }
11

Si vous attendez une tâche, les exceptions levées sont agrégées dans AggregateException. Vous pouvez inspecter les exceptions internes de AggregateException. Cela pourrait être la raison pour laquelle votre cas 2 ne fonctionne pas.

Les exceptions non gérées émises par le code utilisateur exécuté dans une tâche sont renvoyées au thread qui se joint, à l'exception de certains scénarios décrits plus loin dans cette rubrique. Les exceptions sont propagées lorsque vous utilisez l'une des méthodes statiques ou d'instance Task.Wait ou Task.Wait et que vous les gérez en incluant l'appel dans une instruction try-catch. Si une tâche est le parent de tâches enfants attachées ou si vous attendez plusieurs tâches, plusieurs exceptions peuvent être levées. Pour propager toutes les exceptions au thread appelant, l'infrastructure de tâches les encapsule dans une instance AggregateException. AggregateException possède une propriété InnerExceptions qui peut être énumérée pour examiner toutes les exceptions originales levées et gérer (ou non) chacune individuellement. Même si une seule exception est levée, elle est toujours encapsulée dans une AggregateException.

Lien vers MSDN

2
roqz

J'ai un problème similaire que vous avez dans le scénario 3 Le scénario de test a échoué en raison du résultat suivant

Expected: <UserDefineException>
But was:  null

en utilisant Assert.ThrowAsync <>, le problème est résolu

Méthode d'action Mon API Web et méthode de test élémentaire comme ci-dessous

public async Task<IHttpActionResult> ActionMethod(RequestModel requestModel)
{
   throw UserDefineException();
}


[Test]
public void Test_Contrller_Method()
{
   Assert.ThrowsAsync<UserDefineException>(() => _controller.ActionMethod(new RequestModel()));
}    
0
Niraj Trivedi