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

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 ?

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($"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"); } }
class Program { static void Main(string[] args) { KalkulatorTest test = new KalkulatorTest(); test.SumaTest(); test.SumaTestException(); Console.WriteLine("Wszystkie testy zaliczone."); } }

Porażka

Sukces

[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), 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.

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

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)

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.

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

Java -> JUnit, TestNG

PHP -> PHPUnit

C -> MinUnit, CUnit, (biblioteka assert.h), cmocka

C++ -> CppUnit, Cantata, GoogleTest, Boost Test Library

.Net -> Visual Studio Unit Testing Framework (MS Test, VS Prof./Ultimate), NUnit, xUnit.net

Python -> PyUnit

MATLAB Unit Testing Framework

Delphi -> DUnit, Fortran -> pFUnit

Wikipedia: List of unit testing frameworks

Unit Test:

MS Test, Nunit, XUnit.net, MBUnit

Porównanie

Frameworki izolacji

Moq The most popular and friendly mocking framework for .NET

Rhino mock

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

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

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?

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); }

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

Namespace: Microsoft.VisualStudio.TestTools.UnitTesting

Wiele rozszerzeń wspierających: Resharper, TestDriven.net

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

Uruchomienie wybranych testów

Debbugowanie wybranych testów

Pokrycie kodu

Okna: Test Explorer, Code Coverage Results

Assert Class (MSDN)

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 pojedynczej 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łączenia do danych (data row, Data Driven Unit Test)

Informacje o wykonywanym teście (np. ścieżki dostępu)

Informacje 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

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

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

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

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

[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

[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 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); }

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

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

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 (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% .

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

Uruchamianie testów w po stronie serwera (Build)

Raporty z wykonanych testów

Tworzenie zadań z wyników testów

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

Testy jednostkowe w Visual Studio

Jeremy Lindblom, Unit Testing

List of unit testing frameworks

XUnit Test Patterns, Gerard Meszaros

Beginning Mocking With Moq 3 by Justin Etheredge (part 2, part 3, part 4)

Introduction to unit testing

Verifying Code by Using Unit Tests (MSDN)