→ Slide 1

Testy jednostkowe

→ Slide 2

Test jednostkowy (ang. unit test) to technika testowania tworzonego oprogramowania poprzez wykonywanie testów weryfikujących poprawność działania pojedynczych elementów (jednostek) programu - np. metod, obiektów.

Testowany fragment programu poddawany jest testowi, który wykonuje go i porównuje wynik z oczekiwanymi wynikami.

Źródło: pl.wikipedia.org

  • Test jednostkowy
    to fragment kodu, który sprawdza inny fragment kodu
  • Unit
    najmniejsza testowalna część aplikacji
    • funkcja
    • klasa
    • metoda
↓ Slide 3

Aksjomaty testowania

  • Testy są: czasochłonne, kosztowne, ryzykowne, nużące
  • Żadnego realnego oprogramowanie nie da się przetestować całkowicie.
  • Test nie udowadnia braku błędów, a udowadnia jedynie to, ze ich nie znaleźliśmy
  • Nie wszystkie znalezione błędy zostaną naprawione (za duży koszt, brak czasu, ryzyko naprawy)
  • Paradoks pestycydów - błędy w oprogramowaniu uodparniają się na testy
  • Testowanie wymaga wyobraźni i złośliwości
  • Czy warto tracić czas na pisanie testów ?
↓ Slide 4

Czym są testy jednostkowe?

↓ Slide 5

Przykład: klasa testowana

public class Kalkulator
{
	public int Suma(int [] x)
	{
		if (x == null) throw new ArgumentNullException();
 
		int s = 0;
		for(int i=1; i < x.Length ; i++) s+=x[i];
 
		return s;
	}
}
↓ Slide 6

Przykład: klasa testowa

public class KalkulatorTest
{
	public void SumaTest()
	{
		int[] x = { 1, 2, 3, 4 };
		Kalkulator c = new Kalkulator();
		int oczekiwanyWynik = 10;
		int aktualnyWynik = c.Suma(x);
 
		if (aktualnyWynik != oczekiwanyWynik) 
			throw new Exception($"Test oblany: spodziewana wartosc {oczekiwanyWynik}, aktualna wartosc {aktualnyWynik}");
 
	}
}
public class KalkulatorTest
{
	public void SumaTestException()
	{
		Kalkulator c = new Kalkulator();
		try {
			c.Suma(null);
		} 
                catch (ArgumentNullException) {
			return;
		}
		throw new Exception("Test oblany");
	}
}
↓ Slide 7

Przykład: środowisko uruchomieniowe

class Program
{
	static void Main(string[] args)
	{
		KalkulatorTest test = new KalkulatorTest();
 
		test.SumaTest();
		test.SumaTestException();
 
		Console.WriteLine("Wszystkie testy zaliczone.");
	}
}

Porażka

Sukces

↓ Slide 8

Przykład: VS2012 MS Test

[TestClass]
public class UnitTestKalkulator
{
    [TestMethod]
    public void TestSuma10()
    {
      	// arrange 
      	int[] x = { 1, 2, 3, 4 };
      	Kalkulator c = new Kalkulator();
      	int oczekiwanyWynik = 10;
 
      	//act 
      	int aktualnyWynik = c.Suma(x);
 
      	//assert 
      	Assert.AreEqual(oczekiwanyWynik, aktualnyWynik);
    }
}
→ Slide 9

Technika zwinna (agile), wywodząca się z programowania ekstremalnego.

Źródło: Test-driven development Wstęp do Test Driven Development (MSDN)

  • Test → code → refactor
  • najpierw test sprawdzający dodawaną funkcjonalność (test nieudany)
  • implementacja funkcjonalności (test udany)
  • refaktoryzacja napisanego kodu, żeby spełniał on oczekiwane standardy.
↓ Slide 10

Korzyści TDD i testowania

  • Wczesne wykrywanie błędów → mniejszy koszt
  • Odporność oprogramowania na błędy regresyjne, czyli błędy powstałe w wyniku poprawek kodu
  • Ułatwienie refaktoringu i zmian w kodzie pokrytym testami
  • Dokumentowanie i wyjaśnianie kodu. Test wyjaśnia jaką funkcjonalność realizuje jednostka kodu i jak należy jej używać.
  • Lepiej zaprojektowane interfejsy i API. Testy zmuszają do lepszego przemyślenia rozwiązań i dokładnego określenia jakie zadania dana metoda ma wykonywać.
  • Testy mogą pełnić rolę specyfikacji projektowej (TDD) podobnej do UML.
  • Uproszczona integracja, łatwiejsze łączenie różnych fragmentów kodu poddanych wcześniej testom. Testowanie bottom-up. Jednak zbiór UT nie zastąpi testów systemowych (które najczęściej są wykonywane ręcznie)
  • Automatyzacja i powtarzalność (contignous integration): testy można uruchamiać regularnie o określonych porach lub na pewnych etapach produkcji. Oszczędność czasu w stosunku do ręcznego testowania.
  • Możliwość przetestowania funkcjonalności bez uruchamiania całego oprogramowania
→ Slide 11

Wzorzec AAA: Arrange → Act → Assert

  1. przygotowanie środowiska testowego (Fixtures)
  2. ustawienie stanu wejściowego
  3. uruchomienie testowanych jednostek kodu (Act)
  4. porównanie uzyskanych wyników (Assert)
  5. zwolnienie zasobów (Teardown)
↓ Slide 12

xUnit

  • Wzorzec xUnit - zbiór środowisk testowych (frameworks) wywodzących swoją architekturę od SUnit (Kent Beck, 1998, Smaltalk).
  • Assercje - funkcje weryfikujące stan i zachowanie jednostek kodu. Zazwyczaj w postaci wyrażenia logicznego, które zwraca albo prawdę albo fałsz. Niepowodzenie przerywa dany test (rzucany wyjątek).
  • Test runner - program uruchamiający testy i raportujący wyniki
  • Test fixtures (test context) - zbiór warunków wymaganych do przeprowadzenia testu
  • Test suites - zbiór testów uruchamianych w jednym środowisku (fixture). Kolejność uruchomienia nie ma znaczenia.
→ Slide 13

Narzędzia i biblioteki wspierające tworzenie testów, ich organizację, automatyzację wykonywania, raportowanie

  • Biblioteka asercji i innych metod przydatnych przy testowaniu
  • Automatyzacja w procesie wytwórczym
  • Generatory testów, testy parametryczne
  • Mechanizmy izolacji: fake, mock, itd
  • Metryki: pokrycie kodu, ścieżek, …
↓ Slide 14

Środowiska testowe

Wikipedia: List of unit testing frameworks

↓ Slide 15

.NET frameworks

↓ Slide 16

Przykład: MS Test

using Microsoft.VisualStudio.TestTools.UnitTesting;
 
[TestClass]
public class UnitTestKalkulator
{
	private Kalkulator _calc;
 
	[TestInitialize]
	public void Init()
	{
		_calc = new Kalkulator();    
	}
 
	[TestMethod]
	public void TestSuma10()
	{
		int[] x = { 1, 2, 3, 4 };
		int oczekiwanyWynik = 10;
		int aktualnyWynik = _calc.Suma(x);
		Assert.AreEqual(oczekiwanyWynik, aktualnyWynik);
	}
 
	[TestMethod]
	[ExpectedException(typeof(ArgumentNullException))]
	public void TestSumaException()
	{
		_calc.Suma(null);
	}
}
↓ Slide 17

Przykład: NUnit/MBUnit

using NUnit.Framework;
 
[TestFixture]
public class UnitTestKalkulator
{
	private Kalkulator _calc;
 
	[SetUp]
	public void Create()
	{
		_calc = new Kalkulator();
	}
 
	[Test]
	public void TestSuma10()
	{
		int[] x = { 1, 2, 3, 4 };
		int oczekiwanyWynik = 10;
		int aktualnyWynik = _calc.Suma(x);
		Assert.AreEqual(oczekiwanyWynik, aktualnyWynik);
	}
 
	[Test]
	[ExpectedException(typeof(ArgumentNullException))]
	public void TestSumaException()
	{
		_calc.Suma(null);
	}
}
↓ Slide 18

Przykład: xUnit.net

using Xunit;
 
public class KalkulatorTest
{
   private Kalkulator _calc;
 
   public KalkulatorTest()
   {
      _calc = new Kalkulator();
   }
 
   [Fact]
   public void TestSuma()
   { 
      int[] x = { 1, 2, 3, 4 };
      int oczekiwanyWynik = 10;
      int aktualnyWynik = _calc.Suma(x);
      Assert.Equal(oczekiwanyWynik, aktualnyWynik);
   }  
 
   [Fact]
   public void TestSumaException()
   {
      Assert.Throws<ArgumentNullException>(() => _calc.Suma(null));
   }
}
→ Slide 19
  • Test wyłącznie pojedynczej jednostki, nie można pisać testów badających kilka funkcji naraz
  • Testy powinny być niezależne od siebie
  • Izolacja: testy niezależne od zewnętrznych zasobów, są one zastępowane makietami symulującymi ich działanie (mock, fake, stub, …)
  • Przejrzystość - testy wyjaśniają i dokumentują kod
  • Szybkość - testy powinny wykonywać się szybko, to nie są testy wydajności
  • Konwencje nazewnicze - nazwa testu oddaje intencje
  • Pisz kod tak aby był łatwy do testowania
↓ Slide 20

Scenariusze

  • Analiza ścieżek - badanie przebiegu możliwych ścieżek od punktu początkowego do końcowego, wszystkie możliwe kombinacje rozgałęzień
    • Boundary test - działania w pętli pomijane lub pętla uruchamiana raz (dla wszystkich ścieżek)
    • Interior test - działania we wnętrzu pętli uważa się za przetestowane, jeśli zostały wykonane wszystkie ścieżki, które są możliwe przy dwukrotnym powtórzeniu pętli
  • Klasy równoważności - zbiór danych o podobnym sposobie przetwarzania. Testujemy wybrane elementy ze zbioru. na wejściu kilka elementów przetwarzanych w ten sam sposób
    • klasy poprawności/niepoprawności - przypadki dla których przewidujemy poprawne/niepoprawne działanie
      Np. {2, 4, 70) dla liczb dodatnich całkowitych
  • Wartości brzegowe - wewnątrz, pomiędzy lub na granicy klas równoważności
    Np. {-1, 0, 2, 4, 70) dla liczb dodatnich całkowitych
↓ Slide 21

Zakres dostępu

  • metody publiczne, testowane bez problemu
  • metody chronione - nie można w bezpośredni sposób wywołać metody chronionej ze sterownika testu. Rozwiązaniem jest stworzenie klasy opakowującej dziedziczącej po badanej klasie i wywołanie chronionych metod w dziedziczącej klasie bazowej
  • metody prywatne - za pomocą mechanizmu refleksji można ominąć ograniczenia i wywołać prywatną metodę.
  • Czy testy jednostkowe powinny dotyczyć wyłącznie metod publicznych?
↓ Slide 22

Testy prywatnych metod

Klasa PrivateObject pozwala opakować klasę zawierającą prywatne elementy do testowania. Dostęp do prywatnych elementów za pomocą metody Invoke()

using Microsoft.VisualStudio.TestTools.UnitTesting;
 
[TestMethod]
public void TestPrywatnejMetody()
{
   var obj = new PrivateObject(typeof(KlasaZPrywatnąMetodą));
   var wynik = obj.Invoke("PrywatnaMetoda");
   Assert.AreEqual(wynik, spodziewanyWynik);
}
→ Slide 23
  • Widoczność:
    • Testy funkcjonalne (czarna skrzynka), zazwyczaj wykonywanie przez osoby nie wytwarzające kodu.
      Testy black-box: acceptance, fuzz, smoke, performance (load, stress)
    • Testy strukturalne (biała/przezroczysta skrzynka), test ma wgląd w testowany kod.
      Testy white-box: unit, regression, integration, mutation
  • Skala:
    • jednostkowe - pojedyncze funkcjonalności
    • systemowe - testy aplikacji jako całość: bezpieczeństwo, wymagania niefunkcjonalne, wydajność
    • integracyjne - współpraca komponentów, testuje współdziałanie klas i modułu
→ Slide 24

Szablony projektów: Unit Test Project, Visual C#, C++

  • Namespace: Microsoft.VisualStudio.TestTools.UnitTesting
  • Wiele rozszerzeń wspierających: Resharper, TestDriven.net
↓ Slide 25

Generowanie metod testowych

  • Narzędzia do generowania projeku testowego i metod testowych dostępne w VS 2010 ale nie w 2012/13, powróciły w 2015
  • Wtyczki np. Unit Test Generator
↓ Slide 26
  • Uruchomienie wybranych testów
  • Debbugowanie wybranych testów
  • Pokrycie kodu
  • Okna: Test Explorer, Code Coverage Results

↓ Slide 27

VS Test Explorer

↓ Slide 28

Typy asercji

    • AreEqual / AreNotEqual - porównanie warości
    • AreSame / AreNotSame - porównanie referencji
    • Fail - test jednostkowy jest niezaliczony
    • Inconclusive - wykonanie testu jest nierozstrzygnięte
    • IsTrue / IsFalse - porównanie wartości true/false
    • IsInstanceOfType / IsNotInstanceOfType - czy dostarczona wartość nie jest podanego typu
    • IsNull / IsNotNull - czy dostarczona wartość nie jest NULL
  • CollectionAssert zestaw metod testowych dla porównywania kolekcji (np. CollectionAssert.AreEqual)
  • StringAssert zestaw asercji do testowania napisów
    • Contains
    • Matches / DoesNotMatch dopasowanie wyrażenia regularnego
    • StartsWith / EndsWith
↓ Slide 29

Artybuty

  • TestClass określa klasę zawierającą metody testowe
  • Przygotowanie środowiska testowego (fixture/teardown) dla assembly, zestawu testów w klasie i pojedyńczej metody testowej
    • AssemblyInitialize / AssemblyCleanup
    • ClassInitialize / ClassCleanup
    • TestInitialize / TestCleanup
  • ExpectedException spodziewany wyjątek rzucany z testu
  • Description opis testu
  • Ignore test nie będzie uruchomiony
  • TestCategory pozwala grupować testy
  • WorkItem powiązanie testu z zadaniem (work item)

UnitTesting Namespace

↓ Slide 30

Kontekst testu

  • Klasa TestContext przechowuje informacje, które można dostarczyć do testów
    • Połaczenia do danych (data row, Data Driven Unit Test)
    • Informacje o wykonywanym teście (np. ścieżki dostępu)
    • Infromacje do testowania ASP.NET, np.: URL serwera Web, dostęp do obiektu Page
  • Dostęp do własności TextContext (tworzona automatycznie lub należy dodać ręcznie)
  • Metody oznaczone atrybutem ClassInitialize i AssemblyInitialize muszą dostarczyć obiekt TextContext w argumencie
↓ Slide 31

Setup fixture

→ Slide 32

  • xUnit dla języków .Net, początkowo port z JUnit
  • najpopularniejsze środowisko dla tej platformy
  • wersja 3.0 przepisana od nowa, obecnie wersja 3.2
  • biblioteki: framework, mock
  • aplikacja uruchamiająca testy: GUI, console
  • Assercja: classic model vs. Constraint-Based Assert Model (NUnit 2.4)
↓ Slide 33

GUI

↓ Slide 34

CLI

→ Slide 35

„xUnit.net is a free, open source, community-focused unit testing tool for the .NET Framework. Written by the original inventor of NUnit v2, xUnit.net is the latest technology for unit testing C#, F#, VB.NET and other .NET languages. xUnit.net works with ReSharper, CodeRush, TestDriven.NET and Xamarin.”

Zaprojektowany z myślą o maksymalnym uproszczeniu testowania

Pakiety NuGet

  • xUnit.net framework
  • xunit.runner.console uruchamianie testów w konsoli
  • xunit.runner.visualstudio integracja z Test Explorer w VS
  • Wtyczka do Resharpera: runner for xUnit.net

xUnit website
Porównanie: NUnit vs. MS Test vs. xUnit

→ Slide 36
  • Metody testowe mogą pobierać dane do testów ze źródeł dostępnych na maszynach uruchamiających testy
[DataSource(@"Provider=Microsoft.SqlServerCe.Client.4.0; Data Source=C:\Data\MathsData.sdf;", "Numbers")]
[TestMethod]
public void AddIntegers_FromDataSourceTest()
{
	var target = new Maths();
 
	// Access the data
	int x = Convert.ToInt32(TestContext.DataRow["FirstNumber"]);
	int y = Convert.ToInt32(TestContext.DataRow["SecondNumber"]); 
	int expected = Convert.ToInt32(TestContext.DataRow["Sum"]);
	int actual = target.IntegerMethod(x, y);
	Assert.AreEqual(expected, actual);
}

Źródło: How To: Create a Data-Driven Unit Test

→ Slide 37
[Theory]
[InlineData(3)]
[InlineData(5)]
[InlineData(6)]
public void MyFirstTheory(int value)
{
    Assert.True(IsOdd(value));
    }
 
    bool IsOdd(int value)
    {
        return value % 2 == 1;
    }
}
  • w NUnit bardzo szeroki zakres parametryzacji za pomocą odpowiednich atrybutów
[Theory]
[PropertyData("SplitCountData")]
public void SplitCount(string input, int expectedCount)
{
     var actualCount = input.Split(' ').Count();
     Assert.Equal(expectedCount, actualCount);
}
 
public static IEnumerable<object[]> SplitCountData
{
   get
   {
      // Or this could read from a file. :)
      return new[]
      {
         new object[] { "xUnit", 1 },
         new object[] { "is fun", 2 },
         new object[] { "to test with", 3 }
      };
   }
}

Zródło: Theory DDT, Tom DuPont

→ Slide 38
[Fact(Skip="Trait Extensibility is not working in 1654"), Category("Slow Test")]
public void LongTest()
{
    Thread.Sleep(500);
}
 
[Fact, Trait("Category", "Supa")]
public void LongTest2()
{
   Assert.True(true); 
}
[Fact(Timeout=50)]
public void TestThatRunsTooLong()
{
    System.Threading.Thread.Sleep(250);
}
→ Slide 39
  • AutoFixture is an open source library for .NET designed to minimize the 'Arrange' phase.
  • Tworzenie anonimowych zmiennych
[TestMethod]
public void IntroductoryTest()
{
   // Fixture setup
   Fixture fixture = new Fixture();
 
   int expectedNumber = fixture.Create<int>();
   MyClass sut = fixture.Create<MyClass>();
   // Exercise system
   int result = sut.Echo(expectedNumber);
   // Verify outcome
   Assert.AreEqual<int>(expectedNumber, result, "Echo");
   // Teardown
}
→ Slide 40
[Theory, AutoData]
public void IntroductoryTest(int expectedNumber, MyClass sut)
{
   int result = sut.Echo(expectedNumber);
   Assert.Equal(expectedNumber, result);
}
→ Slide 41
  • Izolacja: test jednostkowy nie może wykraczać poza zakres testowanej jednostki kodu
  • Testy wykraczające poza ten zakres
    • to testy integracji
    • są wolniejsze
    • mogą się nie powieść ze względu na błędy w tych zewnętrznych modułach
  • Separacja od zewnętrznych procesów wymusza tworzenie kodu bardziej modularnego, prostszego w testowaniu i ponownym wykorzystaniu (dependency inversion principle, warstwy abstrakcji pomiędzy komunikującymi się modułami)
  • Obiekty imitujące dostęp do zewnętrznych procesów: Fake, Mock, Stub, Spies, Exstract and Override, ….
  • Frameworks .NET: Moq, FakeItEasy, RhinoMocks, NSubstitute, …
↓ Slide 42

Mock objects

  • Atrapy obiektów (mock objects) - symulowane obiekty naśladujące w kontrolowany sposób zachowanie rzeczywistych obiektów.
    • symulowanie połączenia do bazy danych
    • symulowanie z zasobami, które mogą być niedostępne, np.: połączenia z serwisami sieciowymi
    • symulowanie obiektów, które jeszcze nie powstały
    • symulowanie w celu zwiększenia szybkości testów
↓ Slide 43

Mocks, fakes i inne

  • Wzorce Test Doubles, różnią się przede wszystkim sposobami wykorzystania
    • Dummy - zwraca pewną domyślna wartość
    • Stub - pozwala zwracać więcej wartości, bardziej złożona logika wewnętrzna
    • Spy
    • Fake - metody tego obiektu dostarczają niezbędne dane do działania testu
    • Mock - to obiekt Fake zawierający w sobie assercję testową lecz nie tylko
  • Inne: strict mock, dynamic mock, loose mock, substitute, mole.
  • „no need to learn what’s the theoretical difference between a mock, a stub, a fake, a dynamic mock, etc.” (Moq project)
  • „all fake objects are just that — fakes. Usage determines whether they're mocks or stubs.” (FakeItEasy)
↓ Slide 44

Dependency Injection

  • Wstrzykiwanie zależności - wzorzec projektowy i wzorzec architektury oprogramowania polegający na usuwaniu bezpośrednich zależności pomiędzy komponentami na rzecz architektury typu plug-in. Szczególna realizacja paradygmatu z odwróceniem sterowania sterowania (Inversion of Control, IoC).
  • Wstrzyknięcie przez : konstruktor, settery, interfejs
  • DI pozwala wstrzyknąć do obiektów testowanych zależności od obiektów-zaślepek (atrap, mock objects), w ten sposób TTD narzuca stosowanie dobrych praktyk
↓ Slide 45

Przykład DI

public class Client {
 
	//  Wewnętrzna referencja do usługi 
	private Service service;
 
	//  Wstrzyknięcie przez konstruktor
	Client(Service service) {
		this.service = service; 
	}
 
	// Metoda w której klient korzysta z usługi (chcemy wyizolować to działanie)
	public double Compute(double x) {
		return x * service.GetSomeValue();
	}
}
↓ Slide 46

Przykład: biblioteka Moq

// Tworzymy obiekt atrapę 
var mockClient = new Mock<Service>();
 
// Ustawiamy metodę aby zwracała symulowaną wartość 
mockClient.Setup(mock => mock.GetSomeValue()).Returns(1);
 
// Wstrzykujemy atrapę do obiektu który będzie testowany 
var client = new Clent(mockClient.Object);
 
// Test 
Assert.IsEqual(client.Compute(2), oczekiwanaWartosc);
→ Slide 47

Pokrycie kodu mierzy, ile procent kodu zostało sprawdzone przez testy jednostkowego. Przyjmuje się, że dobrze napisane testy powinny mieć pokrycie rzędu przynajmniej 70% .

↓ Slide 48

Metryki pokrycia

  • Pokrycie wyrażeń (ang. statement coverage) – metryka stanowi iloraz liczby linii kodu wywołanych przez testy jednostkowe oraz całkowitej liczby linii kodu. Jeśli jakaś linia nie została pokryta, istnieje zatem ryzyko wystąpienia w niej błędu.
  • Pokrycie rozgałęzień (ang. branch coverage) – pokrycie nie bada linii kodu, a rozgałęzienia zrealizowane za pomocą np. instrukcji if (czy test przetestował ścieżki true i false)
  • Pokrycie ścieżki (path coverage) - ścieżka to unikatowa sekwencja rozgałęźień w funkcji
  • inne: pokrycie funkcji/metod, klas (metod w klasach)/ patch coverage
↓ Slide 49

Code coverage w VS

  • VS2010: Test → Edit Test Settings → Local
    • Data and Diagnostic → Code Coverag
    • Configure - wybór źródła danych do analizy
  • VS2012: Test → Analyze Code Coverage → All Tests/ Selected Test
↓ Slide 50

Testowanie mutacyjne

Testowanie mutacyjne – technika automatycznego badania, na ile dokładnie testy jednostkowe sprawdzają kod programu. Polega na automatycznym wielokrotnym wprowadzaniu małych losowych błędów w programie i uruchamianiu za każdym razem testów jednostkowych, które powinny te błędy wykryć. Testowanie mutacyjne wymaga dużej mocy obliczeniowej i dlatego dopiero od niedawna próbuje się je wykorzystywać w praktyce.

Źródło: pl.wikipedia.pl

→ Slide 51
  • Środowisko uruchomieniowe : MS Test, NUnit, xUnit.net (wtyczka do R#)

↓ Slide 52

Unit test explorer

↓ Slide 53

Unit test sesions

↓ Slide 54

dotTrace i DotCover

  • profilowanie testów dotTrace Performance
  • Pokrycie kodu DotCover

→ Slide 55
  • Uruchamianie testów w po stronie serwera - konfugiracja buldów
  • Raporty z wykonanych testów

→ Slide 56
  • Testy jednostkowe nie dają 100% pewności, że kod nie posiada błędów
  • Należy stosować je wraz z innymi testami: testy integracyjne, systemowe, obciążeniowe itd.
  • Nie wszystko da się przetestować: zachowania niedeterministyczne, aplikacje wielowątkowe
  • Na jedną linię kodu może przypadać nawet do 5 linii kodu testu - nie zawsze jest to opłacalne
  • Kod testujący jak każdy inny kod nie jest odporny na błędy
→ Slide 57