2013-03-26 12 views
32

Ho un regolatore di UserController con questa azioneNunit asincrona eccezione prova affermazione

// 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))); 
       }); 
} 

Sto cercando di scrivere un test per il caso in cui butto un'eccezione 404.

Ecco quello che ho provato, con l'uscita -

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>()); 
} 

Risultato test fallito

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)); 
} 

Risultato Test fallito

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)); 
} 

Risultato test fallito

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"); 
} 

Risultato test passa

Questions -

  1. Perché sarebbe Assert.Throws non gestisce HttpResponseException, quando ExpectedException fa?
  2. Non voglio testare solo l'eccezione generata. Voglio far valere il codice di stato della risposta. Qual è il modo di fare questo?

Qualsiasi confronto su questo comportamento e la sua causa/e sarebbe fantastico!

+0

si dovrebbe aggiungere più codice dal tuo pubblico Task Get (stringa domainUserName) come tutti i test sono per DomainUser = "foo" e l'unico errore che hai mostrato è per empty domainUser (o null) – JleruOHeP

+0

@JleruOHeP - grazie -got portato via un po '. Ho modificato il codice. –

+0

Dopo le modifiche tutte le test case sono sempre le stesse? E il caso 1 non fa ancora eccezione? – JleruOHeP

risposta

38

Stai vedendo problemi dovuti alla async void.

In particolare:

1) async() => await userController.Get("foo") viene convertito in TestDelegate, che restituisce void, così il vostro espressione lambda è trattata come async void. Quindi il corridore test inizierà l'esecuzione del lambda ma non aspetterà che si completi. Il lambda ritorna prima dello Get completo (perché è async) e il test runner vede che è ritornato senza eccezioni.

2) Wait avvolge qualsiasi eccezione in un AggregateException.

3) Ancora una volta, la lambda async viene trattata come async void, quindi il test runner non attende il suo completamento.

4) Si consiglia di fare questo async Task anziché async void, ma in questo caso il test runner attende il completamento e quindi vede l'eccezione.

Secondo this bug report, c'è una soluzione per questo arrivo nella prossima build di NUnit. Nel frattempo, puoi creare il tuo metodo ThrowsAsync; un example for xUnit is here.

+0

Grazie - ho il sospetto di un bug, felice che sia confermato. Userò l'approccio 'ThrowsAsync', sembra molto più pulito di quello che ho ora. –

+0

Il tuo approccio funziona bene, ho dovuto estendere l'idea di ThrowsAsync per aggiungere funzionalità di assert, ma non era troppo difficile. Ho aggiornato la mia risposta qui sotto con quello che ho adesso. Grazie ancora. –

+1

Il bug è stato risolto dal 2.6.3 – DalSoft

2

Se si attende un'attività, le eccezioni generate vengono aggregate in AggregateException. È possibile controllare le eccezioni interne di AggregateException. Questo potrebbe essere il motivo per cui il caso 2 non funziona.

Le eccezioni non gestite generate dal codice utente in esecuzione in un'attività vengono propagate al thread di collegamento, tranne in alcuni scenari descritti più avanti in questo argomento. Le eccezioni vengono propagate quando si utilizza uno dei metodi statici o di istanza Task.Wait o Task.Wait e li si gestisce racchiudendo la chiamata in un'istruzione try-catch. Se un'attività è l'elemento principale delle attività secondarie associate o se si stanno eseguendo più attività, potrebbero essere generate più eccezioni. Per propagare tutte le eccezioni al thread chiamante, l'infrastruttura delle attività le racchiude in un'istanza AggregateException. AggregateException ha una proprietà InnerExceptions che può essere enumerata per esaminare tutte le eccezioni originali che sono state lanciate e gestire (o non gestire) ognuna individualmente. Anche se viene lanciata solo un'eccezione, viene comunque racchiusa in un'eccezione AggregateException.

Link to MSDN

+0

Sì, è vero. Non volevo esaminare 'AggregateException' per verificare se' HttpResponseException' è stata lanciata, ma sembra che non ci sia un'opzione? –

+0

Non credo che ci sia un modo per aggirare l'eccezione di AggregateException, ma penso che non sia così male. – roqz

11

This blog parla di problemi simili al mio.

ho seguito la raccomandazione proposta lì, e hanno un test come questo -

[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)); 
    } 

non sono troppo soddisfatti, ma questo funziona.

EDIT 1

Ispirato da @StephenCleary, ho aggiunto una classe di supporto statica che fa il sostiene che sto cercando. Ecco come si presenta -

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); 
    } 
} 

ora posso avere un test come -

[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); 
    } 
19

non sono sicuro quando è stato aggiunto, ma la versione attuale di NUnit (3.4.1 al momento della scrittura) include un metodo ThrowsAsync

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

non ho ancora testato questo esempio in particolare, ma dovrebbe funzionare in questo modo:

[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() }; 

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

    Assert.That(exception.Response.StatusCode, Is.EqualTo(HttpStatusCode.NotFound)); 
} 
+4

Questa risposta dovrebbe essere al top in modo che le persone non perdano tempo a provare tutte le soluzioni personalizzate. La risposta è già istantaneamente integrata in NUnit –

+0

Phew, sono contento di non aver smesso di scorrere verso il basso :) –

+0

Quando si utilizza Assert.ThrowsAsync <> non penso che il test debba essere asincrono (in questo caso). Basta renderlo vuoto. – nashwan

Problemi correlati