Testy jednostkowe
Unit test - co to jest?
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
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 ?
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; } }
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"); } }
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
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); } }
Test-driven development (TDD)
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.
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
Wzorce w testowaniu jednostkowym
Wzorzec AAA: Arrange → Act → Assert
- przygotowanie środowiska testowego (Fixtures)
- ustawienie stanu wejściowego
- uruchomienie testowanych jednostek kodu (Act)
- porównanie uzyskanych wyników (Assert)
- zwolnienie zasobów (Teardown)
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.
Unit Test Frameworks
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, …
Środowiska testowe
Wikipedia: List of unit testing frameworks
.NET frameworks
- Unit Test:
- Frameworki izolacji
- 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, …
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); } }
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); } }
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)); } }
Dobrze napisane testy
- 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
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
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?
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); }
Rodzaje testów
- 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
Testy jednostkowe w VS
Szablony projektów: Unit Test Project, Visual C#, C++
- Namespace: Microsoft.VisualStudio.TestTools.UnitTesting
- Wiele rozszerzeń wspierających: Resharper, TestDriven.net
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
Menu test
- Uruchomienie wybranych testów
- Debbugowanie wybranych testów
- Pokrycie kodu
- Okna: Test Explorer, Code Coverage Results
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
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)
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
NUnit
- 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)
xUnit.net
„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
frameworkxunit.runner.console
uruchamianie testów w konsolixunit.runner.visualstudio
integracja z Test Explorer w VS- Wtyczka do Resharpera: runner for xUnit.net
Data Driven Unit Test
- 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); }
Xunit.net: Teorie
[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
Skip i Timeout
[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); }
AutoFixture
- 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 }
AutoData
[Theory, AutoData] public void IntroductoryTest(int expectedNumber, MyClass sut) { int result = sut.Echo(expectedNumber); Assert.Equal(expectedNumber, result); }
Techniki izolacji
- 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, ….
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
- Obiekty Fake i Mock realizują wzorzec Dependency injection
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)
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
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(); } }
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);
Pokrycie kodu (code coverage)
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% .
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
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
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
Integracja z TFS
- Uruchamianie testów w po stronie serwera - konfugiracja buldów
- Raporty z wykonanych testów
Podsumowanie
- 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
Bibliografia
- Jeremy Lindblom, Unit Testing
- XUnit Test Patterns, Gerard Meszaros