Generator danych Faker.net

Każdy z nas jest choć trochę leniwy. Jedni troszkę mniej, inni troszkę bardziej. Ja na ten przykład czasem się rozpędzam i piszę kod, który potrzebuje. Potem o nim opowiadam, a potem ktoś pyta czemu nie skorzystałem z jakiejś tam gotowej biblioteki. Wiem, że każdy z nas należy do mensy i wie, że ten wpis nie wziął się z powietrza.
Dzisiejszy wpis dedykuje koledze Ievgenowi (blog lub @ibezuglyi)(dla ktorego specjalnie załatwiłem sobie literki, żeby wpisać jego imię do komputera), który to pokazał mi Fakera – nie palec, ale taką bibliotekę. Służy ona do generowania różnych, ale skończonych, losowych danych.
Dane podzielone są tematycznie na:

  • ArrayFaker
  • BooleanFaker
  • CompanyFaker
  • DateTimeFaker
  • EnumFaker
  • InternetFaker
  • LocationFaker
  • NameFaker
  • NumberFaker
  • PhoneFaker
  • StringFaker
  • TextFaker
W każdej znajdą się ciekawe metody, które bardzo upraszczają, ułatwiają i przyspieszają tworzenie testów, lub przynajmniej losowych wpisów.
Nowy użytkownik w naszym serwisie to taki kod:

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: Consolas, “Courier New”, Courier, Monospace;
background-color: #ffffff;
/*white-space: pre;*/
}

.csharpcode pre { margin: 0em; }

.csharpcode .rem { color: #008000; }

.csharpcode .kwrd { color: #0000ff; }

.csharpcode .str { color: #a31515; }

.csharpcode .op { color: #0000c0; }

.csharpcode .preproc { color: #cc6633; }

.csharpcode .asp { background-color: #ffff00; }

.csharpcode .html { color: #800000; }

.csharpcode .attr { color: #ff0000; }

.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}

.csharpcode .lnum { color: #606060; }

   1:  var user = new UserAccount
   2:  {
   3:      Email = Faker.InternetFaker.Email() + Faker.NumberFaker.Number(),
   4:      Password = Faker.NumberFaker.Number(1000000000, 2000000000).ToString()
   5:  };

Ponieważ maile mogą się powtarzać, dodajemy jeszcze więcej losowości. Hasło nie przejdzie testów na trudność, ale będzie choć trochę losowe. Czaicie prostotę i moc? Następnym razem, gdy będziecie pisać własną bibliotekę do generowania losowych danych skorzystajcie z Fakera. A jak będziecie pisać inną bibliotekę, wcześniej skorzystajcie z googla LUB (znowu od Ievgena) http://nugetmusthaves.com.
Tym razem udało się krótko. Inne biblioteki warte polecenia?

Testowanie klas abstrakcyjnych

Pewnie każdy na swojej ścieżce programistycznej spotkał się z klasą abstrakcyjną. Wrzucamy tam kod, który zdaje się być domyślną implementacją pewnej grupy klas i szkoda nam kopiować tego zachowania do każdej z nich osobna. Skoro wszystkie zachowują się podobny sposób, czasem tylko dodając coś od siebie, to warto wykorzystać dziedziczenie i napisać mniej (DRY). Czyli prawdopodobny wydaje się taki scenariusz:

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: Consolas, “Courier New”, Courier, Monospace;
background-color: #ffffff;
max-height: 500px;
overflow: auto;
/*white-space: pre;*/
}

.csharpcode pre { margin: 0em; }

.csharpcode .rem { color: #008000; }

.csharpcode .kwrd { color: #0000ff; }

.csharpcode .str { color: #a31515; }

.csharpcode .op { color: #0000c0; }

.csharpcode .preproc { color: #cc6633; }

.csharpcode .asp { background-color: #ffff00; }

.csharpcode .html { color: #800000; }

.csharpcode .attr { color: #ff0000; }

.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}

.csharpcode .lnum { color: #606060; }

   1:  namespace AbstractTesting
   2:  {
   3:      public abstract class AbstractClass
   4:      {
   5:          public virtual int Method(int input)
   6:          {
   7:              switch (input)
   8:              {
   9:                  case 1:
  10:                      return 1;
  11:                  case 2:
  12:                      return 4;
  13:                  case 3:
  14:                      return 8;
  15:                  default:
  16:                      return 0;
  17:              }
  18:          }
  19:      }
  20:   
  21:      public class ConcreteClass1 : AbstractClass
  22:      {
  23:          public override int Method(int input)
  24:          {
  25:              switch (input)
  26:              {
  27:                  case 1:
  28:                      return -1;
  29:                  default:
  30:                      return base.Method(input);
  31:              }
  32:   
  33:          }
  34:      }
  35:   
  36:      public class ConcreteClass2 : AbstractClass
  37:      {
  38:          public override int Method(int i)
  39:          {
  40:              switch (i)
  41:              {
  42:                  case 4:
  43:                      return 16;
  44:                  default:
  45:                      return base.Method(i);
  46:   
  47:              }
  48:          }
  49:      }
  50:   
  51:      public class ConcreteClass3 : AbstractClass
  52:      {
  53:      }
  54:   
  55:      public class ConcreteClass4 : AbstractClass
  56:      {
  57:          public override int Method(int i)
  58:          {
  59:              switch (i)
  60:              {
  61:                  case 1:
  62:                      return -1;
  63:                  case 2:
  64:                      return -2;
  65:                  case 3:
  66:                      return -3;
  67:                  default:
  68:                      return base.Method(i);
  69:              }
  70:          }
  71:      }
  72:   
  73:      public class ConcreteClass5 : AbstractClass
  74:      {
  75:          public override int Method(int i)
  76:          {
  77:              switch (i)
  78:              {
  79:                  case 1:
  80:                      return -1;
  81:                  case 2:
  82:                      return -4;
  83:                  case 3:
  84:                      return -8;
  85:                  default:
  86:                      return 1;
  87:              }
  88:          }
  89:      }
  90:  }

Celowo pomijam dodatkowe metody w klasie bazowej i klasach pochodnych, nie na tym chce się tutaj skupić.
Teraz można zadać sobie pytanie, jak mam to przetestować? Przynajmniej ja je sobie zadawałem. Czy testować w każdej klasie tylko to co wnosi ona nowego, a dla klasy klasy bazowej napisać specjalnego mocka, który umożliwi stworzenie jej instancji, a następnie przetestowanie wspólnego kodu. Czy też pominąć w testach klasę bazową, ale dla każdej z klas dziedziczących pisać powtarzający się test, sprawdzający wszystkie warunki switcha? Jest też trzecie rozwiązanie tego problemu, sugerując się podpowiedziami kolegów z pracy, trzeba być wystarczająco odważnym, wrzucić na produkcję i zobaczyć czy zabangla. Ale tego przykładu nie będę tutaj analizować.

Zamiast rozwodzić się nad tym, co jest lepsze, a co gorsze, sprawdzimy jak to wychodzi w praniu. Pierwsze podejście, napisać mniej testów:

   1:  [TestFixture]
   2:  public class AbstractTestClass
   3:  {
   4:      [Test]
   5:      public void t0_1()
   6:      {
   7:          var sut = new MockAbstractClass();
   8:          Assert.AreEqual(1, sut.Method(1));
   9:      }
  10:   
  11:      [Test]
  12:      public void t1_4()
  13:      {
  14:          var sut = new MockAbstractClass();
  15:          Assert.AreEqual(4, sut.Method(2));
  16:      }
  17:   
  18:      [Test]
  19:      public void t3_8()
  20:      {
  21:          var sut = new MockAbstractClass();
  22:          Assert.AreEqual(8, sut.Method(3));
  23:      }
  24:   
  25:      [Test]
  26:      public void t4_0()
  27:      {
  28:          var sut = new MockAbstractClass();
  29:          Assert.AreEqual(0, sut.Method(4));
  30:      }
  31:  }
  32:   
  33:  public class MockAbstractClass : AbstractClass
  34:  {
  35:  }
  36:   
  37:  [TestFixture]
  38:  public class TestC1
  39:  {
  40:      [Test]
  41:      public void t0_1()
  42:      {
  43:          var sut = new ConcreteClass1();
  44:          Assert.AreEqual(-1, sut.Method(1));
  45:      }
  46:  }
  47:   
  48:  [TestFixture]
  49:  public class TestC2
  50:  {
  51:      [Test]
  52:      public void t0_16()
  53:      {
  54:          var sut = new ConcreteClass2();
  55:          Assert.AreEqual(16, sut.Method(4));
  56:      }
  57:  }
  58:   
  59:  [TestFixture]
  60:  public class TestC4
  61:  {
  62:      [Test]
  63:      public void t1_m1()
  64:      {
  65:          var sut = new ConcreteClass4();
  66:          Assert.AreEqual(-1, sut.Method(1));
  67:      }
  68:   
  69:      [Test]
  70:      public void t2_m2()
  71:      {
  72:          var sut = new ConcreteClass4();
  73:          Assert.AreEqual(-2, sut.Method(2));
  74:      }
  75:   
  76:      [Test]
  77:      public void t3_m3()
  78:      {
  79:          var sut = new ConcreteClass4();
  80:          Assert.AreEqual(-3, sut.Method(3));
  81:      }
  82:   
  83:      [Test]
  84:      public void t4_0()
  85:      {
  86:          var sut = new ConcreteClass4();
  87:          Assert.AreEqual(0, sut.Method(4));
  88:      }
  89:  }
  90:   
  91:  [TestFixture]
  92:  public class TestC5
  93:  {
  94:      [Test]
  95:      public void t1_m1()
  96:      {
  97:          var sut = new ConcreteClass5();
  98:          Assert.AreEqual(-1, sut.Method(1));
  99:      }
 100:   
 101:      [Test]
 102:      public void t2_m2()
 103:      {
 104:          var sut = new ConcreteClass5();
 105:          Assert.AreEqual(-4, sut.Method(2));
 106:      }
 107:   
 108:      [Test]
 109:      public void t3_m3()
 110:      {
 111:          var sut = new ConcreteClass5();
 112:          Assert.AreEqual(-8, sut.Method(3));
 113:      }
 114:   
 115:      [Test]
 116:      public void t4_0()
 117:      {
 118:          var sut = new ConcreteClass5();
 119:          Assert.AreEqual(1, sut.Method(16));
 120:      }
 121:  }

Pokrycie kodu powinno być 100%. Wszystkie test powinny przejść na zielono. Skoro wszystko działa dobrze, to po co się zastanawiać na innym rozwiązaniem? A to dlatego, drogie dzieciaczki, że jeśli wprowadzicie teraz jakąś zmianę w klasie Abstract, to wszystkie testy klas pochodnych nadal będą się świecić na zielono. Pomimo tego, że zmieni się ich zachowanie. W ten sposób nie dowiecie się (z testów), że np. klasa Concret3 od teraz zachowuje się zupełnie inaczej, inne korzystające z domyślnego zachowania też oszalały. Ktoś może powiedzieć, że to poprawne zachowanie, skoro dziedziczysz po czymś/kimś i zdajesz się na jego domyślną implementacje to zmieniasz się razem z nią jak chorągiewka na wietrze. Tylko czy o to chodzi? Czy klasy nie powinny mieć własnego rozumku? Klient korzysta z implementacji klasy 1,2,3,4 czy 5 a nie klasy podstawowej. W takiej sytuacji każda klasa, a w zasadzie test dla każdej z klas powinien wykryć zmiany w jej zachowaniu, poinformować o tym, że zostało ono zmienione i od teraz nie spełnia wcześniej przyjętego rozumowania. Moim zdaniem tak powinny wyglądać testy:

   1:  namespace AbstractTesting
   2:  {
   3:      [TestFixture]
   4:      public class ConcreteClass1Test
   5:      {
   6:          [Test]
   7:          public void T001_M1()
   8:          {
   9:              var sut = new ConcreteClass1();
  10:   
  11:              var result = sut.Method(1);
  12:   
  13:              Assert.AreEqual(-1, result);
  14:          }
  15:   
  16:          [Test]
  17:          public void T002_M2()
  18:          {
  19:              var sut = new ConcreteClass1();
  20:   
  21:              var result = sut.Method(2);
  22:   
  23:              Assert.AreEqual(4, result);
  24:          }
  25:   
  26:          [Test]
  27:          public void T003_M3()
  28:          {
  29:              var sut = new ConcreteClass1();
  30:   
  31:              var result = sut.Method(3);
  32:   
  33:              Assert.AreEqual(8, result);
  34:          }
  35:   
  36:          [Test]
  37:          public void T004_M155()
  38:          {
  39:              var sut = new ConcreteClass1();
  40:   
  41:              var result = sut.Method(155);
  42:   
  43:              Assert.AreEqual(0, result);
  44:          }
  45:      }
  46:   
  47:      [TestFixture]
  48:      public class ConcreteClass2Test
  49:      {
  50:          [Test]
  51:          public void T001_M1()
  52:          {
  53:              var sut = new ConcreteClass2();
  54:   
  55:              var result = sut.Method(1);
  56:   
  57:              Assert.AreEqual(1, result);
  58:          }
  59:   
  60:          [Test]
  61:          public void T002_M2()
  62:          {
  63:              var sut = new ConcreteClass2();
  64:   
  65:              var result = sut.Method(2);
  66:   
  67:              Assert.AreEqual(4, result);
  68:          }
  69:   
  70:          [Test]
  71:          public void T003_M3()
  72:          {
  73:              var sut = new ConcreteClass2();
  74:   
  75:              var result = sut.Method(3);
  76:   
  77:              Assert.AreEqual(8, result);
  78:          }
  79:   
  80:          [Test]
  81:          public void T004_M4()
  82:          {
  83:              var sut = new ConcreteClass2();
  84:   
  85:              var result = sut.Method(4);
  86:   
  87:              Assert.AreEqual(16, result);
  88:          }
  89:   
  90:          [Test]
  91:          public void T005_M55()
  92:          {
  93:              var sut = new ConcreteClass2();
  94:   
  95:              var result = sut.Method(55);
  96:   
  97:              Assert.AreEqual(0, result);
  98:          }
  99:      }
 100:   
 101:      [TestFixture]
 102:      public class ConcreteClass3Test
 103:      {
 104:          [Test]
 105:          public void T001_M1()
 106:          {
 107:              var sut = new ConcreteClass3();
 108:   
 109:              var result = sut.Method(1);
 110:   
 111:              Assert.AreEqual(1, result);
 112:          }
 113:   
 114:          [Test]
 115:          public void T002_M2()
 116:          {
 117:              var sut = new ConcreteClass3();
 118:   
 119:              var result = sut.Method(2);
 120:   
 121:              Assert.AreEqual(4, result);
 122:          }
 123:   
 124:          [Test]
 125:          public void T003_M3()
 126:          {
 127:              var sut = new ConcreteClass3();
 128:   
 129:              var result = sut.Method(3);
 130:   
 131:              Assert.AreEqual(8, result);
 132:          }
 133:   
 134:          [Test]
 135:          public void T004_M55()
 136:          {
 137:              var sut = new ConcreteClass3();
 138:   
 139:              var result = sut.Method(55);
 140:   
 141:              Assert.AreEqual(0, result);
 142:          }
 143:      }
 144:   
 145:      [TestFixture]
 146:      public class ConcreteClass4Test
 147:      {
 148:          [Test]
 149:          public void T001_M1()
 150:          {
 151:              var sut = new ConcreteClass4();
 152:   
 153:              var result = sut.Method(1);
 154:   
 155:              Assert.AreEqual(-1, result);
 156:          }
 157:   
 158:          [Test]
 159:          public void T002_M2()
 160:          {
 161:              var sut = new ConcreteClass4();
 162:   
 163:              var result = sut.Method(2);
 164:   
 165:              Assert.AreEqual(-2, result);
 166:          }
 167:   
 168:          [Test]
 169:          public void T003_M3()
 170:          {
 171:              var sut = new ConcreteClass4();
 172:   
 173:              var result = sut.Method(3);
 174:   
 175:              Assert.AreEqual(-3, result);
 176:          }
 177:   
 178:          [Test]
 179:          public void T004_M55()
 180:          {
 181:              var sut = new ConcreteClass4();
 182:   
 183:              var result = sut.Method(55);
 184:   
 185:              Assert.AreEqual(0, result);
 186:          }
 187:      }
 188:   
 189:      [TestFixture]
 190:      public class ConcreteClass5Test
 191:      {
 192:          [Test]
 193:          public void T001_M1()
 194:          {
 195:              var sut = new ConcreteClass5();
 196:   
 197:              var result = sut.Method(1);
 198:   
 199:              Assert.AreEqual(-1, result);
 200:          }
 201:   
 202:          [Test]
 203:          public void T002_M2()
 204:          {
 205:              var sut = new ConcreteClass5();
 206:   
 207:              var result = sut.Method(2);
 208:   
 209:              Assert.AreEqual(-4, result);
 210:          }
 211:   
 212:          [Test]
 213:          public void T003_M3()
 214:          {
 215:              var sut = new ConcreteClass5();
 216:   
 217:              var result = sut.Method(3);
 218:   
 219:              Assert.AreEqual(-8, result);
 220:          }
 221:   
 222:          [Test]
 223:          public void T004_M55()
 224:          {
 225:              var sut = new ConcreteClass5();
 226:   
 227:              var result = sut.Method(55);
 228:   
 229:              Assert.AreEqual(1, result);
 230:          }
 231:      }
 232:  }

W tym przypadku gdy zmieni się zachowanie klasy podstawowej, wszystkie testy klas korzystających z domyślnej implementacji, automatycznie zgłoszą niezadowolenie ze zmian.
Nic za darmo, w drugim przypadku trzeba napisać więcej testów, a czasem nawet go powtórzyć. Oczywiście można też pozbyć się tego switcha z kodu, albo wyjechać do Niemiec i zbierać ogórki – byle by tylko nie pisać testów. Tutaj miałem na celu pokazanie tego, jak zachowują się oba rozwiązania i które z nich, moim zdaniem jest tym słuszniejszym.

Jak zwykle pozostaje otwarty na krytykę i zastrzegam sobie prawo do popełniania błędów.

NDepend4 – co potrafi statyczna analiza kodu.

Dostałem NDepend4 w zamian za jego opis 🙂 Nie muszę o nim mówić w samych słodkich komentarzach, także można wpis czytać do końca.
NDepend służy to statycznej analizy kodu napisanego w .NET. Potrafi ocenić kod po wieloma względami, np: ze względu na skomplikowanie, ilość linii kod czy instrukcji IL, ilość zmiennych, łatwość modyfikacji, może zasugerować zmianę typów z referencyjnych na wartościowe (ref type –> value type).
Sama instalacja jest banalnie prosta, ściągamy paczkę ze strony, rozpakowujemy oraz wrzucamy plik z licencją :> Chyyyba że jej nie macie, wtedy można skorzystać z darmowej wersji 14 dniowej. To wszystko, potem wystarczy odpalić i załadować plik z solucją, którą chcielibyśmy się bliżej przyjrzeć.

Do opisu wykorzystam swoją pierwszą aplikację na Windows Phone. Trochę wstyd, ale dla dobra ludzkości trzeba się czasem trochę poświęcić. Przy okazji, jest ona dostępna na Baazar (nieoficjalny marketplace dla Windows Phone 7) – nazywa się CompactCal

Po uruchominiu NDepend można załadować plik solucji, którą chcemy poddać analizie. Następnie zostanie wyświetlona lista assemblies, które zostaną przeanalizowane:

Jak widać może się okazać, że czegoś będzie brakować, wtedy można dodać ręcznie brakujące pliki (np. używając drag’n’drop) walnąć OK. Warto zwrócić uwagę z której konfiguracji będą brane: debug czy release.

Czary mary, czekamy chwilę i wybieram opcje Close dialog – chce wszystko sam zobaczyć.
Pierwsze ciary na plecach, bo widzę bałagan w kodzie. Wszytko przez diagram zależności klas, u mnie wygląda on tak:

(Trochę się obrazek rozjechał)
Ci z nas, którzy posiadają Visual Studio wyższą niż expres (i chyba pro też), mogą znać już taki obrazek. Podobny widok można utworzyć w MVSC z menu Architecture -> Generate Dependency Graph -> By Assembly.
Ogólnie rzecz ujmując widać na nim, co od czego zależy i jak bardzo, im grubsza kreska tym więcej zależności w kodzie. Może być taka sytuacja że już tutaj zauważymy zależności których
nie powinno być. Może z jakiegoś powodu nasz View sięga bezpośrednio
do serwisów z danymi, a miał to robić tylko ViewModel. Po rozwiązaniu takich problemów, ja najczęściej usuwam  zależności od systemowy plików, tak jest dla mniej przejrzyściej.
Niestety ND (żeby nie pisać ciągle całej nazwy) nie potrafi sobie poradzić z ułożeniem kwadracików tak, aby linie się nigdzie nie przecinały. Nad czytelnością trzeba diagramu trzeba popracować samodzielnie. Nie wiem czemu, ale lubię właśnie w ten sposób mieć ułożone wszelkie diagramy.
Kolejnym innym widokiem zależności jest Matrix view, jest czytelniejszy niż diagram powyżej, w przypadkach gdy jest bardzo dużo obiektów do analizy.

U mnie tego nie widać, ale gdy pojawią się cyferki na czarnym tle, oznaczać to będzie zależności cykliczne. Choć muszę się przyznać, że gdy po raz pierwszy uruchomiłem ND na tym projekcie pewnie znalazło by się kilka czarnych miejsc. Na szczęście od tamtego czasu zdążyłem poprawić kod, a ten wpis powstaje dużo później. Widać na nim dokładnie zależności modułów, możemy badać zależność na podstawie ilości wykorzystywanych metod, pól, namespaces, typów zmiennych, i jeszcze kilku innych. ND potrafi także wykryć zależności nie bezpośrednie.
Po kliknięciu na jedno ze skrzyżowań, zobaczymy jeszcze dokładniejszy diagram tego, gdzie i jak są wykorzystywane te zależności, np. ViewModel vs. Helpers:

Do tego można wygenerować ciekawszy graph (Export Matrix Code Elements to Graph):

Co w wyniku da coś takiego:

W tym przypadku diagram zależności jest czytelny (linie się nigdzie nie przecinają).

To dopiero początek, jeśli już pogodziliśmy się ze skalą zależności w naszym kodzie warto przejść dalej i zobaczyć jakie reguły złamaliśmy oraz gdzie. ND dostarcza ponad 80 zdefiniowanych reguł. Możemy samodzielnie je modyfikować, dodawać nowe oraz usuwać te z nich , które naszym lub architekta odpowiedzialnego za projekt są z jakiegoś powodu nie potrzebne.

Tak bardzo źle nie jest, brak czerwonych kółeczek.

Jak widać powyżej istnieje kilka różnych grup zasad, z których część została w brzydki sposób naruszona, a część jest akceptowalna. Zobaczmy co zrobiłem źle, sprawdźmy Code Quality (dwuklik i widać szczegóły):

Każda grupa składa się znowu z kilku mniejszych reguł, tutaj jak widać mam najwyraźniej problem ze zbyt dużymi klasami i metodami. Brakiem komentarzy się nie przejmuje. Mój kod komentuje się sam – tak go wytresowałem.

Ważne jest to że parametry definiujące te reguły możemy samodzielnie zmieniać. Dla przykładu domyślne aby metoda została oznaczona za posiadającą zbyt duża ilość parametrów,  musi posiadać ich więcej niż pięć:

Metody zbyt duże muszą mieć więcej niż 30 linii kodu lub więcej niż 200 instrukcji IL.

Gdy już zdecydujemy się obejrzeć listę przewinień służy do tego osobny panel, ja sprawdzę którą z metod tak strasznie spasłem kodem:

Ja mam dwa takie przypadki, gdzie ten drugi jest bliski wartości granicznej. Aby obejrzeć kod wystarczy dwuklik na wybranym przypadku. W wersji darmowej trzeba niestety ręcznie znaleźć kod w MSVC.

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: Consolas, “Courier New”, Courier, Monospace;
background-color: #ffffff;
/*white-space: pre;*/
}

.csharpcode pre { margin: 0em; }

.csharpcode .rem { color: #008000; }

.csharpcode .kwrd { color: #0000ff; }

.csharpcode .str { color: #a31515; }

.csharpcode .op { color: #0000c0; }

.csharpcode .preproc { color: #cc6633; }

.csharpcode .asp { background-color: #ffff00; }

.csharpcode .html { color: #800000; }

.csharpcode .attr { color: #ff0000; }

.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}

.csharpcode .lnum { color: #606060; }

   1:  public static WriteableBitmap GenerateBitmapImage(string aHeading, IEnumerable<string> aItems)
   2:  {
   3:      WriteableBitmap wbm = null;
   4:      StackPanel sp = new StackPanel();
   5:      sp.Margin = new Thickness { Left = 6, Bottom = 0, Right = 6, Top = 0 };
   6:      sp.Height = 173;
   7:      sp.Width = 173;
   8:      sp.Background = new SolidColorBrush(Colors.Transparent);
   9:   
  10:      if (!string.IsNullOrEmpty(aHeading))
  11:      {
  12:          TextBlock heading = new TextBlock();
  13:          heading.Style = (Style)Application.Current.Resources["PhoneTextTitle3Style"];
  14:   
  15:          heading.Text = aHeading;
  16:          heading.Height = 35;
  17:          heading.Width = 173;
  18:   
  19:          sp.Children.Add(heading);
  20:      }
  21:   
  22:      int maxitems = (string.IsNullOrEmpty(aHeading)) ? 5 : 6;
  23:      int c = 0;
  24:   
  25:      foreach (var item in aItems)
  26:      {
  27:          TextBlock tbAction = new TextBlock();
  28:          tbAction.Foreground = (SolidColorBrush)Application.Current.Resources["PhoneAccentBrush"];
  29:          tbAction.Text = item;
  30:          tbAction.Style = (Style)Application.Current.Resources["PhoneTextNormalStyle"];
  31:          tbAction.Margin = new Thickness(4, 0, 0, 0);
  32:          tbAction.Height = 27;
  33:          tbAction.Width = 173;
  34:          sp.Children.Add(tbAction);
  35:          if (c++ > maxitems)
  36:          {
  37:              break;
  38:          }
  39:      }
  40:   
  41:      //call measure, arrange and updatelayout to prepare for rendering
  42:      sp.Measure(new Size(173, 173));
  43:      sp.Arrange(new Rect(0, 0, 173, 173));
  44:      sp.UpdateLayout();
  45:   
  46:      wbm = new WriteableBitmap(173, 173);
  47:      wbm.Render(sp, null);
  48:      wbm.Invalidate();
  49:      return wbm;
  50:  }

Rzeczywiście całkiem sporo, kod z bloków IF oraz FOREACH można wyrzucić do osobnych metod, tak samo ostatnie linijki z kodem który wymusza render na bitmapie. Poprawię to później 😉
To tylko jeden z wielu przypadków, który jest możliwy do przeanalizowania w kodzie. Jak już napisałem ND domyślnie ma ich ponad 80. Na stronie domowej są przykłady jak pisać własne CQL (code query language) i analizować kod na inny sposób, np:

  • Ostrzegaj gdy code coverage poniżej n%
  • Nazwy interfejsów nie zaczynające się od litery I
  • Wykorzystanie wątków inaczej niż przez ThreadPool
  • Zmiany wartości zmiennych, bez wykorzystania mechanizmów synchronizacji
  • Zbyt długa lista typów dziedziczących po X

Oprócz tego co wymieniłem powyżej, generowany jest także dodatkowy raport podsumowujący cały projekt, wykresy, liczby, inne pierdółki, coś co menadżerowie i klienci lubią najbardziej.
Skupmy się na najważniejszej grupie, na programistach.Są tam także informacje, które nam także się przydadzą.
Dodatkowy wykres wyświetlający stopień abstrakcji oraz stabilność waszego kodu. Gdzie stabilność nie oznacza dobrego kodu, a raczej współpracę z innymi typami (wsparcie dla polimorfizmu). W zasadzie ciężko wytłumaczyć ten parametr. Scott Hanselman ma podobny wpis o ND, tam tłumaczy trochę lepiej ten wykres: post Scotta.
Teraz będzie siara, bo o ile jestem zwolennikiem abstrakcji, to w tym projekcie pojechałem bez niej:

Warto trzymać się zielonej strefy.
I tak Service oraz GeocodeService był już tworzony z myślą o testach czy zastosowaniem innego źródła danych, dlatego znajduje się wyżej jeśli chodzi o oś Y (abstrakcję), natomiast Model, Extension (tam są metody rozszerzające), Helpery, etc jest płaskie jak deska. Położenie na osi X działa w taki sposób: jeśli assembly jest nie rozszerzalne, brakuje mu/jej jakiejkolwiek wirtualności, a dodatkowo wiele innych typów od niej zależy tym bliżej strefy bólu (na lewo) będzie się znajdować. U mnie wygląda to tak, że kod nie jest abstrakcyjny (są momenty), jednak nie posiadam klas które są wykorzystywane przez wszystkie inne. Brak tutaj typowych “utilsów” czy klas statycznych, do których wszyscy sięgają. Może dzięki temu jestem z pięknej zielonej strefie szczęścia. Model mógłby dodać od siebie trochę abstrakcji (może ISP), mógłbym sprawdzić czy wszędzie gdzie jest on wykorzystywany jest to potrzebne, może rozbić część klas na trochę mniejsze. Natomiast patrząc na Service widać że zbliża się on do powoli do zbyt wielkiej abstrakcji, lub braku wykorzystania części jego funkcjonalności. Warto sprawdzić czy wszystkie jego metody są potrzebne, może da się część z nich tak zmienić, aby wykorzystywały jakiś wspólny kod.

Z dodatkowych w raporcie ND można zobaczyć ile jest w sumie klas, metod w klasach, właściwości w klasach, linii kodu w projekcie, klasach, metodach, wszystko to co wymieniłem można także badać pod względem ilości poleceń w IL.

Po co utrzymywać kod z którego nikt nie korzysta? W moim przypadku zostało kilka metod, które zostały domyślnie stworzone przez wizarda podczas tworzenia projektu na Windows Phone, np. cała seria ApplicationLaunching, ApplicationActivated, ApplicationDeactivated czy ApplicationClosing. z której nigdzie nie korzystam. Takie proste rzeczy, a czynią kod z którym się pracuje przyjemniejszym w utrzymaniu.

ND wspiera MSVC 2008, 2010 oraz 2012. Umożliwia badanie kodu prosto z menu kontekstowego w edytorze:
Raporty można zapisywać, a następnie porównywać czy zmiany wprowadzane idą ku lepszemu. Dzięki integracji z VS można także prosto edytować i uruchamiać zapytania CQL – prosto ze środowiska pracy.
Na krótkich filmikach widziałem także, że ND potrafi porównywać ze sobą poszczególne wersje aplikacji. Niestety nie wiem jak do tego dojść, podejrzewam, że trzeba pilnować numerów wersji w assembly.
Odpalany z linii poleceń powinien być prosty do integracji z system CI (w domu takiego czegoś nie posiadam) i generować odpowiednio konfigurowalne raporty – menadżerowie się ucieszą. Możliwość zdefiniowania własnych reguł przy pomocy CQL umożliwi generowanie ostrzeżeń podczas budowania kodu, który np. nie posiada testów.

Pisałem wcześniej o tym, że warto rozmawiać. Zadawać pytania i czytać kod nie tylko swój. Poprosić znajomego o sprawdzenie tego co stworzyliśmy. ND wydaje się być czymś co może ten proces częściowo zastąpić. Nie warto jednak od razu usuwać ludzi z listy kontaktów. Jasna sprawa, że dla mnie czy innego Kowalskiego (pozdrawiam Tomka jeśli to czyta) jest on raczej drogim narzędziem 299 euro, szczególnie gdy w domu piszemy kod do szuflady. Ale gdy zarabia się na oprogramowaniu to świetne rozwiązanie, senior czy inny architekt może na początku projektu ustalić pewne zasady, który kod musi spełniać i dopiero gdy czekin w repozytorium spełni je wszystkie, zostanie przesłany do osobistego review. Pozwali to zaoszczędzić wszystkim dużo czasu (czas to pieniądz) oraz pominąć głupie i często powtarzające się błędy, np. brak słowa sealed przy klasie nie przeznaczonej do dziedziczenia.

TL;DR; (podsumowanie)
NDepend to dobre narzędzie do statycznego sprawdzenia kodu. Szczególnie przydatne w komercyjnych projektach, pozwoli automatycznie pilnować porządku w kodzie, zapewni przestrzeganie przyjętych zasad w projekcie. Zadowoli menadżerów wymagających nudnej papierologii. Nawet jeśli piszesz do szuflady, to na koniec lub po zakończeniu projektu, wykorzystaj wersję 14 dniową i sprawdź swój kod. Może się okazać, że da się lepiej.
Jeśli miałbym narzekać, to mogliby dać wersję darmową do domowego użytku.

Jest także NDepened dla Javy oraz dla C++. Niestety nie wiem jak dobre/złe są.

Linki:
NDepend strona domowa: http://www.ndepend.com/Default.aspx
Cennik: http://www.ndepend.com/Purchase.aspx
Lista ficzerów, część z nich posiada filmiki. Zabawne jest to, że korzystają z syntezatora mowy: http://www.ndepend.com/Features.aspx

Jeśli macie pytania to proszę o kontakt, jeśli chcecie żebym opisał jakąś część funkcjonalności ND to piszcie. Jeśli chcecie abym sprawdził jakiś kod to piszcie.

Metody rozszerzające – testowanie i porządek w api.

Jak każdemu porządnemu developerowi zdarza mi się czasem napisać testy. Jak każdemu porządnemu developerowi, czasem zdarza mi się wykorzystać mechanizm metod rozszerzających (jeśli nie wiesz o czym mówię sprawdź na msdn). Jak każdy prawdziwy developer, chciałem przetestować logikę, która była wykorzystywana w jednej z takich metod. W zasadzie to nie w samej metodzie, chciałem sprawdzić czy zostanie wywołana z wartościami, które są dla mnie ważne.
Zacznę od metod rozszerzających, a testowanie przyjdzie samo.

Tak wygląda interfejs udostępniony przez samą (bez metod rozszerzających) klasę List:

Aby przewinąć listę na sam dół wystarczą trzy wciśnięcia Page Down. Jak widać jedyny import, z którego korzystam to System.Collections.Generic, wymagany aby móc skorzystać z List

To się stanie gdy dodam System.Linq: (obrazek się troszkę nie mieści)

Pojawi się nowa funkcjonalność, zdefiniowana w System.Linq, która pozwala robić cuda z listą i innymi IEnumerable (jak również innymi obiektami). Po imporcie aby przewinąć listę metod, trzeba osiem razy walnąć w Page Down.

Co zrobić, aby lista nowych funkcjonalności nie zaśmiecała dostępnego api? Można to wszystko ukryć przy pomocy jednej klasy, która przekierowuje żądania dalej, do właściwej części, gdzie zdefiniowane będą rozszerzenia
Normalnie implementacja rozszerzeń może wyglądać jakoś tak:

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: Consolas, “Courier New”, Courier, Monospace;
background-color: #ffffff;
/*white-space: pre;*/
}

.csharpcode pre { margin: 0em; }

.csharpcode .rem { color: #008000; }

.csharpcode .kwrd { color: #0000ff; }

.csharpcode .str { color: #a31515; }

.csharpcode .op { color: #0000c0; }

.csharpcode .preproc { color: #cc6633; }

.csharpcode .asp { background-color: #ffff00; }

.csharpcode .html { color: #800000; }

.csharpcode .attr { color: #ff0000; }

.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}

.csharpcode .lnum { color: #606060; }

   1:  namespace ConsoleApplication
   2:  {
   3:   
   4:      class Program
   5:      {
   6:          static void Main(string[] args)
   7:          {
   8:              MyClass mc = new MyClass();
   9:          }
  10:      }
  11:   
  12:      public static class MyClassExtension
  13:      {
  14:          public static void Foo0(this MyClass target) { }
  15:          public static void Foo1(this MyClass target) { }
  16:          public static void Foo2(this MyClass target) { }
  17:          public static void Foo3(this MyClass target) { }
  18:          public static void Foo4(this MyClass target) { }
  19:          public static void Foo5(this MyClass target) { }
  20:          public static void Foo6(this MyClass target) { }
  21:          public static void Foo7(this MyClass target) { }
  22:          public static void Foo8(this MyClass target) { }
  23:          public static void Foo9(this MyClass target) { }
  24:      }
  25:   
  26:      public class MyClass
  27:      {
  28:      }
  29:  }

Po takiej implementacji otrzymujemy cztery metody podstawowe, oraz dodatkowy dziesięć nowych z rozszerzenia w MyClassExtension.

Można trochę to wszystko uprościć dodając odpowiednią warstwę abstrakcji (abstrakcja was uwolni, zapamiętajcie to). Opis poniżej:

   1:  namespace ConsoleApplication
   2:  {
   3:      using System;
   4:   
   5:      class Program
   6:      {
   7:          static void Main(string[] args)
   8:          {
   9:              MyClass mc = new MyClass();
  10:              mc.ExtensionService().Foo0();
  11:          }
  12:      }
  13:   
  14:      public static class MyClassExtension
  15:      {
  16:          public static Func<MyClass, IMyClassExtensionService> ExtensionFactory { get; set; }
  17:   
  18:          static MyClassExtension()
  19:          {
  20:              ExtensionFactory = target=> new MyClassExtensionReleaseVersion(target);
  21:          }
  22:   
  23:          public static IMyClassExtensionService ExtensionService(this MyClass target)
  24:          {
  25:              return ExtensionFactory(target);
  26:          }
  27:      }
  28:   
  29:      public interface IMyClassExtensionService
  30:      {
  31:          void Foo0();
  32:          void Foo1();
  33:          void Foo2();
  34:          void Foo3();
  35:          void Foo4();
  36:          void Foo5();
  37:          void Foo6();
  38:          void Foo7();
  39:          void Foo8();
  40:          void Foo9();
  41:      }
  42:   
  43:      public class MyClassExtensionReleaseVersion : IMyClassExtensionService
  44:      {
  45:          private MyClass myclass;
  46:   
  47:          public MyClassExtensionReleaseVersion(MyClass target)
  48:          {
  49:              this.myclass = target;
  50:          }
  51:   
  52:          public void Foo0() { }
  53:          public void Foo1() { }
  54:          public void Foo2() { }
  55:          public void Foo3() { }
  56:          public void Foo4() { }
  57:          public void Foo5() { }
  58:          public void Foo6() { }
  59:          public void Foo7() { }
  60:          public void Foo8() { }
  61:          public void Foo9() { }
  62:      }
  63:   
  64:      public class MyClass
  65:      {
  66:      }
  67:  }

Tak się to prezentuje teraz:

Zacznę od linijki 29 gdzie zdefiniowany został interfejs ze wszystkimi metodami, które mają działać jako rozszerzenie dla klasy MyClass. Linia 43 to implementacja tego interfejsu, warto zauważyć że klasa ta nie jest statyczna, oraz nie posiada statycznych pól, a metody nie przyjmują parametrów. W konstruktorze otrzymuje ona obiekt, na rzecz którego ma działać. Najlepsze zostawiam na koniec, linijka 14, klasa udostępniająca rozszerzenie dla MyClass. W ExtensionService (23) wykorzystywana jest właściwość ExtensionFactory, który jest zwyczajną metodą wytwórczą, zwracającą nowy obiekt MyClassExtensionReleaseVersion, do którego przesyła obiekt, na rzecz którego ma zostać wywołana metoda rozszerzająca. Mam nadzieję, że kod tłumaczy to prościej niż ja. Linia 10 to nowy sposób na wywołanie metod rozszerzających. 
Właściwość ExtensionFactory została zdefiniowana jako publiczna, co umożliwia jego zmianę w razie potrzeby testowania. Tak samo obiekt rozszerzający jest deklarowany poprzez interfejs, w związku z czym w testach można wykorzystać np. MyClassExtensionMockVersion i sprawdzać czy klasa zachowuje się tak jak tego oczekujemy.

Ja skorzystałem z tego rozwiązania podczas testowania nawigacji w projekcie, który korzysta z PRISMa, tam do zmiany widoków wykorzystywany jest interfejs IRegionManager i kilka metod rozszerzających ten interfejs. Chciałem sprawdzić, czy po wykonaniu operacji X jedna z moich klas zarząda zmiany widoku. Jedynym sposobem, na sprawdzenie parametrów przesłanych w query było stworzenie i podstawienie własnej implementacji metod rozszerzających. Na początku testów mam taki zapis:

   1:  Capture.Common.Prism.PrismExtensions.ServiceFactory = p => new NavigationServiceMock(p);

Tak wygląda mój lipny serwis, który działa zamiast tego z Prism:

   1:  public class NavigationServiceMock : INavigation
   2:  {
   3:      private IRegionManager p;
   4:   
   5:      public NavigationServiceMock(IRegionManager p)
   6:      {
   7:          this.p = p;
   8:      }
   9:   
  10:      public IRegionManager AddToRegion(string regionName, object view)
  11:      {
  12:          throw new NotImplementedException();
  13:      }
  14:   
  15:      public IRegionManager RegisterViewWithRegion(string regionName, Func<object> getContentDelegate)
  16:      {
  17:          throw new NotImplementedException();
  18:      }
  19:   
  20:      public IRegionManager RegisterViewWithRegion(string regionName, Type viewType)
  21:      {
  22:          throw new NotImplementedException();
  23:      }
  24:   
  25:      public void RequestNavigate(string regionName, string source)
  26:      {
  27:          throw new NotImplementedException();
  28:      }
  29:   
  30:      public void RequestNavigate(string regionName, Uri source)
  31:      {
  32:          LoadTradeViewModelTest.NavigationServiceMock_RequestedNavigatedTargetRegionName = regionName;
  33:          LoadTradeViewModelTest.NavigationServiceMock_ReqestedNavigatedTargetUri = source.OriginalString;
  34:      }
  35:   
  36:      public void RequestNavigate(string regionName, string source, Action<NavigationResult> navigationCallback)
  37:      {
  38:          throw new NotImplementedException();
  39:      }
  40:   
  41:      public void RequestNavigate(string regionName, Uri source, Action<NavigationResult> navigationCallback)
  42:      {
  43:          throw new NotImplementedException();
  44:      }
  45:  }

Najważniejsze w tym wszystkim to RequestNavigate (30) gdzie zapisuje żądany region i uri. Później w teście porównuje czy są takie jak oczekiwałem. Wszystkie NotImplementedException są nie używane w testach, dlatego też mogły pozostać w takiej domyślnie oferowanej przez VS formie.
Normalna implementacja przekazuje wszystkie parametry do oryginalnej implementacji.

Pojawia się pewien problem, przy próbie stworzenie podobnego rozwiązania dla klas generycznych. Ten problem to kompilator, który cały czas twierdzi że metody rozszerzające muszą być w klasach statycznych, nie generycznych. Same metody mogą być generyczne. Aby to obejść na szybko wymyśliłem coś takiego

   1:  namespace ConsoleApplication
   2:  {
   3:      using System;
   4:      using System.Collections.Generic;
   5:   
   6:      class Program
   7:      {
   8:          static void Main(string[] args)
   9:          {
  10:              List<int> l = new List<int>();
  11:              l.GetExtension().Foo1();
  12:          }
  13:      }
  14:   
  15:      public static class ExtensionsFactory<T>
  16:      {
  17:          public static Func<List<T>, IListExtensionService> Factory { get; set; }
  18:      }
  19:   
  20:      public static class ListExtension
  21:      {
  22:          public static IListExtensionService GetExtension<T>(this List<T> target)
  23:          {
  24:              if (ExtensionsFactory<T>.Factory == null)
  25:              {
  26:                  ExtensionsFactory<T>.Factory = o => new ListExtensionSerivce<T>(o);
  27:              }
  28:   
  29:              return ExtensionsFactory<T>.Factory(target);
  30:          }
  31:      }
  32:   
  33:      public interface IListExtensionService
  34:      {
  35:          void Foo1();
  36:      }
  37:   
  38:      public class ListExtensionSerivce<T> : IListExtensionService
  39:      {
  40:          private List<T> list;
  41:   
  42:          public ListExtensionSerivce(List<T> target)
  43:          {
  44:              this.list = target;
  45:          }
  46:   
  47:          public void Foo1()
  48:          {
  49:              Console.WriteLine("foo and the list");
  50:          }
  51:      }
  52:  }

Cały trik w tym przypadku, to przeniesienie odpowiedzialności na wytwarzanie obiektów z ListExtension do ExtensionsFactory. A podczas wywołania metody, należny się upewnić że fabryka już istnieje.

Myślę że wystarczy mojego wymądrzania się na dziś. Podoba się? Nie podoba się? Macie inne sposoby? Pytania? Uwagi? Cokolwiek?

ps
Mam nadzieję, że więcej wyjaśniłem niż zakręciłem.
ps2
Jak już wszystko napiszę, to czytam czy jest ok, a potem i tak mam lekkiego stresa, że coś głupiego napisałem.

xUnit vs Event – jak go przetestować?

Rozpoczynam kolejny projekt, który ma przynieść mi chwałę, sławę i pieniądze. Piszę go przy wykorzystaniu TDD (jak zwykle angielska wiki ma więcej do powiedzenia) Wszystko szło ładnie do momentu gdy nie natrafiłem na test w którym chciałem sprawdzić czy klasa którą testuję wywoła event. No bo jak sprawdzić teraz coś co wykona się później?
Na szczęście wujek google zna programistów, którzy znają odpowiedź na takie i inne pytania, w związku z czym szybko znalazłem odpowiedź na swój problem.
Zamieszczę ja tutaj ze stosownymi komentarzami od siebie, plus kilka własnych uwag, które może ktoś uzna za wartościowe. Jest taki oto sobie kod:

  1. // arrange
  2. var cut = new AsyncImageServiceDownloader();
  3. var isp = new ImagesServiceProvider();
  4. const string ServiceAddress = “http://www.digart.pl/”;
  5. const string PageSuffix = “/przegladaj/nowe.html?p=”;
  6. isp.ServiceAddress = ServiceAddress;
  7. isp.PageAddressSuffix = PageSuffix;
  8. cut.AddProvider(isp);
  9. int downloaded = 0;
  10. int canceled = 0;
  11. int suspended = 0;
  12. int resumed = 0;
  13. const uint Requested = 30;
  14. ManualResetEvent synvEvent = new ManualResetEvent(false);
  15.  
  16. // act
  17. cut.AddProvider(isp);
  18. cut.PageDownloaded += (s, e) => { downloaded++; };
  19. cut.PageDownloadCanceled += (s, e) => { canceled++; };
  20. cut.PageDownloadSuspended += (s, e) => { suspended++; };
  21. cut.PageDownloadResumed += (s, e) => { resumed++; };
  22.  
  23. cut.DownloadPageAsync(0, 0, Requested);
  24. for (uint i = 0; i < Requested; ++i)
  25. {
  26.     cut.SuspendDownload(0, i);
  27. }
  28.  
  29. synvEvent.WaitOne(1000);
  30. for (uint i = 0; i < suspended; ++i)
  31. {
  32.     cut.ResumeDownload(0, i);
  33. }
  34.             
  35. synvEvent.WaitOne(15 * 1000);
  36. // assert
  37. Assert.That(downloaded, Is.EqualTo(Requested));
  38.  

Widzimy podział na trzy AAA (nie znalazłem polskiego).
Na początku przygotowuje środowisko, które będę chciał sprawdzić; cut (class under test) to właściwy obiekt, który chce sprawdzić, isp służy do poprawnego zainicjalizowania obiektu cut. Ważny obiekt ManualResetEvent jest deklarowany i tworzony w linii 14 posłuży mi do synchronizacji i czekania na eventy. Linie od 18 do 21 to podpięcie się pod eventy, którymi klasa może rzucić. Choć w tym przypadku interesują mnie tylko dwa: suspended oraz downloaded. Przy użyciu wyrażeń lambda podpinam się pod eventy, a jedyną rzecz jaką robię to zliczam ich wywołania. W tym teście nie interesuje mnie zawartość argumentów.
Skoro wszystko mam już gotowe do działania to ogień! Linia 23 wywołuje asynchroniczne zaciąganie 30 elementów (requested=30). Chwilę potem do akcja wkracza pętla z niecierpiącym zwłoki poleceniem SuspendDownload następnie daję chwilę testowanemu obiektowi na wykonanie kodu odpowiedzialnego za wywołanie odpowiednich eventów. Wszystko dzięki wywołaniu ManualResetEvent.WaitOne(1000) – specjalnie podany jest timeout, ponieważ nigdzie nie będzie zmieniony jego stan. Czas się kończy, testy wykonują się dalej. Kolejne rozkazy; ResumeDownload tyle razy ile razy zdążyłem go zawiesić. I kolejny odpoczynek z timeout 15 sekund, co pozwala spokojnie zakończyć się wszystkim operacjom ściągnięcia.
Na sam koniec sprawdzam czy rzeczywiście udało się ściągnąć tyle ile początkowo żądałem. Voila! To tyle, proste prawda?

Moje dwie uwagi:
– Pamiętaj że w obsłudze eventa możesz samodzielnie zmienić stan ManualEventReset, jeśli z tego skorzystasz nie zapomnij jednak dodać timeout do WaitOne w przeciwnym wypadku, w razie niepowodzenia testów ManualResetEvent zawiesi działanie testów.
– Zastanów się czego dokładnie oczekujesz od twojego cut. Jeżeli cut wywoła PageDownloaded trzy razy to od razu będziesz szczęśliwy? A może chcesz sprawdzić czy cut wywoła PageDownloaded tylko trzy razy, a żadnych innych eventów ani razu.

Jakieś uwagi? Inne sposoby? Literówka?
Wiesz jak mnie znaleźć.
JS