Testy jednostkowe–struktura folderów

Na blogu oraz MSDN pisałem niejednokrotnie o testach jednostkowych oraz integracyjnych. Ostatnio jednak zastanawiałem się nad podstawowym problemem – jak zorganizować to od strony struktury katalogowej? Musimy rozważyć następujące problemy:

  1. Testy powinny być w każdej chwili dostępnie do odpalenia i weryfikacji.
  2. Wykonując prostą refaktoryzację (zmiana nazwy klasy), automatycznie nazwa powinna zaktualizować się w testach.
  3. Testy jednostkowe stanowią dobrą dokumentację oraz instrukcję używania zaimplementowanej funkcjonalności. Z tego względu każdy z developerów z łatwością powinien odnaleźć dowolne projekty testów zanim przejdzie do konkretnej specyfikacji. Moim zdaniem jeśli ktoś chce zobaczyć co robi kod, powinien zacząć od przeczytania testów a nie właściwego kodu.
  4. Zbyt wysoka liczba projektów w solucji ZNACZĄCO obniża wydajność VS.
  5. Projekty zawierające testy będą odpalanie automatycznie na serwerze w jakiś odstępach czasu.

Zdecydowałem się podzielić testy na dwie grupy – wymagające konfiguracji oraz te, wykonywane w całości w pamięci. Warto zaznaczyć, że nie mam podziału na testy integracyjne oraz jednostkowe. O ile test integracyjny nie wymaga zasobów poza pamięcią (bazy danych, usługi itp.) lub jakieś konfiguracji (np. nazwa portu, ip address) wciąż przechowuje je w projekcie z innymi testami jednostkowymi z tym, że w odpowiednim namespace. Wciąż mogą być w każdej chwili odpalane i weryfikowane ponieważ nie wymagają jakiejkolwiek konfiguracji. Czyli jeśli mam test integracyjny który łączy klasę A z klasą B (połączenie jest in-memory), umieszczam go w odpowiednim namespace w testach jednostkowych.

Testy jednostkowe powinny być na bieżącą odpalane i sprawdzanie przez programistów (poza automatycznym uruchamianiem przez buildmachine). Aby to ułatwić wszystkie projekty testów umieszczam w osobnym folderze. Proponuje podział na trzy foldery: Production Code, In-Memory tests oraz Configurable tests. Na przykład:

Production Code:

  • SampleApp.Presentation.Behaviour.csproj
  • SampleApp.Presentation.Views.csproj
  • SampleApp.BusinessLayer.csproj
  • Device1 (folder)
    • SampleDevice.csproj

In-Memory Tests:

  • AllTests.sln (opcjonalne)
  • RunAll (opcjonalne)
  • SampleApp.Presentation.Behaviour.Test.csproj
  • SampleApp.BusinessLayer.Test.csproj
  • Device1 (folder)
    • SampleDevice.Testcsproj

Configurable Tests – analogicznie do In-Memory Tests

 

Struktura katalogów w In-Memory Tests jest taka sama jak w Production Code – z tym, że zamiast właściwego kodu, projekty zawierają wyłącznie testy. Opcjonalnie tworze AllTests.sln, który ułatwia ewentualne debugowanie (np. odpalamy wszystkie unit testy i chcemy sprawdzić co dokładnie popsuło się w którymś z nich).

Warto również w solucjach w Producton Code dodać referencje do projektu z testami. Dzięki temu jakakolwiek refaktoryzacja na kodzie, zostanie również wykonana na testach (np. zmiana klasy, kolejności parametrów itp.). Wadą tego jest oczywiście spowolnienie solucji i dlatego mam wątpliwości – w dużych solucjach naprawdę każdy dodany projekt znaczący ma wpływ na wydajność.

Jak widać jestem zwolennikiem tworzenia nowego projektu z testami dla każdego projektu. Uważam, że bardzo ułatwia to czytanie (testy jako dokumentacja). W In-Memory Tests można również umieścić jakiś skrypt uruchamiający wszystkie testy – taki szybki test dla programistów, którzy zmodyfikowali coś w Production Code.

Podział na foldery a nie mieszanie projektów z testami z projektami produkcyjnymi w jednym folderze, ułatwia znalezienie konkretnego testu (szczególnie nowym programistom).

Interfejsy IObservable oraz IObserver

W przyszłych postach chcę zająć się Reactive Extensions, jednak zanim zacznę cykl postów o tym, najpierw przedstawię dwa interfejsy wprowadzone w .NET 4.0. Interfejsy umożliwiają implementację wzorca obserwator. IObserver powinien zostać zaimplementowany dla klasy, która chcę być powiadamiana o zmianach dokonywanych na klasie implementującej IObservable. Przyjrzyjmy się najpierw metodom IObserver:

  1. OnCompleted – obserwacja wszelkich zmian zakończona.
  2. OnError – wystąpił błąd.
  3. OnNext – Nowa zmiana np. dodano element do kolekcji.

Z kolei IObservable wymaga wyłącznie jednej metody, Subscribe odpowiedzialnej za subskrypcję obserwatora. Napiszmy więc klasę dokonującą zmian na bazie danych:

class PersonRepository:IObservable<string>
{        
        #region IObservable<string> Members

        private List<IObserver<string>> _observers = new List<IObserver<string>>();

        public IDisposable Subscribe(IObserver<string> observer)
        {
            _observers.Add(observer);
            return null;
        }       

        #endregion

        public void AddPerson(string person)
        {
            // jakas akcja
            if(person==null)
                _observers.ForEach(o => o.OnError(new NullReferenceException()));
            else
                _observers.ForEach(o=>o.OnNext(person));
        }
        public void Close()
        {
            _observers.ForEach(o => o.OnCompleted());
        }
}

Jak widać IObservable jest generycznym typem. W poście używam po prostu typu string. Metoda Subscribe odpowiedzialna jest za subskrypcję obserwatorów. Warto uwagę zwrócić na IDisposable. Dzięki temu można zaimplementować zwalnianie zasobów gdy obserwator jest już nie dostępny (pomięto to w poście). Następnie dodano dwie metody AddPerson oraz Close – tylko po to aby pokazać co można zrobić na podstawie IObservable. Gdy użytkownik wykona metodę AddPerson, wszyscy obserwatorzy zostaną o tym powiadomieni. Przejdźmy więc do implementacji IObserver:

class SampleObserver:IObserver<string>
{
        #region IObserver<string> Members

        public void OnCompleted()
        {
            MessageBox.Show("Completed");
        }

        public void OnError(Exception error)
        {
            MessageBox.Show(string.Format("Error: {0}", error.Message));
        }

        public void OnNext(string value)
        {
            MessageBox.Show(string.Format("Next: {0}",value));
        }

        #endregion
}

A na zakończenie przykład wykorzystania powyższych klas

PersonRepository repository=new PersonRepository();
repository.Subscribe(new SampleObserver());            
repository.AddPerson("test");
repository.AddPerson(null);
repository.Close();

Wykonanie AddPerson lub Close spowoduje wyświetlenie MessageBox, zgodnie z tym co zostało zaimplementowane w przykładowym obserwatorze. .NET dostarcza wyłącznie interfejsy, co może wydawać się nieco niepotrzebne. Zdecydowałem się jednak na takie wprowadzanie, aby następny post (o RX) był łatwiejszy w przyswojeniu.

Jak nie używać blokad lock

Słowo lock służy do synchronizacji  kodu między wątkami. Często jednak przeglądając kod widzę niepoprawne użycie. Rozważmy następujący przykład:

public class OrderManager
{
    public void Submit()
    {
        lock(this)
        {
            //...
        }
    }
}

Przedstawiona konstrukcja może spowodować wiele trudnych do wykrycia problemów. Co jeśli użytkownik naszej biblioteki również wykorzysta źle lock i napisze:

OrderManager orderManager=new OrderManager();
//...
lock(orderManager)
{
    orderManager.Submit();
}

Spowoduje to oczywiście deadlock’a. Pierwszy lock się uda (na orderManager), z kolei na drugim wykonanie się zakleszczy. Nie możemy wymagać od użytkowników naszego kodu, że wiedzą jakiego typu lock’i zaimplementowaliśmy. Musimy zawsze pisać kod, który będzie działać w jak największej liczbie przypadków użycia. Rozwiązanie jest proste:

public class OrderManager
{
    private object _sync=new object();
    
    public void Submit()
    {
        lock(_sync)
        {
            //...
        }
    }
}

Jeśli wykorzystujemy rzadko Lock ,warto dodać lazy loading dla _sync:

public class OrderManager
{
    private object _sync;
    
    public void Submit()
    {
        lock(GetSyncHandle())
        {
            //...
        }
    }
    private object GetSyncHandle()
    {
        System.Threading.Interlocked.CompareExchange(ref _sync, new object(), null);
          return _sync;
    }
}

CompareExchange gwarantuje atomowość operacji – dzięki temu wiemy, że dokładnie jedna kopia _sync zostanie utworzona.

Wielowątkowość: podstawowe pojęcia – deadlock, livelock, starvation.

O wielowątkowości pisałem już niejednokrotnie. Niestety w żadnym z moich postów, nie wyjaśniłem podstawowych pojęć związanych z współbieżnością. Oczywiście jeśli wykorzystujemy wątki do prostych zadań typu asynchroniczne połączenie z usługą, poważniejszych problemów nie doświadczymy. W przypadku jednak nieco bardziej zaawansowanych algorytmów, musimy zawsze badać nasz kod pod kątem:

1. Zakleszczenie (deadlock) – występuję gdy wątek A czeka aż wątek B skończy swoją operację a wątek B czeka aż wątek A zakończy akcję. W takiej sytuacji oczywiście algorytm nigdy nie skończy operacji, ponieważ wątki czekają na siebie nawzajem. Przykład w c#:

    private readonly object objectLockA = new object();
    private readonly object objectLockB = new object();
    
    public void MethodA()
    {
        lock(objectLockA)
        {
            lock(objectLockB)
            {        
            }
        }
    }
    
    public void MethodB()
    {
        lock(objectLockB)
        {
            lock(objectLockA)
            {
            }
        }
    }
    

Załóżmy, że MethodA oraz MethodB uruchamiane są w dwóch różnych wątkach.Ponadto przypuśćmy, że aktualnie obydwu metodom udało się wykonać pierwszy lock  – lock(objectLockA) dla metody A oraz lock(objectLockB) dla metody B. I co dalej? Oczywiście algorytm się zakleszczy ponieważ w przypadku metody A nie można będzie uzyskać blokady do objectLockB (została ona już przyznana metodzie B) a w przypadku metody B sytuacja wygląda analogicznie.

2. Zagłodzenie (starvation). Mamy kilka wątków i jeśli algorytm lub architektura pozwala na to, że jeden proces nigdy nie będzie miał szansy wykonania się, wtedy mówimy o zagłodzeniu procesu. W przypadku architektury systemu, powinna ona dopuścić do wykonania również wątki o najniższym priorytecie. Jeśli algorytm szeregowania procesów jest zły, wtedy istnieje szansa, że wątek zostanie zagłodzony, ponieważ zawsze będą wykonywane inne procesy np. o wyższym priorytecie. Problem również można rozwiązać na poziomie algorytmu. Można przecież tak zaprojektować algorytm,  że będzie on dopuszczał inne wątki do wykonania (np. poprzez wykonanie funkcji Sleep w określonych momentach). Sleep jednak jest najbardziej prymitywną metodą unikania zagłodzenia i powinno się problem rozwiązać poprzez poprawny przepływ między wątkami .

3. Livelock – stanowi specjalny przypadek zagłodzenia. Występuje gdy obydwa procesy aby uniknąć deadlock’a zatrzymują wykonywanie kodu aby dać szansę innym wątkom na wykonanie się. Aby ułatwić zrozumienie tego, wyobraźmy sobie sytuację gdy dwie osoby na korytarzu aby minąć siebie wybierają ciągle tą samą trasę, kończąc na ciągłej wzajemnej blokadzie. Livelock może wydawać się podobny do deadlock (rezultat jest taki sam). W przypadku Livelock stan procesu się jednak zmienia. Z kolei w deadlock, pozostaje ciągle taki sam. W przykładzie dwóch osób na korytarzu można zauważyć, że ciągle zmieniają swoją pozycje (lewo, prawo). Analizując deadlock i powyższy fragment kodu c#  można zauważyć, że stan pozostaje taki sam -  nic się nie dzieje, po prostu czekamy na akcję, nie zmieniając swojego stanu. Livelock jest częstym problemem w niepoprawnych algorytmach wykrywania i usuwania deadlock.

WPF: TextBlock vs. Label

Na pierwszy rzut oka, TextBlock oraz Label są bardzo podobne. Obydwie kontrolki(?)  wyświetlają tekst. Różnice są jednak spore i postaram się je wyjaśnić. Sprawdźmy co następujący kod wyświetli:

<StackPanel>
   <Label>Label</Label>
   <TextBlock>TextBlock</TextBlock>
</StackPanel>

image

Kontrolka Label jest lekko przesunięta w prawo. Jest to spowodowane faktem, że Padding dla Label domyślnie ma wartość 5. W przypadku TextBlock jest to 0, dlatego też na powyższym screenie TextBlock przylega do krawędzi.

Ustawmy IsEnabled StackPanel’a na false i sprawdźmy jak zachowają się kontrolki:

<StackPanel IsEnabled="False">
   <Label>Label</Label>
   <TextBlock>TextBlock</TextBlock>
</StackPanel>

image

Label zachował się jak zwykła kontrolka, kolor został ustawiony na szary. W przypadku TextBlock nic nie zostało zmienione.

Kolejną zaletą Label jest wsparcie dla tzw. Access Keys. Dzięki nim użytkownik naciskając Alt oraz specjalnie oznaczoną literę z  Label Content, może przejść do sąsiadującego TextBox np.

<StackPanel>
   <Label Target="{Binding ElementName=textBox}">La_bel</Label>
   <TextBox x:Name="textBox"></TextBox>

   <TextBox></TextBox>
   <TextBox></TextBox>
</StackPanel>

Jeśli użytkownik naciśnie Alt + b, focus zostanie ustawiony na TextBox (textBox). Literę należy poprzedzić znakiem _.

Teraz przyszedł czas na wady Label. Jeśli zajrzymy na MSDN to dowiemy się, że TextBlock nie jest tak naprawdę kontrolką ponieważ dziedziczy z FrameworkElement. Label z kolei dziedziczy z ContentControl (dlatego zawiera właściwość Content). Z tego powodu Label jest znacznie cięższą kontrolką i bardziej obciążającą zasoby systemu. Stosujmy zatem Label w przypadku formularzy jako etykieta pól edycyjnych. Jeśli chcemy wyświetlić większy fragment tekstu, wtedy lepszym rozwiązaniem jest TextBlock. Warto również zwrócić uwagę na to, że TextBlock jest wykorzystywany jako składowa różnych kontrolek np. Button. Przeładowując więc globalnie style dla Textblock, zmienimy również wygląd m.in. button’ów.

PropertyChanged bez przekazywania string’a

Często musimy implementować interfejs INotifyPropertyChanged. Szczególnie w przypadku wykorzystania wzorca MVVM oraz WPF. Najczęściej programiści wykorzystują podstawową implementację i przekazują nazwę parametru jako czysty string np:

if(PropertyChanged!=null)
{
    PropertyChanged(this,new PropertyChangedEventArgs("propName"));
}

Wszystko działa bardzo dobrze, dopóki nie zmienimy nazwy właściwości. Przede wszystkim należy wtedy pamiętać o zaktualizowaniu wszystkich wywołań ProperyChanged. Nie możemy ponadto skorzystać ze standardowego narzędzia do refaktoryzacji dostarczonego przez Visual Studio – string nie zostanie zmieniony. Istnieje jednak bardzo proste rozwiązanie, polegające na wykorzystaniu wyrażeń Lambda. Najlepiej rozważmy od razu fragment kodu:

public static class INotifyPropertyChangedExtensions
{
   public static void Raise<T>(this PropertyChangedEventHandler handler,object sender, Expression<Func<T>> expression)
   {
       if (handler != null)
       {
           var body = propertyExpression.Body as MemberExpression;
           if (body == null)
               throw new ArgumentException("'expression' should be a member expression");

           var expression = body.Expression as ConstantExpression;
           if (expression == null)
               throw new ArgumentException("'expression' body should be a constant expression");
   
           var e = new PropertyChangedEventArgs(body.Member.Name);
           handler(sender, e);
       }
   }      
}

Powyższa klasa to rozszerzenie do ProperyChangedEventHandler. Sprawdza ona czy jest podpięte jakieś zdarzenie, jeśli tak to odpala je przekazując dynamicznie skonstruowany PropertyChangedEventArgs. Teraz zamiast przekazywać nazwę właściwości w formie stringu, przekazujemy po prostu samą właściwość:

PropertyChanged.Raise(this,()=>this.Price);

WPF, dynamiczne i statyczne menu

Ostatnio napotkałem problem stworzenia menu zawierającego zarówno dynamiczne elementy (binding) jak i statyczne. Okazało się, że wcale nie jest to takie proste jak to jest w przypadku większości rzeczy w WPF. Zacznijmy jednak od przedstawienia sposobów tworzenia menu w WPF. Pierwszy to oczywiste statyczne menu, w całości zdefiniowane w XAML:

<Menu Height="23" Name="menu1" Width="200">
    <MenuItem Header="Plik">
        <MenuItem Header="statyczny tekst 1"/>
        <MenuItem Header="statyczny tekst 2"/>
        <MenuItem Header="statyczny tekst 3"/>
    </MenuItem>
</Menu>

W powyższym kodzie nie ma nic nadzwyczajnego. Sytuacja jest również łatwa gdy chcemy w całości zdefiniować menu jako dynamiczne:

<Menu Height="23" Name="menu1" Width="200">
   <MenuItem Header="Plik" ItemsSource="{Binding Items}">        
       <MenuItem.ItemTemplate>
           <DataTemplate>
               <TextBlock Text="{Binding DisplayName}" />
           </DataTemplate>
       </MenuItem.ItemTemplate>
       <MenuItem.ItemContainerStyle>
           <Style TargetType="MenuItem">
               <Setter Property="Command" Value="{Binding ShowCmd}" />
           </Style>
       </MenuItem.ItemContainerStyle>
   </MenuItem>
</Menu>

Gdzie pojedynczy item to:

public class Item
{
   public string DisplayName { get; set; }
   public ICommand ShowCmd { get; set; }
}

Co jednak w przypadku gdy chcemy aby menu Plik zawierało część statycznych podmenu zdefiniowanych w XAML a część pochodziła z kolekcji (na dodatek z różnych kolekcji)? Sytuacja się trochę komplikuje ale możemy rozwiązań problem za pomocą CollectionContainer:

<Menu Height="23" Name="menu1" Width="200">
   <Menu.Resources>
       <CollectionViewSource x:Key="dynamicItems" Source="{Binding Items}"/>            
   </Menu.Resources>
   
   <MenuItem Header="Plik">        
       <MenuItem.ItemsSource>
           <CompositeCollection>
               <MenuItem Header="Statyczny element 1"/>
               <MenuItem Header="Statyczny element 2"/>
               <CollectionContainer Collection="{Binding Source={StaticResource dynamicItems }}"/>               
           </CompositeCollection>
       </MenuItem.ItemsSource>
   </MenuItem>
   
</Menu>

Przede wszystkim przenieśliśmy elementy do ItemsSource. CompositeCollection pozwala zdefiniować kolekcje menu składającej się z statycznych i dynamicznych elementów. Dynamiczne pozycje zdefiniowane są za pomocą CollectionContainer, który wykorzystuje jako źródło danych CollectionViewSource, który z kolei powiązany jest z Items. Po uruchomieniu powyższego przykładu zobaczymy menu składające się z różnych submenu ale niestety musimy jeszcze określić jaka właściwość powinna być wyświetlana oraz jaka komenda z klasy Item powinna zostać powiązana:

<MenuItem Header="Plik">            
  <MenuItem.Resources>
      <Style TargetType="{x:Type MenuItem}">
          <Style.Triggers>
              <DataTrigger Binding="{Binding Converter={StaticResource objectToTypeConverter}}" Value="{x:Type itemsNamespace:Item }">
                  <Setter Property="Header" Value="{Binding DisplayName}"/>
                  <Setter Property="Command" Value="{Binding ShowCmd}"/>
              </DataTrigger>
          </Style.Triggers>
      </Style>
      <CollectionViewSource x:Key="dynamicItems" Source="{Binding Items}"/>
  </MenuItem.Resources>
  <MenuItem.ItemsSource>
      <CompositeCollection>                    
          <CollectionContainer Collection="{Binding Source={StaticResource dynamicItems }}"/>
          <MenuItem Header="Statyczny element 1"/>
          <MenuItem Header="Statyczny element 2"/>
      </CompositeCollection>
  </MenuItem.ItemsSource>
</MenuItem>

Za pomocą DataTrigger sprawdzamy czy typ DataContext jest równy typeof(Item). Jeśli tak to znaczy, że jest to element dynamiczny powiązany z kolekcją Items i możemy ustawić dowolne właściwości(w tym komendę i header). Na zakończenie konwerter objectToTypeConverter:

public class ObjectToTypeConverter:IValueConverter
{
   #region IValueConverter Members

   public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
   {
       if (value == null)
           return null;
       else
           return value.GetType();
   }

   public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
   {
       throw new NotImplementedException();
   }

   #endregion
}

.NET Framework 4 Client Profile vs .NET Framework 4

Dzisiaj króciutki post. Wchodząc we właściwości projektu jako Target Platform można zauważyć .NET Framework 4 Client profile oraz .NET Framework 4. Jaka jest różnica?

Client profile to taka mocno uproszczona wersja .NET framework przeznaczona dla aplikacji klienckich. Dzięki temu użytkownik nie musi instalować pełnego .NET Framework a uproszczony i mniej ważący Microsoft .NET Framework 4 Client Profile. Wersja kliencka zawiera między innymi wsparcie dla WPF, WCF, WWF, Entity Framework, Linq To SQL, MEF. Nie zawiera jednak żadnego wsparcia dla ASP.NET oraz bardziej zaawansowanych funkcji WCF.

Jeśli wykorzystujemy do instalacji Click-Once, Client Profile z pewnością będzie doskonałym wyborem – rozmiar paczki instalacyjnej jest mniejszy niż w przypadku instalacji pełnego framework’a.

Code Contracts–programowanie defensywne

Code Contracts stanowią kolejny mechanizm ułatwiający programowanie defensywne – sposób wytwarzania oprogramowania odporny na wszelkie niespodziewane wartości (NULL, dzielenie przez zero, wartości skrajne itd.) Rozważmy klasyczny przykład – funkcja dzielenia:

private float Divide(float dividend, float divisor)
{
  if (divisor == 0)
      throw new DivideByZeroException();
  return dividend / divisor;
}

Funkcja jest zaimplementowana poprawnie – sprawdza czy divisor nie jest zerem. Są jeszcze inne skrajne przypadki jak float.NaN lub float.Inf ale ograniczmy się chwilowo tylko do dzielenia przez zero. Code Contracts umożliwia zdefiniowanie warunków wejściowych, wyjściowych oraz tzw. invariant w prostszy sposób niż zwykła walidacja za pomocą instrukcji IF. Od wersji Visual Studio 2010, CS zostały wbudowane w .NET, wcześniej należało instalować osobną paczkę. Zostały one opracowane w Microsoft Research (patrz Spec#). Z użyciem Code Contracts powyższa funkcja wygląda następująco:

private float Divide(float dividend, float divisor)
{
  Contract.Requires(divisor != 0);

  return dividend / divisor;
}

Składnia trochę prostsza ale to chyba niewystarczający argument aby przekonać się do Code Contracts. Osobiście w Code Contracts (CS) lubię statyczne sprawdzanie warunków. Niestety trzeba ściągnąć osobną instalkę ale warto…  Po instalacji otwórzmy właściwości projektu i przejdźmy do zakładki Code Contracts:

1

 

W opcjach m.in. możemy ustawić sprawdzanie kontraktów runtime i static. Uruchomienie aplikacji z divisor równym zero spowoduje wyrzucenie wyjątku przez kontrakt:

image

 

Możemy ustawić np. pełne sprawdzanie kontraktów dla wersji DEBUG oraz pominięcie dla wersji RELEASE (wydajność). Statyczne sprawdzanie działa w tle i wszelkie wykryte błędy zostaną pokazane w formie warning:

2

Sprawdźmy jak zadziała statyczne sprawdzanie dla poniższego kodu:

Random random = new Random();
Divide(3, randim.Next());

Oczywiście divosor w tym przypadku może zostań wylosowany zero. Podobna sytuacja przytrafi się gdy użytkownik przekazuje tą wartość za pomocą UI. Statyczne sprawdzanie wyświetli komunikat “CodeContracts: requires unproven: divisor !=0”:

3

Dzięki poprawnemu zdefiniowaniu CS możemy uniknąć trudnych do zidentyfikowania błędów. Nie musimy w końcu pisać kodu, który obsługuje wszystkie zakresy parametrów. Jeśli wiemy, że nasza funkcja nigdy nie będzie wywołana np. z wartością NULL to po co implementować stosowną obsługę? Wystarczy zdefiniować kontrakt a następnie statyczne lub dynamiczne sprawdzanie zidentyfikuje czy aktualny kod pozwoli na złamanie kontraktu. Nie musimy pisać wszechstronnych funkcji, za to musimy być świadomi problemów z tym związanych i jasno sprecyzować nasze założenia (Requieres, Assert, Assume) na podstawie których projektowaliśmy funkcję.

Co do warunków wstępnych warto również wspomnieć, że można nakazać wyrzucić stosowny wyjątek w przypadku złamania kontraktu:

  Contract.Requires<DivideByZeroException>(divisor != 0);

Kolejną kwestią są warunki wyjściowe, które sprawdzane są po zakończeniu działania funkcji. Definiuje się je jednak na początku ciała metody. Zobaczmy to na przykładzie:

private int Add(int numberA,int numberB)
{
  Contract.Ensures(numberA > 5);
  Contract.Ensures(Contract.OldValue(Name) == Name);
  Contract.Ensures(Contract.Result<int>() == numberA+numberB);
  Name+="test";

  return numberA + numberB;
}

Pierwszy warunek gwarantuje, że numberA po zakończeniu funkcji będzie większy od 5. Kolejny gwarantuje, że pole Name nie zostanie zmienione podczas wykonywania funkcji Add. Ostatni, że wynik funkcji będzie wynosił numberA+numberB. Podobnie jak wartość zwracana (Result) można walidować parametry wyjściowe (out,ref) za pomocą Contract.ValueAtReturn. Analogicznie do Contract.Requires można również wyrzucić konkretny wyjątek w przypadku złamania kontraktu:

Contract.EnsuresOnThrow<InvalidOperationException>(numberA > 5);

Invariant stanowią warunki, które sprawdzane są po zakończeniu każdej metody publicznej. Możemy za pomocą invariant zdefiniować warunki, które powinny być prawdziwe zawsze, niezależnie od tego, która aktualnie metoda jest wykonywana:

[ContractInvariantMethod]
private void Invariant () 
{
    Contract.Invariant ( this.Width >= 0 );
    Contract.Invariant ( this.Height >=500);
}

Jak widać invariant definiuje się za pomocą osobnej funkcji ze specjalnym atrybutem. Powyższej metody nie musimy nigdzie wywoływać – za to jest odpowiedzialny CS.

Na zakończenie wypada wspomnieć również o Contract.Assert oraz Contract.Assume. Contract.Assert jest bardzo podobne do Debug.Assert. Umożliwia po prostu sprawdzanie warunków w określonych momentach kodu np:

Random random = new Random();
int divisor = random.Next();

Contract.Assert(divisor != 0);
Divide(10, divisor);
//........
private float Divide(float dividend, float divisor)
{
  Contract.Requires(divisor != 0);

  return dividend / divisor;
}

W powyższym przykładzie za pomocą asercji sprawdzamy czy divisor jest różny od zera. Static checker od razu zwróci uwagi ponieważ nie można zagwarantować powyższej asercji – co w przypadku gdy Next zwróci 0? Metoda Divide również zwróci uwagę ze względu na Contract.Requires

Assume jest funkcją bardzo podobną a w przypadku runtime checking identyczną. Różnica istnieje tylko w statycznym sprawdzaniu. Załóżmy, że  ze nasz generator Random zawsze zwraca wartości > 0 i możemy przyjąć takie założenie. W takim przypadku Divide i kontrakt Requires nie powinien zwracać uwagi. Za pomocą Assume możemy zdefiniować nasze założenie:

Random random = new Random();
int divisor = random.Next();

Contract.Assume(divisor != 0);
Divide(10, divisor);

Powyższy kod nie zwróci żadnej uwagi. Oczywiście jak wspomniałem, w przypadku Runtime Assume działa jak Assert i nasze założenia zostaną i tak zweryfikowane. Różnica jest tylko na etapie Design time. Może lepszym przykładem byłoby użycie coś w stylu FetchData niż Random. Bardzo prawdopodobne, że FetchData zawsze zwróci wartość różną od 0 (w bazie mamy np. same dodatnie wartości) i wtedy warto zdefiniować assumption aby nie łamać wszystkich następnych kontraktów w kodzie. Assume po prostu na etapie design time dodaje pewny warunek do kolekcji faktów.