Otwarte karty

modyfikatory

Public, Protected, Private

Do czego służą modyfikatory dostępu? Do ograniczania dostępu do metod, pól i właściwości klas (C#). Ograniczenie to sprawdzane jest w trakcie kompilacji i gdy sięgasz za daleko taki komunikat pojawia się na ekranie:
Error    CS0122    ‚Test.fooPrivate()’ is inaccessible due to its protection level.
Ale wystarczy odrobina refleksji tu i tam i można spokojnie olać modyfikatory:

Continue reading

Piszę testy na 100%

testyBajki

Znowu wpis o mitach. Bo jak często widać, że w jakimś projekcie pokrycie testami wynosi 100%? Częściej się o tym mówi, niż widzi.

Menagiery i biznes się chyba pogodził z tym, że ciężko będzie to osiągnąć i miękną, mówią dobra to robimy 80% albo 60% jeśli projekt mało ważny, a metryki jakieś muszą mieć – a szkoda, bo to nie jest aż taka ciężka sprawa.

Dygresja

Teraz taka myśl przyszła mi do głowy, czy ktoś patrzy w ogóle na te procenty, które kryją kod? Bo jak się uprzeć, to wysoki poziom można zrobić, bez napisania sensownych testów. Np. przetestować wszystkie właściwości: zarówno get i set. I już kilka procent zaliczone.Ale nie o tym chciałem pisać.

Co zrobić, gdy za jakiś kawałek kodu ciężko się zabrać, albo jest tam coś systemowego? Ja wtedy zawsze wracam do mojej mantry, którą jest spychologia. Alternatywą jest microsoft fakes – ale to dużo roboty.

Treść właściwa wpisu nastąpi teraz

Continue reading

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.