Categories
C#

Czy ufasz programom?

Wpis [part not set] z 8 w serii C# - podstawy

Zaufanie to złożone zagadnienie. W zależności od naszych doświadczeń nasz poziom zaufania do ludzi jest różny. Jeżeli w przeszłości nieprzyjemnie sparzyliśmy się względem drugiej osoby to trudno nam przyjdzie jej zaufać. Z drugiej strony, w przypadku solidnej współpracy łatwiej nam przyjdzie powierzyć innemu człowiekowi nasze tajemnice czy dobra materialne.

I choć daleki jestem od personifikowania oprogramowania to dostrzegam tutaj kilka podobieństw. Odpowiedz proszę na pytanie: jak często aktualizujesz programy na Twoim komputerze? Czujesz, na ekranie pojawia się nowe okno Dostępna jest aktualizacja, stajesz przed wyborem: Aktualizuj lub Odłóż na później. Którą pigułkę wybierzesz? Jeżeli w przeszłości miałeś problemy po lub w trakcie aktualizacji to nie będziesz skory do ponownego wskoczenia do tej samej rzeki – będziesz odkładał update w nieskończoność. Przyczyną tego zachowania była utrata zaufania – poprzedni proces aktualizacji nie przebiegł tak bajkowo jak obiecywał ekran z nowościami.

A teraz podkręćmy miernik konsekwencji. Twoje biuro projektowe zakupiło nowiutki, lśniący program do MESu. Sprzedawca obiecywał, że teraz połowa dotychczasowej roboty zrobi się sama a raporty będą tak czytelne, że nawet Twoja teściowa zrozumie jak redukowane są momenty zginające nad słupami. Uruchamiasz nowy soft, poznajesz jego interfejs, modelujesz pierwsze konstrukcje. Myślisz – kurczę, faktycznie kawał dobrego programu, pracuje się aż miło – trzyma Cię jeszcze fala początkowego entuzjazmu. Wypluwasz raport, za raportem, koledzy z biurek obok patrzą z niedowierzaniem jak bardzo (znów) jarasz się pracą. Jeden z nich, najbardziej doświadczony podchodzi, patrzy na przebieg wykresu sił wewnętrznych w belce i kiwa z niedowierzaniem głową. Odkrył błąd, gruby błąd w Twojej najnowszej zabawce, który spowodowałby nadmierne zarysowania konstrukcji i uruchomiłby ulubioną grę budowlańców: spier*lo-pong, czyli wzajemne przerzucanie się odpowiedzialnością kto zawinił. W tym wypadku, winnym byłbyś Ty – za bardzo zaufałeś nowemu programowi, którego producent w warunkach użytkowania zastrzegł, że całą odpowiedzialność zrzuca na Ciebie, bo jesteś przecież doświadczonym inżynierem.

Trochę przejaskrawiłem, wiem, ale potrzebowałem tej historii żeby wskazać, że błędy w oprogramowaniu mogą być bardzo kosztowne i zazwyczaj to użytkownik ponosi ich konsekwencje. Błędy, czyli bugi towarzyszą rozwojowi softu od pierwszych dni i pozostają z nim, większe lub mniejsze, przez cały czas. Programowanie to złożony proces, to wykuwanie nowej, wirtualnej materii przy pomocy potęgi ludzkiego umysłu i sprawności palcy. Czasami, gdzieś pomiędzy kolejnymi uderzeniami klawiszy do kodu źródłowego wkradną się błędy .

Bugów nie unikniemy mówiąc pracuj uważniej, skup się, przestań myśleć o porannej walce o założenie czapki przez Twoje dziecko. One były, są i będą – lepiej zaakceptujmy ten fakt i zastanówmy się jak minimalizować ich częstotliwość, jak szybko je wykrywać.

Spójrz na poniższą metodę. Czy potrafisz na podstawie tego screena powiedzieć jaka będzie wartość Fctm dla klasy betonu C40/50?

Ja, jako autor, również z marszu Ci nie powiem, muszę sie zastanowić, przeanalizować flow działania. Program składa się z setek, tysięcy, milionów podobnych metod, z których każda wykonuje inne czynności. Szukanie błędów metodą organoleptyczną, scrollując po kodzie to ostateczność, często konieczna, ale w ostatniej fazie, kiedy już wiemy, że gdzieś tam czai się błąd. Tylko skąd wiemy, że program posiada błędy?

Możemy poczekać na maile/telefony od zdenerwowanych użytkowników, którzy napotkają na bugi. To jazda po bandzie, zwłaszcza w przypadku płatnego oprogramowania, gdzie użytkownik oczekuje jakości za wydane pieniądze. Nie traktujmy ich jako testerów.

Właśnie, testerzy! Może by tak zatrudnić nowych ludzi do testowania albo poprosić kolegę obok żeby między narysowaniem szachtu a zamodelowaniem biegu schodowego sprawdził nasz program. Na pierwsze nie ma pieniędzy, na drugie nie ma czasu. Przynajmniej w klasycznym środowisku budowlanym.

Potrzebujemy innego rozwiązania, czegoś co dawałoby poczucie pewności i niezawodności tworzonego przez nas oprogramowania. Czegoś, co samo testowałoby program, co sygnalizowałoby błędy zanim zauważy je użytkownik. Potrzebujemy programu testującego inny program – potrzebujemy testów jednostkowych.

Testy jednostkowe to specyficzny rodzaj testowania aplikacji. Kontroli poddawane są bardzo małe fragmenty kodu, zazwyczaj pojedyncze metody. Skoro średniej wielkości program składa się z tysięcy metod – testów też muszą być tysiące. Obawiasz się pewnie, że wykonanie takiej liczby testów to długie godziny żmudnej procedury. Uspokoję Cię, że testy jednostkowe mają ukierunkowane działanie, są szybkie przez co ich wykonanie to ułamki sekundy. Dlatego całkiem zgrabnie skaluja się wraz z rozwojem aplikacji.

Testy spełniają jedną podstawową rolę – dają programiście możliwość szybkiego sprawdzenia, że jego nowe zmiany nie zepsuły poprzedniej funkcjonalności programu. Pracując nad dużą aplikacją w wieloosobowym zespole nie sposób znać wszystkich smaczków i niuansów nagromadzonych w wyniku wielu miesięcy/lat pracy. Testy jednostkowe dają Ci spokój ducha i pozwalają sprawdzić przykazanie programisty: po pierwsze nie spier* tego co już działa.

Oczywiście testowanie odizolowanych fragmentów aplikacji z użyciem testów jednostkowych nie gwarantuję, że po zebraniu ich w większe bloki funkcyjne dalej wszystko będzie działało. Istnieją różne strategie testowania, na potrzeby tego wpisu skupię się wyłącznie na testach jednostkowych, bo chciałbym sprawdzić czy uzyskuję poprawne obiekty naszej nowej klasy Concrete.

Pierwszym krokiem jest dodanie nowego projektu do naszej solucji. Tym razem w polu wyszukiwania wpisz NUnit i wybierz wersję dla .NET Framework. NUnit to jedna z możliwych platform testowych, czyli zestaw narzędzi, które umożliwiają sprawne tworzenie i wykonywanie testów.

Standardowa konwencja nazywania projektów z testami to: <NazwaTestowanegoProjektu>.Tests, dlatego ja użyję nazwy ConcreteLibrary.Tests. Po utworzeniu projektu zmienię jeszcze nazwę początkowego pliku .cs w ramach nowego projektu na ConcreteTest – bo to właśnie klasę Concrete będę chciał przetestować.

Czas na dodanie zależności pomiędzy naszym nowym projektem a tym co już było. W tym celu kliknę PPM na references i wskażę w nowym oknie ConcreteLibrary.

Warto jeszcze zadokować nowe okno w Visual Studio, które umożliwi szybki podgląd wyników testowania. Włącz Test Explorer’a, a następnie przeciągnij go w obszar, gdzie dotychczas widziałeś Twoje projekty w formie drzewa (Solution Explorer):

Ok, pora na pierwszy test. Zacznę od negatywnego scenariusza, czyli próby stworzenia obiektu Concrete dla nieobsługiwanej klasy betonu, czyli czegoś co nie zaczyna się od litery C i nie posiada w swojej nazwie dokładnie jednego znaku /. Niech na początku będzie to pusty string. Próba utworzenia obiektu w tej sytuacji powinna zakończyć się rzucenie wyjątku (błędu w trakcie wykonywania). Sprawdźmy czy tak się stanie.

using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;

namespace ConcreteLibrary.Tests
{
    [TestClass]
    public class ConcreteTest
    {
        [TestMethod]
        public void Empty_concrete_name_is_invalid()
        {
            Assert.ThrowsException<ArgumentNullException>(() => new Concrete(""));
        }
    }
}

Nazewnictwo testów to przedmiot sporów i dyskusji akademików i praktyków. Ja preferuję nazewnictwo ‘naturalne’, czyli zapis prawie jak zdanie, które oddaję sens tego testu. Nazewnictwo testów jest kluczowe kiedy któryś z testów przestaje przechodzić – zwraca błąd. Przy jasnych i klarownych nazwach wystarczy wtedy rzut oka na nazwę testu żeby mieć pojęcie co poszło nie tak.

Ok, mamy zadeklarowany test, warto by sprawdzić czy on przechodzi. W tym celu przejdź do nowo dodanej zakładki Test Explorer i wybierz przycisk Run. Po chwili otrzymasz rezultat Twojego testu – zielono, znaczy jest dobrze.

Patrząc na nasz pierwszy test coś przyszło mi do głowy. Co się stanie jeżeli ktoś spróbuje utworzyć nowy obiekt klasy Concrete nie podając nic, przekazując do konstruktora null? No właśnie, sprawdźmy to pisząc nowy test i wykonajmy go.

Pięknie – program się wysypał! Ha, udało się w porę opatrzyć to zachowanie testem i sprawdzić zachowanie w kontrolowanych warunkach. To oznacza, że nasz konstruktor nie wie co zrobić w tej sytuacji. Zmieńmy to zachowanie modyfikując go w źródłowym projekcie ConcreteLibrary:

        public Concrete(string name)
        {
            if(string.IsNullOrEmpty(name))
                throw new ArgumentNullException(nameof(Name), "Provided concrete class name is not allowed");

            Name = name;

            InitalizeConcreteProperties();
        }

Nowością są linijki 3 i 4 w powyższym listingu. Sprawdzają one czy przekazany tekst w ogóle istnieje i czy przypadkiem nie jest pusty. Jeżeli taka sytuacja zaistnieje to zwrócony zostanie wyjątek.

Zmieniliśmy to co wychwycił test – sprawdźmy czy nasza zmiana zadziała i (co najważniejsze) czy nie zepsuliśmy tego co już działało. Po uruchomieniu wszystkich testów widzę już tylko kolor zielony – jest dobrze 🙂

Dodajmy jeszcze co najmniej dwa testy sprawdzające wartości powstające w trakcie inicjalizacji betonu. Dla Fctm mieliśmy instrukcję warunkową, która w zależności od wartości wytrzymałości na ściskanie wyznaczała kolejny parametr z różnych wzorów – sprawdźmy czy to działa. Tylko najpierw musimy do naszego testowego projektu dodać odwołanie do paczki nugetowej UnitsNet – tej od zamiany jednostek. Zrób to proszę identycznie jak w poprzednim wpisie.

Zestaw czterech testów sprawdzających podstawowe zachowanie konstruktora może wyglądać tak:

using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;

namespace ConcreteLibrary.Tests
{
    [TestClass]
    public class ConcreteTest
    {
        [TestMethod]
        public void Empty_concrete_name_is_invalid()
        {
            Assert.ThrowsException<ArgumentNullException>(() => new Concrete(""));
        }

        [TestMethod]
        public void Null_concrete_name_is_invalid()
        {
            Assert.ThrowsException<ArgumentNullException>(() => new Concrete(null));
        }

        [TestMethod]
        public void Check_concrete_parameters_when_less_than_50_MPa()
        {
            Concrete concrete = new Concrete("C30/37");

            double delta = 0.01;
            Assert.AreEqual(concrete.Fck.Megapascals, 30, delta);
            Assert.AreEqual(concrete.Fck_cube.Megapascals, 37, delta);
            Assert.AreEqual(concrete.Fcm.Megapascals, 38, delta);
            Assert.AreEqual(concrete.Fctm.Megapascals, 2.90, delta);
            Assert.AreEqual(concrete.ElasticModulus.Gigapascals, 32.837, delta);
        }

        [TestMethod]
        public void Check_concrete_parameters_when_greater_than_50_MPa()
        {
            Concrete concrete = new Concrete("C55/67");

            double delta = 0.01;
            Assert.AreEqual(concrete.Fck.Megapascals, 55, delta);
            Assert.AreEqual(concrete.Fck_cube.Megapascals, 67, delta);
            Assert.AreEqual(concrete.Fcm.Megapascals, 63, delta);
            Assert.AreEqual(concrete.Fctm.Megapascals, 4.21, delta);
            Assert.AreEqual(concrete.ElasticModulus.Gigapascals, 38.214, delta);
        }
    }
}

Czy cztery testy wystarczą? To zależy 😉 Wraz z pisaniem kolejnych metod w ramach klasy beton powinniśmy dodawać coraz to nowe testy, obejmujące nowe funkcjonalności. W ten sposób niejako jednocześnie rozwijamy naszą aplikację i naszego prywatnego testera, który będzie na nasze zawołanie sprawdzał efekty naszej pracy – przyznasz, że to kusząca perspektywa.

Series Navigation

Leave a Reply

Your email address will not be published. Required fields are marked *