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
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
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
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
-
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
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)
-
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
Kontekst testu
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
-
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 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
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;
}
}
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
[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
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
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
Testy w Resharper
Unit test explorer
Unit test sesions
dotTrace i 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