520 likes | 627 Views
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.
E N D
ZaawansowaneTechnikiObiektowe Wprowadzenie Jak pisać UT ?
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 publicclass Authentication {privatestring _key;publicstring Key { get {return _key;} set {_key = value;} }publicstringEncodePassword(stringpassword) {if (password==null || password.Length==0) {thrownewValidationException("Password is empty");}// do the encoding ...returnencoded_password; }
Test jednostkowy – przykład 1 cd. [TestFixture] publicclass TestFixture1 { Authentication authenticator; [SetUp] publicvoid Init() { // set up our authenticator and key authenticator = new Authentication();authenticator.Key = "TESTKEY"; } [Test] publicvoid Encoding_ForArgument_ShouldReturnProperValue() { String result = authenticator.EncodePassword("user"); // Validate that for "user" and "TESTKEY" //key we should get proper resultAssert.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) { varmyClass = new MyClass(); return myClass.Divide(numerator,denominator); }
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ż zaimplemen-towanych 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 (zarazpo) 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) Warto poczytać, popatrzeć ... • Andy Hunt, Dave Thomas "Pragmatic Unit Testingin C# with Nunit" • Roy Osherove "The Art of Unit Testingwith Examples in .NET" • Gerard Meszaros "xUnit Test Patterns" Prezenacje wideo: • "Roy Osherove - Understanding Test Driven Development.wmv" • "Roy Osherove - Unit Testing Best Practices.wmv"
Zachowanie ... public class InvoiceProcessor {private ISender sender;private ILogger logger; public InvoiceProcessor(ISender nSender, ILogger nLogger) { sender = newSender; logger = nLogger; } public bool Process(...) { logger.Log("start"); if (...) { ... bool ret = sender.Send(invoice); ... }} } var procesor = new InvoiceProcesor(new InvoiceSender(...), new Logger()); TEST
...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 - środo-wisko 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 public void StartMonitoring(...) { ... if (System.IO.File.Exists("myFile")) //send email } Niejawne wejście Niejawne wyjście
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 } }
Dedykowana Podklasa class SystemMonitorTestSubclas : SystemMonitor { public bool fileExists = true; public bool emailSent = false; public virtual void SendEmail(...) { emailSent = true; } public virtual bool FileExists (...) { return fileExists; } } var sut = new SystemMonitorTestSubclas (); A z mockiem: varsut = 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());