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
Aksjomaty testowania
- 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
Czym są testy jednostkowe?
Przykład: klasa testowana
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(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"); } }
Przykład: środowisko uruchomieniowe
class Program { static void Main(string[] args) { KalkulatorTest test = new KalkulatorTest(); test.SumaTest(); test.SumaTestException(); Console.WriteLine("Wszystkie testy zaliczone."); } }
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
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)
Korzyści TDD i testowania
- 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
Struktura testw jednostkowych
Wzorzec AAA: Arrange → Act → Assert
- przygotowanie środowiska testowego (Fixtures)
- ustawienie stanu wejściowego
- uruchomienie testowanych jednostek kodu
- 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 : 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.
Unit Test Frameworks
- 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
Narzędzia i biblioteki wspierające tworzenie testów, ich organizację, automatyzację wykonywania, raportowanie
List_of_unit_testing_frameworks
DotNET 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, …
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 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
Scenariusze
- 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
Zakres dostępu
- 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)
Testy jednostkowe w Visual Studio
- 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
UT w VS 2012/2013
Szablony projektów: Unit test C#, C++
Menu test
- Uruchomienie wybranych testów
- Debbugowanie wybranych testów
- Pokrycie kodu
- Okna: Test Explorer, Code Coverage Results
VS Test Explorer
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
private TestContext testContextInstance; public TestContext TestContext { get { return testContextInstance; } set { testContextInstance = value; } }
Setup fixture
Data Driven Unit Test
- 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
NUnit
- 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
„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 konsolixunit.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
Xunit.net: DDT Inline
[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
Xunit.net: DDT Property
[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 { return new[] { new object[] { "xUnit", 1 }, new object[] { "is fun", 2 }, new bject[] { "to test with", 3 } }; } }
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 }
[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ł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, ….
Mock objects
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
- 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% . 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
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
Testy w Resharper
- Środowisko uruchomieniowe : MS Test, NUnit, xUnit.net (wtyczka do R#)
Unit test explorer
Unit test sesions
dotTrace i DotCover
- profilowanie testów dotTrace Performance
- Pokrycie kodu DotCover
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
Podsumowanie
- 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
Bibliografia
- Jeremy Lindblom, Unit Testing
- XUnit Test Patterns, Gerard Meszaros