Testy Jednostkowe

Download Report

Transcript Testy Jednostkowe

Zaawansowane
Techniki Obiektowe
Wprowadzenie
Jak pisać UT ?
TESTY JEDNOSTKOWE WPROWADZENIE
W czym pomagają testy jednostkowe?




Ułatwiają znajdowanie błedów
Ułatwiają zrozumienie kodu
Ułatwiają utrzymanie kodu
Ułatwiają pisanie kodu
Test jednostkowy

Jest to automatyczny fragment kodu uruchamiający i
weryfikujący poprawność wykonania pewnego aspektu
kodu produkcyjnego

Testy są pisane z wykorzystaniem framework-ów. Dzięki
temu mogą być stworzone i uruchamiane szybko i łatwo:





NUnit
MSTest
MBUnit
Xunit
Testy mogą być uruchamiane pojedynczo lub masowo
przez każdego członka zespołu. Są częścią projektu ale
nie są dostarczane do klientów.
NUnit
Dedykowane GUI
 Wtyczki do VS:

 R#
 TestDriven.Net
Test jednostkowy – elementy (1)
bool IsLoginOk(string user, string password);
[TestFixture]
Class TestClass {
[Test]
public void TestLogin()
{
LoginComponent sut = new LoginComponent ();
bool result = sut.IsLoginOk("user","password");
Assert.AreEqual (false,result,
"invalid user/password shouldn't be accepted");
}
}
NUnit – troche infrastruktury

[SetUp] - metoda wywoływana przed każdym
testem.
 Konstruktor nie jest wywoływany w takim
momencie bo obiekt nie jest tworzony za
każdym razem
[TearDown] – metoda wywoływana po
każdym teście
 [FixtureSetup]/[FixtureTeardown] –
analogicznie

Test jednostkowy – elementy (2)
[TestFixture]
Class TestClass {
LoginComponent sut;
[SetUp]
public void Init() { sut = new LoginComponent (); }
[Test]
public void TestLogin2()
{
var result = sut.IsLoginOk("Iksinski","realPassword"););
Assert.AreEqual(true,result,
" valid user/password should be acceprted")
);
}
}
Test jednostkowy – elementy (3)
[Test]
[ExpectedException(typeofInvalidArgumentException))]
public void TestLogin3()
{
var result = sut.IsLoginOk(null,null);
}
Test jednostkowy – przykład 1
public class Authentication
{
private string _key;
public string Key
{
get {return _key;}
set {_key = value;}
}
public string EncodePassword(string password)
{
if (password==null || password.Length==0)
{
throw new ValidationException
("Password is empty");
}
// do the encoding
...
return encoded_password;
}
Test jednostkowy – przykład 1 cd.
[TestFixture]
public class TestFixture1
{
Authentication authenticator;
[SetUp] public void Init()
{
// set up our authenticator and key
authenticator = new Authentication();
authenticator.Key = "TESTKEY";
}
[Test]
public void Encoding_ForArgument_ShouldReturnProperValue()
{
String result = authenticator.EncodePassword("user");
// Validate that for "user" and "TESTKEY"
//key we should get proper result
Assert.AreEqual("fwe94t-gft5", result);
}
NUnit podstawowy model asercji
Are(Not)Equals, AreSame
 Contains
 Greater, GreaterOrEqual, Less,
LessOrEqual
 IsEmpty, IsNaN, IsFalse, IsTrue, ,
Is(Not)Null,
 Is(Not)InstanceOfType,
Is(Not)AssignableFrom

NUnit – Assert + fluent interface
Assert.That(1 + 1, Is.EqualTo(2));
 Assert.That(2.5000 + 2.5001, Is.EqualTo(5).Within(.0001));
 Assert.That( "Hello", Is.EqualTo( "hello" ).IgnoreCase );
 Assert.That(o1, Is.SameAs(o2));
 Assert.That(new ArrayList(), Is.Empty);
 Assert.That(ht, Is.InstanceOfType(typeof(IDictionary)));
 Assert.That( phrase, Text.Contains( "tests fail" ) );
 Assert.That( phrase, Text.EndsWith( "PASSING!" )
.IgnoreCase );
 Assert.That( phrase, Text.Matches( "Make.*tests.*pass" ) );
 Assert.That( iarray, Has.Some.GreaterThan(2));
...i inne

MSTest vs NUnit

Analogiczne atrybuty np.:
TestFixture -> TestClass
 Test -> Test Method
 SetUp – TestSetUp
Nieco słabszy model asertów
[Timeout], [DataSource]
Nieintuicyjna organizacja testów: listy testów, wykonanie
w oddzielnych katalogach
Automatycznie generowane testy (niekoniecznie
sensowna struktura, nazewnictwo itd?),
Generowane akcesory do prywatnych składowych (czy
prywatne elementy powinny byc testowane?)
Wparcie ze strony IDE







Testy sterowane danymi




Pojedynczy kod testu (parametryzowany)
Test jest uruchamiany wielokrotnie dla różnych
zestawów danych
Dane dla testu mogą być umieszczone w kodzie
lub brane z zewnętrznych źródeł (txt, xml, csv,
xls, mdb itd.)
UWAGA: to nie jest panaceum
–
słaba diagnostyka
Testy sterowane danymi MSTest
[TestClass]
public class TestClass
{
[TestMethod]
[DeploymentItem("FPNWIND.MDB")]
[DataSource("System.Data.OleDb", "Provider=Microsoft.Jet.OLEDB.4.0;
Data Source=\"FPNWIND.MDB\"", "Employees",
DataAccessMethod.Sequential)]
public void TestMethod()
{
Console.WriteLine( "EmployeeID: {0}, LastName: {1}",
TestContext.DataRow["EmployeeID"],
TestContext.DataRow["LastName"] );
}
}
Testy sterowane danymi NUnit
[TestCase(2.5d, 2d, Result=1.25d)]
[TestCase(-2.5d, 1d, Result = -2.5d)]
public double ValidateDivision(double numerator, double denominator)
{
var myClass = new MyClass();
return myClass.Divide(numerator,denominator);
}
TESTY JEDNOSTKOWE
- JAK PISAĆ DOBRE TESTY
Dobre testy jednostkowe






Zrozumiałe
Powtarzalne
Niezależne
Szybkie
Łatwe do uruchomienia
Łatwe w utrzymaniu
Po co pisać testy jednostkowe?




Testy weryfikują na bieżąco konkretne aspekty
zachowania klas. Złamanie założeń powoduje
załamanie konkretnych testów.
Przy dodawaniu/zmianach funkcjonalności testy
chronią przed zepsuciem już zaimplementowanych funkcji.
Stanowią dokumentację i zarazem przykłady
użycia
Kod powinień być pisany prosto. Działający kod
można i należy udoskonalać. Aby to było
bezpieczne potrzebne są testy.
(TJ) Jak pisać testy?
Testy powinny testować jedną klasę/funkcję a nie cały
system...
 Kod nie może zawierać "hack-ów" (if test ....)
 Test który zawsze działa – nic nie testuje. Zawsze
należy sprawdzić czy są przypadki gdy test zawodzi
 Typowy kod jest trudny do testowania.
 Testy dla istniejącego (i stabilnego kodu) mają
umiarkowany sens (chyba że chcemy kod zmieniać)
Dwa podejścia:
 Testy piszemy po (zaraz po) napisaniu kodu – w ten
sposób możemy kod stosunkowo łatwo zmienić, zawsze
należy sprawdzić czy test upada
 Testy piszemy przed kodem (TDD/BDD)

(TJ) Jak nazywać testy?




Nazwa testu powinna dobrze lokalizować błąd.
Najlepiej bez debugowania, analizy komunikatów.
Czy nazwy w prezentowanych przykładach były
dobre?
Dobre nazwy zwalniają ze szczegółowych
komunikatów przy asercjach
Konwencje
 LoginComponent_InvalidUser_ShuldThrowException
 WhenUserIsInvalid.

IsLoginOk_shouldthrowException
Trudno nazwać test, który dotyczy wiele aspektów
zachowania klasy
(TJ) Jak używać testów?




Są często (stale?) uruchamiane podczas
kodowania
Są cyklicznie uruchamiane na serwerze buildów.
Testy odzwierciedlają kontakt pomiędzy
użytkownikiem i dostarczycielem funkcjonalności
Testy stanowią wyznacznik jakości architektury
kodu -> testy mogą służyć tworzeniu dobrej
architektury (TDD/BDD)
(TJ) Co testować


Logikę. Instrukcje warunkowe, pętle itd.
Testowanie prostych properties/funkcji mija się z
celem.
Publiczny interfejs. Jeżeli metody prywatne
zawierają nietrywialna logikę może to znak, że
klasa powinna zostać zrefaktoryzowana.
 Np.
samochód vs. silnik
(TJ) Życie prywatne klasy
Jeżeli testy wymagają dostępu do
niepublicznych składników np. dla weryfikacji
stanu (niepokojące...) :
 Nie
należy rozhermetyzować klasy
 Można dodać klasę potomną dla potrzeb testu
(składniki protected)
 Można użyć refleksji
(TJ) Inicjalizacja Sut


SUT = system under test
SUT nie powinien być wspołdzielony pomiędzy wieloma
testami (tj inicjalizacja test1, test2 itd).



Wrażliwość na kolejnośc wykonania
Trudna diagnostyka
Sut może być kazdorazowo inicjowany w teście lub
inicjowany w SetUp. To drugie poejście ułatwia redukcje
redundancji
(TJ) Jakośc kodu





Testy to też kod – równiez powinien być (bardzo) dobrej
jakości
 Krótki, zrozumiały kod
 Dobre nazewnictwo
 Brak powtórzeń
Testy można i należy refaktoryzować
Testy nie powinny zawierać logiki – jak testować testy?
Jeśli test zawiera logikę należy ją wydzielić (np. do funkcji).
Takie funkcje mozna przetestować.
Dobrej jakości testy nie wymagają intensywnej pielęgnacji.
Projekty padają nie z powodu braku ale z powodu złej
jakości testów
(TJ) Duplikacja

Duplikacja to ZŁO :
 Duży
koszt pielęgnacji
 Utrudniona poprawa testów/rozwój kodu (Rak testów)

W celu uniknięcia duplikacji:
 Buildery
obiektów testowych
 Własne asercje
 Metody weryfikujące
 Testy sterowane danymi
(TJ) Struktura

Testy można grupować w klasy (np. dla
wspólnej inicjalizacjj SUT)

Jedna klasa testowa nie musi (i zwykle nie
odpowiada) jednej klasie testowanej raczej
konkretnym danym testowym
Czesto (zwykle?) dla pojedynczej funkcji piszemy
kilka testów: jeden test - jeden aspekt działania
funkcji (jeden asert logiczny)

(TJ) Filozofia: definiowania testów
Jeden po drugim: przyrostowy development
 Wszystkie na raz: np definiujemy pojedyncze
user story jako sekwencje testów

(TJ) Filozofia: budowa test fixture

Up front
 Łatwo
o błedny projekt
 Niepotrzebny kod – YAGNI (You aren't gonna
need it)

Test po teście:
Nie należy pisać kodu na wyrost
• Przyrostowy development
• Fresh Fixture
•
(TJ) Filozofia: co testować
Stan obiektów
 Zachowanie obiektów:

Testujemy wołania innych funkcji/obiektów
• Intensywne użycie "test doubles" – delikatne
testy
• Zasada proś [o przysługę] nie pytaj [o stan]
•
•
Jak trzeba mieszamy podejścia
(TJ) Test doubles (zastępcy?)
(TJ) Warto poczytać, popatrzeć ...



Andy Hunt, Dave Thomas "Pragmatic Unit Testing
in C# with Nunit"
Roy Osherove "The Art of Unit Testing with
Examples in .NET"
Gerard Meszaros "xUnit Test Patterns"
Prezenacje wideo:
 "Roy Osherove - Understanding Test Driven
Development.wmv"
 "Roy Osherove - Unit Testing Best Practices.wmv"
TESTY JEDNOSTKOWE
- TESTOWANIE ZACHOWANIA
InvoiceProcessor
Zachowanie ...
-sender : ISender
-logger : ILogger
+Process()
+InvoiceProcessor()
1
1
«interface»
ISender
+Send()
public class InvoiceProcessor {
private ISender sender;
InvoiceSender
private ILogger logger;
public InvoiceProcessor(ISender nSender, ILogger nLogger) { +Send()
sender = newSender;
logger = nLogger;
}
public bool Process(...) {
logger.Log("start");
if (...) {
...
bool ret = sender.Send(invoice);
TEST
...
}
}
}
var procesor = new InvoiceProcesor(new InvoiceSender(...), new Logger());
...to nie stan
Problem 1: ignorujemy zachowanie kodu logger.Log()
Problem 2: nie mamy skonfigurowanego sendera –czy
sender.Send() zwrócil true czy false
Problem 3: czy sender zostal wywolany i z jakimi
paramerami
Wymagane zastępstwo
Problem 1:
public class FakeLogger : Ilogger {
public void Log(string msg) {}
}
Problem 2:
public class FakeSender : ISender {
public bool Ret = true;
public bool Send (obiect toSend) { return Ret; }
}
Wymagane zastępstwo
Problem 3:
public class FakeSenderValidator : ISender {
public bool Ret = true;
public bool SendWasCalled = false;
public object SendArgument;
public bool Send (object toSend) {
SendWasCalled = true;
SendArgument = toSend;
return Ret;
}
}
Bez nowych klas...
Stub:
– obiekt kreowany dynamicznie – akceptujący wołania i
ew. Zwracający konkretne wartości
Mock:
– obiekt kreowany dynamicznie – z mozliwością
weryfikacji konkretnych zachowań
Mocking frameworks:
 Nmock, Moq – stosunkowo proste
 Rhino mock – bardzo zaawansowany
 TypeMock – jeszcze bardziej zaawansowany ale ...
komercyjny
Przykład 1, 2
[Test]
public void Process_whenSendingSuccesful_...() {
//Problem1:
var logger = MockRepository.GenerateStub<ILogger>();
//Problem2:
var sender = MockRepository.GenerateStub<ISender>();
sender. Stub(s => s.Send(null)).
IgnoreArguments().
Return(true);
InvoiceProcessor sut = new InvoiceProcessor(sender, logger);
var result = Sut.Process(....);
...
}
Przykład 3
[Test]
public void Process_whenSendingSuccesful_...() {
var logger = MockRepository.GenerateStub<ILogger>();
var sender = MockRepository.GenerateStub<ISender>();
sender. Stub(s => s.Send(null)).
IgnoreArguments().
Return(true);
Invoice invoice = ...;
InvoiceProcessor sut = new InvoiceProcessor(sender, logger);
var result = Sut.Process(invoice);
...
//Problem 3:
sender.AssertWasCalled( s => s.Send(invoice) );
}
Więcej o opcjach
sender.AssertWasCalled( s => s.Send(null),
options => options.IgnoreArguments()
.Repeat.Twice());
sender.AssertWasNotCalled( s => s.Send(null) );
sender.AssertWasCalled (
s=>s. Error(0,null),
options => options. IgnoreArguments()
.Constraints(Is.LessThan(10),
Text.StartsWith("Error"))
.Repeat.Twice());
Jakie parametry miało wołanie Error
int cnt = 0;
sender.Stub(s => s.Error(0, 0))
.IgnoreArguments()
.Do(new CallbackDelegate(MyFunction));
.Do((Delegates.Action<int, string>) delegate(int x, string msg)
{
Console.WriteLine(msg);
cnt = cnt + x;
}
)
.WhenCalled(
tmp => {
Console.WriteLine(tmp.Arguments[1]);
cnt = cnt + (int) tmp.Arguments[0];
}
)
Po kolei ...
Stara składnia (nowa niestety nie
wspiera takich konstrukcji)
var mockery = new MockRepository();
var sender = mockery.DynamicMock<ISender>();
using (mockery.Ordered())
{
sender .Expect(s => s.Send(null))
.IgnoreArguments()
.Return(false)
.Repeat.Times (3);
sender.Expect(s => s.Error(0,null))
.IgnoreArguments()
.Repeat.AtLeastOnce();
}
mockery.ReplayAll();
•Uwaga! Ostrożnie! Delikatne testy!
- Nie należy przesadzać z określaniem kolejności!
Co Nosorożec może a czego nie...
2 rodzaje składni (nowa: silne typowanie, jednorodna składnia
dla funkcji z typem i void, wsparcie dla składni AAA – tj.
AssertThatWasCalled)
Można mockować (Rhino Mock):
 Elementy interfejsu (funkcje, properties)
 Funkcje, properties wirtualne
 Wybrane składowe klas (-> PartialMock)
Nie można mockować (Rhino Mock):
 Klasy sealed
 Statyczne składniki
 Funkcje niewirtualnne
Problemy przy testach
Niejawne wejście - środowisko zewnetrzne np.:
 Pojawienie się pliku
 Brak pamieci
 Pojawienie się procesu
 Otrzymanie maila
 Przyciśnięcie przycisku w
GUI
 Zmiana danych w bazie
Niejawne wyjście – efekt
działania kodu np.:
 Skasowanie pliku
 Zabicie procesu
 Wysłanie maila
 Wyświetlenie czegoś na
ekranie, zmiana stanu
elementow GUI
 Zapis danych do bazy
Trudny test
SystemMonitor
+StartMonitoring()
public void StartMonitoring(...)
{
Niejawne wejście
...
if (System.IO.File.Exists("myFile"))
//send email
}
Niejawne wyjście
SystemMonitor
Dedykowana Podklasa
class SystemMonitor{
public void StartMonitoring(...)
{
...
if (FileExists("myFile"))
SendEmail(...)
}
protected virtual bool FileExists(string fileName) {
return System.IO.File.Exists(fileName);
}
protected virtual bool SendEmail (...) {
//send email
}
}
+StartMonitoring()
#FileExists() : bool
#SendEmail()
SystemMonitor
Dedykowana Podklasa
+StartMonitoring()
#FileExists() : bool
#SendEmail()
class SystemMonitorTestSubclas : SystemMonitor {
public bool fileExists = true;
SystemMonitorTestSubclas
public bool emailSent = false;
+fileExists : byte
+emailSent : bool
public virtual void SendEmail(...) { emailSent = true; } #FileExists() : bool
public virtual bool FileExists (...) { return fileExists; } #SendEmail()
}
var sut = new SystemMonitorTestSubclas ();
A z mockiem:
var sut = MockRepository.GeneratePartialMock< SystemMonitor >();
sut.Stub(s => s.FileExist (null)).IgnoreArguments().Return(true);
sut.Stub(s => s.SendEmail(null)).IgnoreArguments();
....
sut.StartMonitoring();
sut.AssertWasCalled( s => s.SendEmail(null),
options => IgnoreArguments());
MailSender
Obiekty izolujący
SystemMonitor
-sender
-fileSystem
+StartMonitoring()
class SystemMonitor {
private FileSystemProvider fileSystem;
private MailSenser sender;
public void StartMonitoring(...)
{
...
while(...) {
...
if (fileSystem.FileExists("myFile"))
sender.SendEmail(...)
}
}
}
+Send()
FileSystemProvider
+FileExists()
Warto poczytać, popatrzeć ...



Andy Hunt, Dave Thomas "Pragmatic Unit Testing
in C# with Nunit"
Roy Osherove "The Art of Unit Testing with
Examples in .NET"
http://www.ayende.com/wiki/Rhino+Mocks.ashx
Prezenacje wideo:
 "Roy Osherove - Test Driven Development, Using
Mock Objects.wmv"
 "Ayende Rahien - Interaction Based Testing with
Rhino Mocks.wmv"