Niejednokrotnie musimy przeczytać jakieś dane “skądś”, i nie mam na myśli bazy danych. Czy to zewnętrzny system przysyła excela, czy na dysku ląduje plik tekstowy, czy jeszcze coś innego. Takie pliki musimy przetworzyć, więc prawdopodobnie utworzymy klasę “XFileProcessor” albo “XFileParser”, czy coś w ten deseń.
To jest akurat idealny scenariusz do pisania testów: dostajemy input i znamy oczekiwany output. Klient przysyła plik wejściowy i mówi, jakie dane powinniśmy z niego uzyskać. Jazda!
Więc jak takie coś testować? Pisząc testy oczywiście, ale… jak te dane testowe przekazać do testów? Można je trzymać na dysku w znanej lokalizacji i w testach je z dysku czytać. Można je dodać do projektu, zaznaczyć property pliku “Copy to output directory” na “Always copy” i… też czytać z dysku.
To drugie rozwiązanie stosowałem przez długi czas, ale jeden czy dwa projekty temu znalazłem lepsze rozwiązanie. Kopiowanie plików w projekcie testowym na dysku z jednego miejsca w inne miejsce nie jest złe. Ale czasami się nie sprawdza. Na przykład gdy używamy NCrunch: chyba to właśnie on sprawił mi trochę problemów, przez co zacząłem szukać alternatywy. NCrunch kopiuje do swojego “workspace”, w którym uruchamia testy, pliki oznaczone jako “do skopiowania” nie we właściwościach pliku w VS, ale w konfiguracji NCrunch (jednak ponownie: ręki sobie za to uciąć nie dam).
Ale to nieważne.
Tak czy siak wygodniejsze okazało się posiadanie dllki z testami, która nie potrzebuje żadnej specjalnej struktury katalogów i plików z danymi w nich umieszczonych. Zamiast tego oznaczam takie pliki jak “embedded resource” (w “build action”) i są one “wkompilowywane” w dllkę. Mechanizm jest oczywiście stary jak świat, jednak z jakichś powodów wcześniej o tym nie pomyślałem.
Dobieranie się do tych zasobów często stanowiło trochę problem, ponieważ nazwą zasobu jest “default namespace” projektu plus cała struktura katalogów aż do docelowego pliku. A na koniec nazwa tegoż. Utworzyłem więc sobie helper wykorzystywany w testach, który otworzy mi stream zasobu i wystarczy mu tylko nazwa pliku, bez całej struktury katalogów:
public static class EmbeddedData { public static string AsString(string name) { var stream = AsStream(name); using (var reader = new StreamReader(stream)) { return reader.ReadToEnd(); } } public static Stream AsStream(string name) { var callingAssembly = Assembly.GetCallingAssembly(); var resources = callingAssembly.GetManifestResourceNames() .Where(x => x.EndsWith(name)) .ToList(); if (resources.Count == 0) { throw new ArgumentException(string.Format("Embedded resource with name '{0}' cannot be found in assembly '{1}", name, callingAssembly.FullName)); } if (resources.Count > 1) { throw new ArgumentException(string.Format("Multiple resource with name '{0}' found in assembly '{1}", name, callingAssembly.FullName)); } string fullName = resources[0]; return callingAssembly .GetManifestResourceStream(fullName); } }
Żadne to rocket science, ale przydało mi się już kilkukrotnie.
Pliki z danymi testowymi | Maciej Aniserowicz o programowaniu…
Dziękujemy za dodanie artykułu – Trackback z dotnetomaniak.pl…
Myślę, że ukradnę ten kawałek kodu i przerobię testy :)
A jak wyglądają skutki np. nie zamiknięcia streama do pliku? Przy zwykłych plikach któryś test wyrzuci “file is used by another process”.
Paweł,
Nawet nie wiem bo korzystam chyba tylko z AsString() :) podejrzewam że nic się nie stanie
ale nie ma to znaczenia, jak w teście będzie wyjątek to się zamknie streama i po kłopocie
Dokładnie tego samego sposobu używam i go propaguje “u siebie”, tylko ja poszedłem krok dalej, bo jestem może bardziej leniwy ;)
Używam strongly-typed resourców do tego, więc do zawartości takiego pliku odwołuje się poprzez [NamespaceDoAssembly].Properties.[NazwaPlikuResource].[NazwaResourca] i tam siedzi już gotowy string z danymi z tego dołączonego pliku. Gorzej jeśli to dane binarne, ale takiego przypadku jeszcze nie miałem, być moze da się ustawić w VS, żeby dostać byte[] albo stream zamiast stringa. Dzięki temu nie trzeba się babrać z nazwami resourca pisanymi stringiem i jedna szansa na literówkę mniej.
Też używam tego sposobu nagminnie od lat. Dotatkowo zrobiłem nawet specjalny typ akcji w projekcie “GzipResource”, dzięki czemu wielomegabajtowe pliki konfiguracyno/danowe dość drastycznie zmniejszają swoją objętość. A w projekcie do obsługi primitywna jak makro klasa ResourceLoaderStream, która dziedziczy po GzipStream, i składa się tylko z konstruktora przyjmującego relatywną nazwę pliku z projektu. Wystarczy podoklejać domyślny namespace, rozszerzenie “.gz”, zamienić backslashe na kropki i voila. Nazwa zasobu jak malowana. Dzięki temu 40MB zasobów daje ostatecznie 1.3MB dll. A dodatkowo wszelkie dane są ukryte przed wścibskim podglądem.