REVEAL
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
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 ?
Czym są testy jednostkowe?
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.");
}
}
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)
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
.NET frameworks
Unit Test:
Frameworki izolacji
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
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
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);
}
Testy jednostkowe w VS
Szablony projektów: Unit Test Project, Visual C#, C++
Generowanie metod testowych
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)
-
Artybuty
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
Kontekst testu
Setup fixture
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.10
aplikacja uruchamiająca testy:
GUI, console
-
GUI
CLI
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
Data Driven Unit Test
[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.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;
}
}
[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
[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
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
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 (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
Testy w Resharper
Unit test explorer
Unit test sesions
dotTrace i DotCover
Integracja z TFS
Uruchamianie testów w po stronie serwera (Build)
Raporty z wykonanych testów
Tworzenie zadań z wyników 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