View page as slide show

Testowanie - testy jednostkowe

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 lub obiektów w programowaniu obiektowym lub procedur w programowaniu proceduralnym. Testowany fragment programu poddawany jest testowi, który wykonuje go i porównuje wynik (np. zwrócone wartości, stan obiektu, wyrzucone wyjątki) z oczekiwanymi wynikami - tak pozytywnymi, jak i negatywnymi (niepowodzenie działania kodu w określonych sytuacjach również może podlegać testowaniu)

Ź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
  • Czy warto tracić czas na pisanie testów ?
  • Ż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
  • Testy są czasochłonne
  • Testowanie jest kosztowne
  • Testowanie jest ryzykowne
  • Paradoks pestycydów
  • Nie wszystkie znalezione błędy zostaną naprawione (za duży koszt, brak czasu, ryzyko naprawy)
  • Testy są nużące
  • Testowanie wymaga wyobraźni i złośliwości

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;
	}
}
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(String.Format("Test oblany: spodziewana wartosc {0}, aktualna wartosc {1}", oczekiwanyWynik, aktualnyWynik));
 
	}
 
	public void SumaTestException()
	{
		Kalkulator c = new Kalkulator();
		try {
			c.Suma(null);
		} 
      catch (ArgumentNullException) {
			return;
		}
		throw new Exception("Test oblany");
	}
}
class Program
{
	static void Main(string[] args)
	{
		KalkulatorTest test = new KalkulatorTest();
 
		test.SumaTest();
		test.SumaTestException();
 
		Console.WriteLine("Wszystkie testy zaliczone.");
	}
}
[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);
		}
 
}

Technika zwinna (agile), zaliczana także do programowania ekstremalnego.

  • 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.

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

  • Wczesne wykrywanie błedów. Odporność na błedy regresyjne. Zmniejszenie kosztu, błędy wykryte przez autora kodu i poprawiane na bieżąco kosztują niewiele
  • 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

Wzorzec AAA: Arrange → Act → Assert

  1. przygotowanie środowiska testowego (Fixtures)
  2. ustawienie stanu wejściowego
  3. uruchomienie testowanych jednostek kodu
  4. porównanie uzyskanych wyników (Assert)
  5. zwolnienie zasobów (Teardown)
  • Wzorzec xUnit zbiór środowisk testowych (frameworks) wywodzących swoją architekturę od SUnit (Kent Beck, 1998, Smaltalk).
  • Assercje : zbiór funkcji i makr weryfikujących 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 case : podstawowa klasa od której wywodzą się wszystkie testy
  • Test fixtures (test context) : zbiór warunków wymaganych do przeprowadzenia testu
  • Test suites : zbiór testów uruchamianych w jesdnym środowisku (fixture). Kolejność uruchomienia nie ma znaczenia.
  • 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, …

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

List_of_unit_testing_frameworks

  • Unit Test
    • MS Test
    • Nunit
    • XUnit.net
    • MBUnit
  • Isolation
    • Moq The most popular and friendly mocking framework for .NET
    • FakeItEasy The easy mocking library for .NET
    • NSubstitute A friendly substitute for .NET mocking frameworks
    • wiele innych: NMock, Typemock, …
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);
	}
}
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);
	}
}
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));
   }
}
  • Test wyłącznie pojedynczej jednostki, nie można pisać testów badających kilka funkcji naraz
  • Testy posinny być niezależne od siebie
  • Izolacja: testy niezależne od zewnętrznych zasobów, są one zastępowane makietami symulującymi ich działanie (mocq, fake, stub, …)
  • Przejrzystość - testy wyjaśniają i dokumentują kod
  • Szybkość - testy powinny wykonywać sie szybko, to nie są testy wydajności
  • Konwencje nazewnicze - nazwa testu oddaje intencje
  • Pisz kod tak aby był łatwy do testowania
  • Analiza ścieżek - badanie przebiegu mozliwych ścieżek od punktu początkowego do koncowego, wszystkie możliwe kombinacje rozgałęzień
    • Boundary test - działania w pętli pomijane lub pętla uruchamiana raz (dla wszystkicj ś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żniości - zbiór danych o podobnym sposobie przetwarzania. Testujemy wybrane elementy ze zbioru. na wejściu klka elementów przetwarzanych w ten sam spoó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
  • metody publiczne, 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 w dziedziczącej klasie bazowej
  • metody prywatne są jeszcze trudniejsze do przetestowania. Nie nie można ich wywołać bezpośrednio w kodzie. Jednak modyfikatory public, protected, private mają znaczenie wyłącznie dla kompilatora. Za pomocą mechanizmu refleksji można ominąć ograniczenia i wywołać prywatną metodę.
  • Czy testy jednostkowe powinny dotyczyć wyłącznie metod publicznych?
  • Testy funkcjonalne (czarna skrzynka), zazwyczaj wykonywanie przez osoby nie wytwarzające kodu
  • Testy strukturalne (biała/przezroczysta skrzynka)
  • jednostkowe - pojedyńcze 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łów
  • testy white-box: unit, regression, integration, mutation
  • testy black-box: acceptance, fuzz, smoke, performance (load, stress)
  • Narzędzia do generowania testów dostępne w VS 2010 ale nie w 2012/13
  • Tworzenie testu istniejącej metody:
    • Create Unit test
    • Wybieranie metod, dla których zostaną wygenerowane testy jednostkowe
    • wygenerowany nowy projekt zawierający testy jednostkowe
    • Automatycznie wygenerowana metoda testująca
  • Uruchamianie testów → Run Tests
  • Raport z wykonania testów
  • Namespace: Microsoft.VisualStudio.TestTools.UnitTesting
    • Assert
    • ExpectedExceptionAttribute
  • Wiele rozszerzeń wspierających, np.: Resharper, TestDriven.net

Szablony projektów: Unit test C#, C++

  • Uruchomienie wybranych testów
  • Debbugowanie wybranych testów
  • Pokrycie kodu
  • Okna: Test Explorer, Code Coverage Results

    • 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
  • 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

  • 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
private TestContext testContextInstance;
public TestContext TestContext
{
	get { return testContextInstance; }
	set { testContextInstance = value; }
}

  • Metody testowe mogą pobierać dane do testów ze źródeł dostępnych na maszynach uruchamiających testy
  • Atrybut DataSource określa źródło danych : bazy danych, pliki xml, tabele Exela, app.config, …
  • Metoda DataRow klasy TextContext pozwala pobrać wiersz danych
[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

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

„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
  • xUnit.net framework (NuGet)
  • 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

  • Teorie w xUnit.net
[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
  • 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łedy 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, …

Atrapy obiektów (mock objects) - symulowane obiekty naśladujące w kontrolowany sposób zachowanie rzeczywistych obiektów.

  • symulowanie połaczenia do bazy danych
  • symulowanie z zasobami, które mogą być niedostępne, np.: połaczenia z serwisami sieciowymi
  • symulowanie obiektów, które jeszcze nie powstały
  • symulowanie w celu zwiększenia szybkości testów
  • 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)
  • 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
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();
	}
}
// 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);

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% . Różne 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
  • 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


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

  • profilowanie testów dotTrace Performance
  • Pokrycie kodu DotCover

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

  • Testy jednostkowe nie dają 100% pewności, że kod nie posiada błedów
  • Należy stosować je wraz z innymi testami: testy integracyjne, systemowe, obciązeniowe 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 też zawiera błedy