Unity 3d – wprowadzenie

Dzisiaj zaczynam nowy cykl wpisów. Pamiętam jak po zaprzestaniu wspierania XNA szukałem nowego framework’a do tworzenia wizualizacji czy gier.

Kilka lat temu może nie było to jeszcze tak oczywiste, ale dzisiaj Unity3d jest pewnego typu standardem dla prostych gier. Unity3d to nie framework, ale cały silnik 2d\3d wraz z zestawem narzędzi. Istnieje kilka typów licencji, ale będę zajmował się wyłącznie unity personal, którego można ściągnąć z stąd.

Jedną z ważniejszych cech Unity to wieloplatformowość. Ten sam kod będzie działał zarówno na Windows, Linux, przeglądarce, iOS jak i na wielu innych platformach.

Na blogu będę oczywiście korzystać z C#, ale Unity wspiera jeszcze skrypty JavaScript. To jest blog programistyczny, dlatego większość postów będzie tego właśnie dotyczyła. Z tego względu, dużo uwagi poświęcę również testom jednostkowym. Przeglądając wiele tutorial’ow, zauważyłem, że ludzie często pomijają testy jak i separacje logiki od warstwy prezentacji. Myślę, że przez to, że wiele rzeczy można zrobić z edytora to bardzo łatwo popełnij błędy związane z zasadami S.O.L.I.D.

Jak wspomniałem, Unity 3d to nie pojedyncza biblioteka a zestaw narzędzi. Głównym elementem jest zatem edytor, od którego wszystko zaczyna się. Po instalacji unity z powyższego linka zobaczymy następujący edytor:
1

Na ekranie widzimy wiele paneli. Najprostszym sposobem jest po prostu spędzenie trochę i czasu  przetestowanie wszystkich menu. Najważniejsze elementy to “Scene”, “Game” oraz “Inspector”.

W pierwszym oknie, jak nie trudno domyślić się, mamy naszą “scenę” czyli świat, który budujemy. Na początku poruszanie się po scenie będzie kłopotliwe, a w raz z czasem, można się przyzwyczaić. W lewym górnym oknie do dyspozycji mamy następujący zestaw przycisków:
2

Bardzo często się z nich korzysta i dlatego warto zapamiętać skróty klawiszowe Q, W, E, R, T.

Pierwszy przycisk (Q), służy do nawigacji. Naciskając lewy przycisk na scenie będziemy mogli poruszać się po niej. Z kolei prawy przycisk myszy albo klawisz ‘ALT’ służy do obracania się. Tutaj warto również zwrócić uwagę na osie w prawym górnym rogu:
3

Pokazują one pozycje i orientację kamery na scenie.

Pozostałe przyciski (W, E, R, T) służą do zmiany pozycji, rotacji oraz skalowania zaznaczonego obiektu:
4

Kolejny ważny panel to “Game”. W tym panelu, nasza gra będzie renderowana. Gdy chcemy ją przetestować, wystarczy nacisnąć przycisk “Play” znajdujący się na górze:

5

Warto wspomnieć, że w dowolnym momencie możemy zatrzymać grę i zmienić parametry takie jak np. rotacja elementu. Za pomocą tego, w łatwy sposób na bieżącą możemy dostosowywać różne parametry do naszych potrzeb.

Wiem, że dzisiaj nie było programowania, ale w następnym wpisie zajmiemy się już Unity Scripting w C#.

Typ Geography w Dapper

W poprzednim wpisie zajęliśmy się typem Geography, który jest przydatny na operacjach związanych z współrzędnymi geograficznymi. Jak wiemy, zapytania SQL wyglądają dość skomplikowanie, tzn.:

INSERT INTO Locations (Location)  
VALUES (geography::STGeomFromText('POLYGON((-122.358 47.653 , -122.348 47.649, -122.348 47.658, -122.358 47.658, -122.358 47.653))', 4326)); 

Na szczęście Dapper ma wsparcie dla Geography i nie trzeba samemu tworzyć powyższych zapytań. Przede wszystkim należy skorzystać z klasy SqlGeography, która znajduje się w Microsoft.SqlServer.Types.dll. Jeśli ta biblioteka nie jest dołączona do projektu, należy dodać odpowiednią referencję:
1

Następnie, dodanie nowej wartości wygląda następująco:

            using (var connection = new SqlConnection(connectionString))
            {
                var geography = SqlGeography.STLineFromText(
                    new SqlChars(new SqlString("LINESTRING(-122.360 47.656, -122.343 47.656 )")), 4326);

                var pars = new { geo = geography };

                connection.Execute("INSERT INTO [Locations] (Location) values (@geo)", pars);
            }

Jak widać, Dapper rozpozna typ SqlGeography i wygeneruje odpowiedni kod SQL. Nie musimy ręcznie wstrzykiwać żadnego zapytania i korzystamy z parametrów tak jak to ma miejsce z innymi typami.

SQL Server – typ geography

W ostatnim w poście pokazałem jak odczytać współrzędne geograficzne. Kolejne możliwe pytanie, to jak je przechowywać w bazie danych. Najprostsze podejście to stworzenie dwóch kolumn typu float, dla szerokości i długości geograficznej.

W SQL Server istnieje jednak lepsze rozwiązanie, a mianowicie typ “geography”. Służy on do przechowywania lokalizacji i nie koniecznie  wyłącznie pojedynczego punktu. Możliwe jest zapisywanie całych poligonów. Utworzenie tabeli z taką kolumną, nie różni się niczym od pozostałych typów:

CREATE TABLE Locations   
    ( Id int IDENTITY (1,1),  
    Location geography );  
GO  

Następnie dodanie lokalizacji, opisującej poligon wygląda następująco:

INSERT INTO Locations (Location)  
VALUES (geography::STGeomFromText('POLYGON((-122.358 47.653 , -122.348 47.649, -122.348 47.658, -122.358 47.658, -122.358 47.653))', 4326));  

W celu dodania lokalizacji w formie pojedynczego punktu, należy stworzyć obiekt POINT zamiast powyższego POLYGON:

INSERT INTO Locations (Location)  
VALUES (geography::STGeomFromText('geography::Point(47.65100, -122.34900, 4326))', 4326));

Zaletą korzystania z geography jest, że wszelkie operacje takie jak znajdowanie odległości między dwoma punktami, są już zaimplementowane. Jeśli chcemy znaleźć części wspólne dwóch lokalizacji wtedy:

DECLARE @g geometry;  
DECLARE @h geometry;  
SET @g = geometry::STGeomFromText('POLYGON((0 0, 0 2, 2 2, 2 0, 0 0))', 0);  
SET @h = geometry::STGeomFromText('POLYGON((1 1, 3 1, 3 3, 1 3, 1 1))', 0);  
SELECT @g.STIntersection(@h).ToString();  

Zwracanie odległości między punktami wygląda następująco:

DECLARE @g geography;  
DECLARE @h geography;  
SET @g = geography::STGeomFromText('LINESTRING(-122.360 47.656, -122.343 47.656)', 4326);  
SET @h = geography::STGeomFromText('POINT(-122.34900 47.65100)', 4326);  
SELECT @g.STDistance(@h);  

Ponadto, SQL Server management studio, ma pewne wsparcie dla typu geography. Wykonując prosty SELECT zobaczymy wyłącznie binarne wartości:
1

Przechodząc jednak do zakładki “Spatial Results”, zobaczymy całą mapkę:
2

W przyszłym wpisie, pokażę jak korzystać z tego typu w C# (a konkretnie w Dapper).

Odczytywanie współrzędnych geograficznych

Ostatnio musiałem odczytać współrzędne geograficzne na podstawie nazwy lokalizacji.

Pierwszą opcją, którą sprawdziłem był pakiet “GoogleMaps.LocationServices”. Po instalacji NuGet, odczytanie współrzędnych było bardzo proste:

            var gls = new GoogleLocationService();
            var location = gls.GetLatLongFromAddress("Warsaw, Poland");

            Console.WriteLine("{0}, {1}", location.Latitude,location.Longitude);

Wynik:
1

Rozwiązanie bardzo proste. Biblioteka korzysta z Google Maps, ale niestety ma limity na liczbę zapytań w ciągu dnia. Co gorsza, nie istnieje możliwość zwiększenia tego limitu.

Z tego względu zdecydowałem się na bezpośrednie połączenie z Google API, a konkretnie z Geocoding API. Co prawda, rozwiązanie też ma limity dzienne, ale po darmowej rejestracji uzyskamy limit 100k dziennie, co w moim w wypadku było wystarczające. API jest w formie REST:
“https://maps.googleapis.com/maps/api/geocode/json?address=Warsaw,Poland&key=YOUR_API_KEY”

Oczywiście należy najpierw uzyskać klucz, rejestrując konto na Console Google. Przykładowa odpowiedź przychodzi w następującym formacie:

{
   "results" : [
      {
         "address_components" : [
            {
               "long_name" : "Warsaw",
               "short_name" : "Warsaw",
               "types" : [ "locality", "political" ]
            },
            {
               "long_name" : "Warszawa",
               "short_name" : "Warszawa",
               "types" : [ "administrative_area_level_3", "political" ]
            },
            {
               "long_name" : "Warszawa",
               "short_name" : "Warszawa",
               "types" : [ "administrative_area_level_2", "political" ]
            },
            {
               "long_name" : "Masovian Voivodeship",
               "short_name" : "Masovian Voivodeship",
               "types" : [ "administrative_area_level_1", "political" ]
            },
            {
               "long_name" : "Poland",
               "short_name" : "PL",
               "types" : [ "country", "political" ]
            }
         ],
         "formatted_address" : "Warsaw, Poland",
         "geometry" : {
            "bounds" : {
               "northeast" : {
                  "lat" : 52.3679992,
                  "lng" : 21.2710983
               },
               "southwest" : {
                  "lat" : 52.0978767,
                  "lng" : 20.8512898
               }
            },
            "location" : {
               "lat" : 52.2296756,
               "lng" : 21.0122287
            },
            "location_type" : "APPROXIMATE",
            "viewport" : {
               "northeast" : {
                  "lat" : 52.3679992,
                  "lng" : 21.2710983
               },
               "southwest" : {
                  "lat" : 52.0978767,
                  "lng" : 20.8512898
               }
            }
         },
         "place_id" : "ChIJAZ-GmmbMHkcR_NPqiCq-8HI",
         "types" : [ "locality", "political" ]
      }
   ],
   "status" : "OK"
}

Generując automatycznie klasy C# w Visual Studio na podstawie JSON uzyskamy:

  public class Rootobject
    {
        public Result[] results { get; set; }
        public string status { get; set; }
    }

    public class Result
    {
        public Address_Components[] address_components { get; set; }
        public string formatted_address { get; set; }
        public Geometry geometry { get; set; }
        public string place_id { get; set; }
        public string[] types { get; set; }
    }

    public class Geometry
    {
        public Bounds bounds { get; set; }
        public Location location { get; set; }
        public string location_type { get; set; }
        public Viewport viewport { get; set; }
    }

    public class Bounds
    {
        public Northeast northeast { get; set; }
        public Southwest southwest { get; set; }
    }

    public class Northeast
    {
        public float lat { get; set; }
        public float lng { get; set; }
    }

    public class Southwest
    {
        public float lat { get; set; }
        public float lng { get; set; }
    }

    public class Location
    {
        public float lat { get; set; }
        public float lng { get; set; }
    }

    public class Viewport
    {
        public Northeast1 northeast { get; set; }
        public Southwest1 southwest { get; set; }
    }

    public class Northeast1
    {
        public float lat { get; set; }
        public float lng { get; set; }
    }

    public class Southwest1
    {
        public float lat { get; set; }
        public float lng { get; set; }
    }

    public class Address_Components
    {
        public string long_name { get; set; }
        public string short_name { get; set; }
        public string[] types { get; set; }
    }

W przypadku usług REST, standardowo używam RestSharp+JSON.NET:

           var query = "https://maps.googleapis.com/maps/api/geocode/json?address=Warsaw,Poland&key=API_KEY";

            var client = new RestClient(query);
            var response = client.Get<dynamic>(new RestRequest());

            Rootobject data = JsonConvert.DeserializeObject<Rootobject>(response.Content);
            
            Console.WriteLine("{0}, {1}", data.results[0].geometry.location.lat, data.results[0].geometry.location.lng);

W praktyce, automatycznie wygenerowane klasy zostałyby zastąpione czytelniejszą strukturą. Tyle jednak wystarczy na szybkie sprawdzenie możliwości API.

Aktualziacja statystyk w SQL Server

O Sql Statistics pisałem już tutaj.
Wiemy, że dzięki takim statystykom dobierane są indeksy oraz plany wykonania. Na przykład jeśli zagęszczenie danych jest bardzo niskie, wtedy nawet warto zrezygnować z indeksów.

Utrzymanie statystyk nie jest łatwym zadaniem. Generalnie SQL Server jest odpowiedzialny za ich przeliczanie. Oczywiście przeliczanie od nowa statystyk za każdym razem, gdy dane są zmieniane, byłoby zbyt czasochłonne. Z tego wynika fakt, że istnieje ryzyko, że statystyki nie odzwierciedlają dokładnie danych.  SQL Server aktualizuje je w następujący sposób:

  1. Gdy nie ma żadnych danych, wtedy przeliczane są one w momencie dodania pierwszego wiersza.
  2.  Gdy jest mniej niż 500 wierszy, wtedy przeliczane są one w momencie osiągnięcia progu 500.
  3.  Gdy jest więcej niż 500 wierszy, każda zmiana 20% powoduje ponowne przeliczenie.

Jeśli mamy kilka milionów danych, przy odrobinie pecha, może zdarzyć się, że mniej niż 20% danych zostanie zmodyfikowanych, ale w taki sposób, że zaburzają one aktualnie wyliczoną dystrybucję danych.

W dowolnym momencie możemy sprawdzić, kiedy ostatnio statystyki były przeliczanie. Wystarczy użyć następującej komendy:

DBCC SHOW_STATISTICS('dbo.Articles',PRICE_INDEX)

W wyniku dostaniemy m.in. kolumnę Updated:

Można również ręcznie zaktualizować statystyki za pomocą Update Statistics:

UPDATE STATISTICS Sales.SalesOrderDetail

Możliwe jest również przeliczenie wszystkich statystyk w bazie:

EXEC sp_updatestats

Ostatnio pisałem o fragmentacji indeksów oraz potrzebie ich przebudowania. Dobrą wiadomością jest fakt, że gdy przebudowujemy indeks, również statystyki są przeliczanie. Nie ma zatem potrzeby wykonywania tych dwóch operacji jednocześnie.

Niestety nie ma łatwego sposobu na sprawdzenie, czy należy przebudować statystyki. Zwykle nie ma takiej potrzeby, ale czasami dane są modyfikowane w tabelach w taki sposób, że potrzebna jest ingerencja. Objawia się to wtedy źle dobranymi planami wykonania.

Fragmentacja indeksów

To jest nie blog o SQL Server, ale myślę, że podstawowe informacje o bazach danych są przydatne dla każdego programisty.

Wszyscy używamy indeksów i wiemy, że dzielą się one m.in. na clustered index oraz non-clustered index. Odpowiednie stworzenie ich może okazać się dość trudnym zadaniem. Niestety nasza rola nie ogranicza się wyłącznie do ich utworzenia ponieważ wcześniej czy później będziemy mieli do czynienia z fragmentacją indeksów.

Co to jest fragmentacja? W informatyce jest to znane pojęcie i oznacza, że dane nie są rozmieszczone równomiernie w pamięci. Indeks jest reprezentowany jako drzewo. Jeśli jego węzły nie są rozmieszczone równomiernie (w spójnych blokach), wtedy odczytywanie danych jest trudniejsze ponieważ musimy skakać z jednego adresu na drugi.

Tak na prawdę w bazach danych musimy być świadomi dwóch typów fragmentacji. Pierwszy z nich to fragmentacja fizyczna dysku. Jeśli dane na dysku nie są spójne, to naturalnie może zdarzyć się, że plik bazy danych również zostanie podzielony na różne bloki w pamięci. Ten typ fragmentacji nie ma nic wspólnego z SQL Server, ale jeśli ma miejsce, spowoduje spowolnienie wszystkich zapytań. Wyobraźmy sobie, że plik bazy danych jest umieszczony na tym samym dysku, gdzie umieszczone są jakieś inne pliki. Jeśli tylko jest tam dużo operacji Input\Output, wtedy fragmentacja może mieć miejsce dosyć często. Rozwiązaniem jest zwykła defragmentacja dysku wykonywana przez Windows. Dodatkowo warto umieszczać plik bazodanowy na osobnym, niezależnym dysku.

Drugi typ fragmentacji, to fragmentacja indeksu i jest ściśle związana z silnikiem baz danych. Wchodząc szczegóły, wyróżnia się fragmentację wewnętrzną oraz zewnętrzna. Jeśli usuniemy dane ze środka tabeli, wtedy pojawią się puste fragmenty na stronach pamięci. Wykonując DELETE wielokrotnie, możemy doprowadzić zatem do fragmentacji wewnętrznej, czyli pustych fragmentów (“dziur”) na stronach pamięci.

Z kolei zewnętrzna fragmentacja ma miejsce, gdy logiczna kolejność stron nie odpowiada fizycznej kolejności. Przytrafia się to, gdy wiersze są dodawane do środkowych stron, w których nie ma już miejsca. Załóżmy, że mamy strony Page1, Page2, Page3, które odpowiadają fizycznej kolejności Page1->Page2->Page3. Załóżmy, że wykonujemy INSERT, który powinien dodać dane na końcu strony Page1. Jeśli nie będzie tam wystarczającao miejsca, wtedy zostanie dodana nowa strona (nazwijmy ją Page1A) i fizyczny porządek będzie wyglądał Page1->Page2->Page3->Page1A, a logiczny z kolei Page1->Page1a->Page2->Page3. Innymi słowy, zewnętrzna fragmentacja to “dziury” pomiędzy stronami, a wewnętrzna to “dziury” w obrębie pojedynczych stron.

Wniosek taki, że indeksy trzeba przebudowywać od czasu do czasu. W  SQL Management Studio można wybrać z menu kontekstowego opcję Rebuild:

1

Na ekranie pojawi się informacja jak bardzo dany indeks jest sfragmentowany:

2

Oczywiście w praktyce automatyzuje się takie zadania. Administratorzy baz danych zwykle definiują skrypty, które wykonają się, gdy poziom fragmentacji osiągnie dany próg.

Na przykład, podany skrypt wyświetli informacje o wszystkich indeksach wraz z ich fragmentacją (źródło):

SELECT dbschemas.[name] as 'Schema', 
dbtables.[name] as 'Table', 
dbindexes.[name] as 'Index',
indexstats.avg_fragmentation_in_percent,
indexstats.page_count
FROM sys.dm_db_index_physical_stats (DB_ID(), NULL, NULL, NULL, NULL) AS indexstats
INNER JOIN sys.tables dbtables on dbtables.[object_id] = indexstats.[object_id]
INNER JOIN sys.schemas dbschemas on dbtables.[schema_id] = dbschemas.[schema_id]
INNER JOIN sys.indexes AS dbindexes ON dbindexes.[object_id] = indexstats.[object_id]
AND indexstats.index_id = dbindexes.index_id
WHERE indexstats.database_id = DB_ID()
ORDER BY indexstats.avg_fragmentation_in_percent desc

Wydajność: HashSet + struktury

Struktury mogą być doskonałym rozwiązaniem w połączeniu ze słownikiem czy HashSet. Zwykle projektuje je się w sposób immutable, więc stanowią naturalny wybór na klucze w HashSet.

Niestety bardzo łatwo popełnić błąd, który poskutkuje gwałtownym spadkiem wydajności.  Załóżmy, że mamy dwie struktury, jedna zawierająca wyłącznie typy proste (value type), a druga również typ referencyjny czyli string:

    public struct WithValueType
    {
        public int ValueType1;
        public int ValueType2;
    }

    public struct WithReferenceType
    {
        public int ValueType;
        public string RefType;
   
    }

Napiszmy teraz prosty benchmark, sprawdzający ile trwa dodanie obiektów do HashSet:

        int times = 1000000;
        
        var withValueType = new HashSet<WithValueType>();
        var withReferenceType = new HashSet<WithReferenceType>();

        // withValueType
        Stopwatch timer = Stopwatch.StartNew();

        for (int i = 0; i < times; i++)
            withValueType.Add(new WithValueType {ValueType1 = i, ValueType2 = i});

        timer.Stop();
        Console.WriteLine($"WithValueType: {timer.ElapsedMilliseconds}");

        // withReferenceType
        timer = Stopwatch.StartNew();

        for (int i = 0; i < times; i++)
            withReferenceType.Add(new WithReferenceType { ValueType = i, RefType = i.ToString() });

        timer.Stop();
        Console.WriteLine($"withReferenceType: {timer.ElapsedMilliseconds}");
      

Na ekranie zobaczymy następujące wyniki:

1

Skąd ta różnica? To już nie jest mikro-optymalizacja, ale bardzo poważny problem wydajnościowy, który ostatnio zmarnował mi sporo czasu na profilowaniu aplikacji. Wiemy, że struktury możemy porównywać poprzez wartość. Jeśli w kodzie mamy warunek np. if(a != b), wtedy konkretne wartości zostaną porównane, a nie jak w przypadku klas, adres instancji. Problem w tym, że wykorzystywany jest mechanizm reflekcji, który zawsze jest wolniejszy. Z dokumentacji MSDN wiemy, że Equals dla struktur działa następująco:

1. Jeśli struktura zawiera przynajmniej jeden typ referencyjny, wtedy jest używany mechanizm reflekcji.
2. Jeśli struktura zawiera wyłącznie typy proste (value types), wtedy dane porównywane są bajt po bajcie.

Zobaczmy implementację ValueType.Equals:

public override bool Equals (Object obj) { 
            BCLDebug.Perf(false, "ValueType::Equals is not fast.  "+this.GetType().FullName+" should override Equals(Object)"); 
            if (null==obj) {
                return false; 
            }
            RuntimeType thisType = (RuntimeType)this.GetType();
            RuntimeType thatType = (RuntimeType)obj.GetType();
 
            if (thatType!=thisType) {
                return false; 
            } 

            Object thisObj = (Object)this; 
            Object thisResult, thatResult;

            // if there are no GC references in this object we can avoid reflection
            // and do a fast memcmp 
            if (CanCompareBits(this))
                return FastEqualsCheck(thisObj, obj); 
 
            FieldInfo[] thisFields = thisType.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
 
            for (int i=0; i<thisFields.Length; i++) {
                thisResult = ((RtFieldInfo)thisFields[i]).InternalGetValue(thisObj,false);
                thatResult = ((RtFieldInfo)thisFields[i]).InternalGetValue(obj, false);
 
                if (thisResult == null) {
                    if (thatResult != null) 
                        return false; 
                }
                else 
                if (!thisResult.Equals(thatResult)) {
                    return false;
                }
            } 

            return true; 
        }

Po metodzie CanCompareBits widzimy, że szybkie sprawdzanie jest wyłącznie możliwe, gdy nie ma typów referencyjnych. Wtedy po prostu są sprawdzane wartości bajtów (szybka operacja). Problemy zaczynają się, gdy dane są przechowywane na stercie, szczególnie w różnych miejscach.

W praktyce, gdy mamy typy referencyjne, wtedy obowiązkowo implementujemy Equals. Powyższe rozważania jednak nie usprawiedliwiają faktu, że to struktura z typami prostymi, a nie referencyjnymi, stanowi największy problem. HashSet zawsze wywołuje GetHashCode. W dokumentacji przeczytamy, że:

        /*=================================GetHashCode==================================
        **Action: Our algorithm for returning the hashcode is a little bit complex.  We look
        **        for the first non-static field and get it's hashcode.  If the type has no
        **        non-static fields, we return the hashcode of the type.  We can't take the 
        **        hashcode of a static member because if that member is of the same type as
        **        the original type, we'll end up in an infinite loop. 
        **Returns: The hashcode for the type. 
        **Arguments: None.
        **Exceptions: None. 
        ==============================================================================*/
        [System.Security.SecuritySafeCritical]  // auto-generated
        [ResourceExposure(ResourceScope.None)]
        [MethodImplAttribute(MethodImplOptions.InternalCall)] 
        public extern override int GetHashCode();

Rozwiązaniem jest własna implementacja GetHashCode:

    public struct WithValueType
    {
        public int ValueType1;
        public int ValueType2;

        public override bool Equals(object obj)
        {
            return base.Equals(obj);
        }

        public bool Equals(WithValueType other)
        {
            return ValueType1 == other.ValueType1 && ValueType2 == other.ValueType2;
        }

        public override int GetHashCode()
        {
            unchecked
            {
                return (ValueType1*397) ^ ValueType2;
            }
        }
    }

    public struct WithReferenceType
    {
        public int ValueType;
        public string RefType;

        public override bool Equals(object obj)
        {
            return base.Equals(obj);
        }

        public bool Equals(WithReferenceType other)
        {
            return ValueType == other.ValueType && string.Equals(RefType, other.RefType);
        }

        public override int GetHashCode()
        {
            unchecked
            {
                return (ValueType*397) ^ (RefType != null ? RefType.GetHashCode() : 0);
            }
        }
    }

Wyniki prezentują się teraz dużo lepiej:
2

Warto również zaimplementować interfejs IEquatable, jeśli chcemy uniknąć boxingu. Więcej informacji w moim poprzednim wpisie.

ASP.NET WebAPI – Pakiet Microsoft.Owin.Testing

W poprzednim wpisie pokazałem częściowo pakiet Owin do testowania. Został on użyty do testów dostawcy. Myślę, że zasługuję on na osobny wpis, ponieważ często w testach integracyjnych czy UI jest przydatny.

Owin.Testing to nic innego jak testowy serwer WWW. Zamiast odpalać proces IIS ręcznie w SetUP, można skorzystać z Owin. Stwórzmy zatem projekt dla testów i zainstalujmy kilka pakietów:

1. nUNit
2. Microsoft.Owin.Testing
3. Microsoft.Owin.Host.HttpListener
4. Restsharp

Następnie podstawowy test wygląda następująco:

   [TestFixture]
    public class WebTests
    {
        [Test]
        public void OwinAppTest()
        {
            using (WebApp.Start<Mystartup>("http://localhost:5454"))
            {
                var httpclient = new RestClient("http://localhost:5454");
                var response =  httpclient.Get(new RestRequest("/api/values"));

                string content = response.Content;

                // assert
            }
        }
    }

Widzimy, że musimy stworzyć OWIN startup w aplikacji ASP.NET WebAPI. Tworzymy zatem plik z szablonu:
1

Domyślnie zostanie wygenerowany następująca klasa:

    public class MyStartup
    {
        public void Configuration(IAppBuilder app)
        {
            // For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=316888
        }
    }

Musimy umieścić tam kod, który zwykle zostaje wykonany zamiast Global.asax. Oznacza to, że możemy usunąć Global.asax, a następnie umieścić w MyStartup następującą konfigurację:

  public class MyStartup
    {
        public void Configuration(IAppBuilder app)
        {
            var config = new HttpConfiguration();

            WebApiConfig.Register(config);

            app.UseWebApi(config);

        }
    }

Aby powyższy kod skompilował się, należy najpierw zainstalować w projekcie WebApi pakiety “Microsoft.AspNet.WebApi.OwinSelfHost” oraz “Microsoft.Owin.Host.SystemWeb”. Pierwszy pakiet jest potrzebny, aby mogło się hostować aplikację w procesie, a nie w IIS. Powyższy test, nie będzie odpalać zatem jakiejkolwiek instancji IIS.

Z kolei drugi pakiet (SystemWeb) jest wymagany w celu odpalenia Startup, w momencie hostowania aplikacji w IIS.

Za pomocą Owin, w łatwy sposób można odpalać aplikacje self-host. Analogiczne pakiety można znaleźć m.in. dla Nancy.

CONSUMER DRIVEN CONTRACTS w PACT-NET – implementacja testów dostawcy

W poprzednim wpisie przedstawiłem framework PACT.NET. Zdołaliśmy zaimplementować testy po stronie konsumentów. Wynikiem tego, był wygenerowany plik JSON zawierający nagrane testy. Dla przypomnienia, test po stronie konsumenta wyglądał następująco:

[TestFixture]
public class ConsumerTests
{
    private IMockProviderService _mockProviderService;
    private IPactBuilder _pactProvider;
 
    [OneTimeSetUp]
    public void OneTimeSetUp()
    {
        _pactProvider = new PactBuilder()
            .ServiceConsumer("ConsumerName")
            .HasPactWith("ProviderName");
 
        _mockProviderService = _pactProvider.MockService(3423,false);
    }
 
    [OneTimeTearDown]
    public void OneTimeTearDown()
    {
        _pactProvider.Build();
    }
 
    [Test]
    public void Should_Return_FirstName()
    {
        //Arrange
        _mockProviderService.Given("there are some persons data")
            .UponReceiving("a request to retrieve all persons")
            .With(new ProviderServiceRequest
            {
                Method = HttpVerb.Get,
                Path = "/api/persons"     
            })
            .WillRespondWith(new ProviderServiceResponse
            {
                Status = 200,
                Headers = new Dictionary<string, string>
                {
                    { "Content-Type", "application/json; charset=utf-8" }
                },
                Body = new[]
                {
                    new
                    {
                        FirstName = "Piotr",
                    }
                }
            });
 
        var consumer = new PersonsApiClient("http://localhost:3423");
 
        //Act
        var persons = consumer.GetAllPersons();
 
        //Assert
        CollectionAssert.IsNotEmpty(persons);
        CollectionAssert.AllItemsAreNotNull(persons.Select(x=>x.FirstName));
 
        _mockProviderService.VerifyInteractions();
    }
}

Z kolei wygenerowany JSON:

{
  "provider": {
    "name": "ProviderName"
  },
  "consumer": {
    "name": "ConsumerName"
  },
  "interactions": [
    {
      "description": "a request to retrieve all persons",
      "provider_state": "there are some persons data",
      "request": {
        "method": "get",
        "path": "/api/persons"
      },
      "response": {
        "status": 200,
        "headers": {
          "Content-Type": "application/json; charset=utf-8"
        },
        "body": [
          {
            "FirstName": "Piotr"
          }
        ]
      }
    }
  ],
  "metadata": {
    "pactSpecificationVersion": "1.1.0"
  }
}

Przejdźmy zatem do testu po stronie dostawcy:

    [TestFixture]
    public class ProducerTests
    {
        [Test]
        public void Ensure_That_Consumer_Requirements_Are_Met()
        {
            var config = new PactVerifierConfig();
            IPactVerifier pactVerifier = new PactVerifier(() => { }, () => { }, config);

            pactVerifier
                .ProviderState(
                    "there are some persons data",
                    setUp: DataSetup,tearDown:DataTearDown);
       
            using (var testServer = TestServer.Create<Startup>())
            {
                pactVerifier
                   .ServiceProvider("Persons API", testServer.HttpClient)
                   .HonoursPactWith("Consumer")
                   .PactUri(@"consumername-providername.json")
                   .Verify();
            }
        }

        private void DataTearDown()
        {
        }


        private void DataSetup()
        {
        }

    }

W celu uruchomienia serwera, używamy Microsoft.Owin.Testing czyli klasy TestServer. W momencie odpalenia testu, zostanie zatem stworzony serwer z naszą aplikacją. Następnie “odgrywany” będzie test wygenerowany przez konsumenta. Operujemy na prawdziwych danych, a nie na mock’ach, stąd metody DataSetup oraz DataTearDown w praktyce zawierają odpowiedni kod, dodający oraz usuwający dane testowe.

Po wykonaniu testu, zostanie wygenerowany plik persons_api_verifier.log:

2016-06-01 19:54:01.894 +01:00 [Debug] Verifying a Pact between ConsumerName and ProviderName
  Given there are some persons data
    a request to retrieve all persons
      with GET /api/persons
        returns a response which
          has status code 200
          includes headers
            'Content-Type' with value application/json; charset=utf-8
          has a matching body (FAILED - 1)

Failures:

1) Expected: [
  {
    "FirstName": "Piotr"
  }
], Actual: [
  {
    "FirstName": "Piotr",
    "LastName": "Zielinski"
  },
  {
    "FirstName": "first name",
    "LastName": "last name"
  }
]

Widzimy, że test zakończył się niepowodzeniem, ponieważ oczekiwana zawartość Content jest inna niż ta zwrócona przez prawdziwy serwis.

Za pomocą PathVerifier definiujemy, które testy chcemy wykonać. W naszym przypadku jest tylko jeden. W praktyce definiuje się wiele stanów i kilka plików JSON.