2010-04-29 4 views
9

Sto provando a test dell'unità/verificare che un metodo venga chiamato su una dipendenza, dal sistema in prova (SUT).Test delle unità con Mocks quando SUT sta sfruttando Task Parallel Libaray

  • La depenalizzazione è IFoo.
  • La classe dipendente è IBar.
  • IBar è implementato come barra.
  • La barra chiamerà Start() su IFoo in una nuova attività (System.Threading.Tasks.), Quando Start() viene richiamato sull'istanza Barra.

Unit Test (Moq):

[Test] 
    public void StartBar_ShouldCallStartOnAllFoo_WhenFoosExist() 
    { 
     //ARRANGE 

     //Create a foo, and setup expectation 
     var mockFoo0 = new Mock<IFoo>(); 
     mockFoo0.Setup(foo => foo.Start()); 

     var mockFoo1 = new Mock<IFoo>(); 
     mockFoo1.Setup(foo => foo.Start()); 


     //Add mockobjects to a collection 
     var foos = new List<IFoo> 
         { 
          mockFoo0.Object, 
          mockFoo1.Object 
         }; 

     IBar sutBar = new Bar(foos); 

     //ACT 
     sutBar.Start(); //Should call mockFoo.Start() 

     //ASSERT 
     mockFoo0.VerifyAll(); 
     mockFoo1.VerifyAll(); 
    } 

Attuazione IBar come Bar:

class Bar : IBar 
    { 
     private IEnumerable<IFoo> Foos { get; set; } 

     public Bar(IEnumerable<IFoo> foos) 
     { 
      Foos = foos; 
     } 

     public void Start() 
     { 
      foreach(var foo in Foos) 
      { 
       Task.Factory.StartNew(
        () => 
         { 
          foo.Start(); 
         }); 
      } 
     } 
    } 

Moq Eccezione:

*Moq.MockVerificationException : The following setups were not matched: 
IFoo foo => foo.Start() (StartBar_ShouldCallStartOnAllFoo_WhenFoosExist() in 
FooBarTests.cs: line 19)* 
+2

C'è qualche motivo particolare per non scrivere una semplice simulazione di "IFoo' stesso e usarlo invece? –

risposta

7

@dpurrington & @StevenH: Se cominciamo a mettere questo tipo di cose nel nostro codice

sut.Start(); 
Thread.Sleep(TimeSpan.FromSeconds(1)); 

e abbiamo migliaia di test "unità", allora le nostre prove iniziano a correre nei minuti invece di secondi. Se avessi ad esempio 1000 test unitari, sarà difficile fare eseguire i test in meno di 5 secondi se qualcuno è andato a sparpagliare il codice di prova con Thread.Sleep.

Suggerisco che questa è una cattiva pratica, a meno che non stiamo facendo esplicitamente test di integrazione.

Il mio suggerimento sarebbe quello di utilizzare l'interfaccia System.Concurrency.IScheduler da System.CoreEx.dll e iniettare l'implementazione TaskPoolScheduler.

questo è il mio suggerimento per come questo dovrebbe essere attuata

using System.Collections.Generic; 
using System.Concurrency; 
using Moq; 
using NUnit.Framework; 

namespace StackOverflowScratchPad 
{ 
    public interface IBar 
    { 
     void Start(IEnumerable<IFoo> foos); 
    } 

    public interface IFoo 
    { 
     void Start(); 
    } 

    public class Bar : IBar 
    { 
     private readonly IScheduler _scheduler; 

     public Bar(IScheduler scheduler) 
     { 
      _scheduler = scheduler; 
     } 

     public void Start(IEnumerable<IFoo> foos) 
     { 
      foreach (var foo in foos) 
      { 
       var foo1 = foo; //Save to local copy, as to not access modified closure. 
       _scheduler.Schedule(foo1.Start); 
      } 
     } 
    } 

    [TestFixture] 
    public class MyTestClass 
    { 
     [Test] 
     public void StartBar_ShouldCallStartOnAllFoo_WhenFoosExist() 
     { 
      //ARRANGE 
      TestScheduler scheduler = new TestScheduler(); 
      IBar sutBar = new Bar(scheduler); 

      //Create a foo, and setup expectation 
      var mockFoo0 = new Mock<IFoo>(); 
      mockFoo0.Setup(foo => foo.Start()); 

      var mockFoo1 = new Mock<IFoo>(); 
      mockFoo1.Setup(foo => foo.Start()); 

      //Add mockobjects to a collection 
      var foos = new List<IFoo> 
         { 
          mockFoo0.Object, 
          mockFoo1.Object 
         }; 

      //ACT 
      sutBar.Start(foos); //Should call mockFoo.Start() 
      scheduler.Run(); 

      //ASSERT 
      mockFoo0.VerifyAll(); 
      mockFoo1.VerifyAll(); 
     } 
    } 
} 

Questo permette ora il test per funzionare a piena velocità, senza alcuna Thread.Sleep.

Si noti che i contratti sono stati modificati per accettare un IScheduler nel costruttore Bar (per Dipendenza iniezione) e l'IEnumerable è ora passato al metodo IBar.Start. Spero che questo abbia senso perché ho apportato questi cambiamenti.

La velocità del test è il primo e più ovvio vantaggio di ciò. Il secondo e forse più importante vantaggio è quando si introduce una concorrenza più complessa nel codice, il che rende la verifica notoriamente difficile. L'interfaccia IScheduler e il TestScheduler consentono di eseguire "unit test" deterministici anche in presenza di una concorrenza più complessa.

+0

Sono d'accordo con il punto iniziale sulla mia soluzione. Penso che avrei scelto di usare gli eventi invece di uno schedulatore, ma in entrambi i casi, meglio della mia risposta. – dpurrington

+0

Purtroppo questo non funziona più, poiché tutti i metodi su [System.Threading.Tasks.TaskScheduler] (http://msdn.microsoft.com/en-us/library/system.threading.tasks.taskscheduler.aspx) sono sigillati . –

+2

@Richard: si noti che il tipo da iniettare è l'interfaccia IScheduler implementata da TestScheduler. Non sto cercando di sovrascrivere i metodi su TaskSheduler, sto sfruttando il fatto che sia TaskScheduler che TestScheduler implementano l'interfaccia IScheduler e in quanto tali sono intercambiabili. –

0

i test usa troppi dettagli di implementazione, Tipi IEnumerable<IFoo>. Ogni volta che devo iniziare a testare con IEnumerable crea sempre un certo attrito.

0

Thread.Sleep() è decisamente una cattiva idea. Ho letto su SO più volte che "Le app reali non dormono". Prendilo come vuoi, ma sono d'accordo con questa affermazione. Soprattutto durante i test unitari. Se il codice di test crea falsi errori, i test sono fragili.

Recentemente ho scritto alcuni test che attendono correttamente che le attività parallele finiscano di essere eseguite e ho pensato di condividere la mia soluzione. Mi rendo conto che questo è un vecchio post, ma ho pensato che avrebbe fornito un valore a chi cercava una soluzione.

La mia implementazione comporta la modifica della classe in prova e il metodo in prova.

class Bar : IBar 
{ 
    private IEnumerable<IFoo> Foos { get; set; } 
    internal CountdownEvent FooCountdown; 

    public Bar(IEnumerable<IFoo> foos) 
    { 
     Foos = foos; 
    } 

    public void Start() 
    { 
     FooCountdown = new CountdownEvent(foo.Count); 

     foreach(var foo in Foos) 
     { 
      Task.Factory.StartNew(() => 
      { 
       foo.Start(); 

       // once a worker method completes, we signal the countdown 
       FooCountdown.Signal(); 
      }); 
     } 
    } 
} 

oggetti CountdownEvent sono a portata di mano quando si dispone di più attività in parallelo esecuzione ed è necessario attendere il completamento (come quando aspettiamo di tentare un'asserzione in unit test). Il costruttore si inizializza con il numero di volte che deve essere segnalato prima che segnali il codice in attesa che l'elaborazione sia completa.

Il motivo per cui il modificatore di accesso interno viene utilizzato per CountdownEvent è perché di solito le proprietà e i metodi vengono impostati su interno quando i test di unità devono accedervi. Quindi aggiungo un nuovo attributo assembly nell'assembly sotto il file di prova Properties\AssemblyInfo.cs in modo che gli interni siano esposti a un progetto di test.

[assembly: InternalsVisibleTo("FooNamespace.UnitTests")] 

In questo esempio, FooCountdown attenderà da segnalare 3 volte se ci sono 3 oggetti foo in Foos.

Ora è così che si attende che FooCountdown segnali il completamento dell'elaborazione in modo da poter proseguire con la propria vita e smettere di sprecare cicli cpu su Thread.Sleep().

[Test] 
public void StartBar_ShouldCallStartOnAllFoo_WhenFoosExist() 
{ 
    //ARRANGE 

    var mockFoo0 = new Mock<IFoo>(); 
    mockFoo0.Setup(foo => foo.Start()); 

    var mockFoo1 = new Mock<IFoo>(); 
    mockFoo1.Setup(foo => foo.Start()); 


    //Add mockobjects to a collection 
    var foos = new List<IFoo> { mockFoo0.Object, mockFoo1.Object }; 

    IBar sutBar = new Bar(foos); 

    //ACT 
    sutBar.Start(); //Should call mockFoo.Start() 
    sutBar.FooCountdown.Wait(); // this blocks until all parallel tasks in sutBar complete 

    //ASSERT 
    mockFoo0.VerifyAll(); 
    mockFoo1.VerifyAll(); 
}