2010-04-29 10 views
9

Próbuję przeprowadzić test jednostkowy/sprawdzić, czy metoda jest wywoływana w zależności od testowanego systemu (SUT).Testowanie jednostkowe za pomocą Mocks, gdy SUT korzysta z zadania Równoległego Libaray

  • Depedency to IFoo.
  • Klasa zależna to IBar.
  • IBar jest zaimplementowany jako pasek.
  • Pasek wywoła Start() w IFoo w nowym zadaniu (System.Threading.Tasks.), Gdy funkcja Start() zostanie wywołana w instancji Bar.

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

Realizacja IBAR jak 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 Wyjątek:

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

Czy istnieje jakiś szczególny powód, dla którego nie należy napisać prostej, próbnej implementacji 'IFoo', a zamiast tego użyć? –

Odpowiedz

7

@dpurrington & @StevenH: Jeśli rozpocząć wprowadzanie tego rodzaju rzeczy w naszym kodzie

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

i mamy tysiące „unit” testów wtedy nasze testy zaczną w protokole zamiast sekund. Jeśli miałeś na przykład 1000 testów jednostkowych, ciężko będzie przeprowadzić testy w mniej niż 5 sekund, jeśli ktoś odszedł i zaśmiecił bazę kodu testowego za pomocą Thread.Sleep.

Sugeruję, że jest to zła praktyka, chyba że wyraźnie przeprowadzamy testy integracji.

Moja sugestia to użycie interfejsu System.Concurrency.IScheduler z System.CoreEx.dll i wstrzyknięcie implementacji TaskPoolScheduler.

To jest moja propozycja dla jak to powinno być realizowane

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

Pozwala to teraz test, aby uruchomić z pełną prędkością bez Thread.Sleep.

Należy zauważyć, że zamówienia zostały zmodyfikowane, aby zaakceptować moduł ISheduler w konstruktorze prętów (w przypadku iniekcji zależnej), a wartość IEnumerable została teraz przekazana do metody IBar.Start. Mam nadzieję, że to ma sens, dlaczego wprowadziłem te zmiany.

Szybkość testowania jest pierwszą i najbardziej oczywistą korzyścią z tego. Druga i być może ważniejsza korzyść z tego jest taka, kiedy wprowadzasz bardziej złożoną współbieżność do swojego kodu, co sprawia, że ​​testowanie jest bardzo trudne. Interfejs IScheduler i TestScheduler umożliwiają prowadzenie deterministycznych "testów jednostkowych" nawet w obliczu bardziej złożonej współbieżności.

+0

Zgadzam się z twoim początkowym punktem dotyczącym mojego rozwiązania. Myślę, że wybrałbym użycie zdarzeń zamiast harmonogramu, ale tak czy inaczej, lepiej niż moja odpowiedź. – dpurrington

+0

Niestety to już nie działa, ponieważ wszystkie metody w [System.Threading.Tasks.TaskScheduler] (http://msdn.microsoft.com/en-us/library/system.threading.tasks.taskscheduler.aspx) są zapieczętowane . –

+2

@Richard: Należy zauważyć, że wtryskiwanym typem jest interfejs IScheduler, który implementuje przyrząd TestScheduler. Nie próbuję przesłonić metod na TaskSheduler, jestem wykorzystanie faktu, że zarówno TaskScheduler i TestScheduler implementować interfejs IScheduler i jako takie są wymienne. –

0

Twoje testy używa zbyt wielu szczegółów implementacji, IEnumerable<IFoo> typy. Ilekroć muszę zacząć testować z IEnumerable zawsze tworzy jakieś tarcie.

0

Thread.Sleep() to zdecydowanie zły pomysł. Czytałem na SO kilka razy nad tym, że "Prawdziwe aplikacje nie śpią". Weź to jak chcesz, ale zgadzam się z tym stwierdzeniem. Zwłaszcza podczas testów jednostkowych. Jeśli twój kod testowy tworzy fałszywe błędy, twoje testy są kruche.

Napisałem ostatnio kilka testów, które odpowiednio czekają, aż zadania równoległe zakończą wykonywanie i pomyślałem, że udostępnię moje rozwiązanie. Zdaję sobie sprawę, że jest to stary post, ale myślałem, że przyniesie on wartość tym, którzy szukają rozwiązania.

Moja realizacja polega na modyfikacji badanej klasy i testowanej metody.

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

obiekty CountdownEvent są przydatne, gdy masz kilka równoległych zadań wykonującego i trzeba poczekać na zakończenie (jak wtedy, gdy czekamy próbować się dochodzić w testach jednostkowych). Konstruktor inicjuje tyle razy, ile powinien być sygnalizowany, zanim zasygnalizuje to oczekiwaniu, że przetwarzanie zostało zakończone.

Powodem, dla którego modyfikator dostępu wewnętrznego jest używany w Odliczaniu czasu, jest to, że zazwyczaj ustawiam właściwości i metody na wewnętrzne, gdy testy jednostkowe wymagają dostępu do nich. Następnie dodajemy nowy atrybut zespołu w pliku testowym pod numerem Properties\AssemblyInfo.cs, aby wewnętrzne obiekty były wystawione na projekt testowy.

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

W tym przykładzie FooCountdown będzie czekać być sygnalizowane 3 razy, jeśli istnieją 3 obiekty Foo w Foo.

Teraz tak czekasz, aż FooCountdown zasygnalizuje zakończenie przetwarzania, abyś mógł kontynuować życie i przestać marnować cykle procesora na 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(); 
} 
Powiązane problemy