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"