→ Slide 1
Testy jednostkowe
→ Slide 2
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
↓ Slide 3
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 ?
↓ Slide 4
Czym są testy jednostkowe?
↓ Slide 5
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;
}
}
↓ Slide 6
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");
}
}
↓ Slide 7
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
↓ Slide 8
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);
}
}
→ Slide 9
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.
↓ Slide 10
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
→ Slide 11
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)
↓ Slide 12
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.
→ Slide 13
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, …
↓ Slide 14
↓ Slide 15
.NET frameworks
Unit Test:
Frameworki izolacji
↓ Slide 16
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);
}
}
↓ Slide 17
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);
}
}
↓ Slide 18
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));
}
}
→ Slide 19
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
↓ Slide 20
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
↓ Slide 21
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?
↓ Slide 22
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);
}
→ Slide 23
Testy jednostkowe w VS
Szablony projektów: Unit Test Project, Visual C#, C++
↓ Slide 24
Generowanie metod testowych
↓ Slide 25
Uruchomienie wybranych testów
Debbugowanie wybranych testów
Pokrycie kodu
Okna: Test Explorer, Code Coverage Results
↓ Slide 26
↓ Slide 27
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)
-
↓ Slide 28
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
↓ Slide 29
↓ Slide 30
Setup fixture
→ Slide 31
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
-
↓ Slide 32
GUI
↓ Slide 33
CLI
→ Slide 34
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
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
→ Slide 35
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
→ Slide 36
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
→ Slide 37
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);
}
→ Slide 38
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
}
→ Slide 39
AutoData
[Theory, AutoData]
public void IntroductoryTest(int expectedNumber, MyClass sut)
{
int result = sut.Echo(expectedNumber);
Assert.Equal(expectedNumber, result);
}
→ Slide 40
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, ….
-
↓ Slide 41
↓ Slide 42
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)
↓ Slide 43
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
↓ Slide 44
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();
}
}
↓ Slide 45
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);
→ Slide 46
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% .
↓ Slide 47
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
↓ Slide 48
→ Slide 49
Testy w Resharper
↓ Slide 50
Unit test explorer
↓ Slide 51
Unit test sesions
↓ Slide 52
dotTrace i DotCover
→ Slide 53
Integracja z TFS
Uruchamianie testów w po stronie serwera (Build)
Raporty z wykonanych testów
Tworzenie zadań z wyników testów
→ Slide 54
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
→ Slide 55