GoDev

Blog programisty.

Programowanie Reaktywne - Bileciki do kontroli - Unit Tests of Create Cold/Hot Observable.


Artykuł ten jest częścią serii artykułów na temat Programowania reaktywnego.

Zapraszam na GitHub-a.

Tematy

  1. Wstęp
  2. Zabawa z czasem - Timer
  3. Kto za tym stoi? - Scheduler
  4. Nie zapominaj - Subscribe
  5. Zabawa z czasem - Interval
  6. Zabawa z czasem - Buffer
  7. Zabawa z czasem - Delay
  8. Zabawa z czasem - Sample
  9. Zabawa z czasem - Throttle
  10. Zabawa z czasem - Timestamp/TimeInterval
  11. Tworzymy dane - Generators
  12. Tworzymy dane - Własna klasa publikująca
  13. Marudzimy - Skip
  14. Marudzimy - Take
  15. Łap To! - ConsoleKey
  16. Kombinatorzy - Concat
  17. Kombinatorzy - Repeat
  18. Kombinatorzy - Start With
  19. Kombinatorzy - Ambiguous
  20. Kombinatorzy - Merge
  21. Kombinatorzy - Zip
  22. Kombinatorzy - Switch
  23. Kombinatorzy - When-And-Then
  24. Kombinatorzy - Combine Latest
  25. Transformers - Select
  26. Transformers - OfType and Cast
  27. Transformers - Metadata
  28. Bileciki do kontroli - Unit Tests of Interval
  29. Bileciki do kontroli - Unit Tests of Observer Interval
  30. Bileciki do kontroli - Unit Tests of Create Cold/Hot Observable
  31. Szpryca - AutoFac

Wstęp

Reactive Extensions - CreateHotObservable W ostatnim poście wyzwania poruszę ponownie tematykę związaną z testowanie. Dzisiaj wejdziemy jeszcze głębiej i przetestujemy dokładniej co się dzieje w trakcie odbierania danych od dystrybutora. Zapraszam do absorbowania treści.

CreateColdObservable i CreateHotObservable

Na tapetę wybrałem dzisiaj dwa sposoby tworzenia obiektów obserwowanych:

  • CreateColdObservable - dzięki tej metodzie możemy utworzyć strumień, z wykorzystaniem porcji nagrań, tak jak poprzednio w poście opisywałem użycie Recorded<Notification<…. Właśnie z wykorzystaniem tej klasy możemy przygotować historię publikowania treści przez strumień. Publikowanie jest względem dokonania zapisu na strumień,
  • CreateHotObservable - działa tak samo jak metoda wyżej, z tą różnicą, iż zaczyna od momentu utworzenia strumienia.

Rozwinięcie powiązania z tworzeniem lub zapisem, znajduje się w dalszej części postu.

W tym kontekście napisałem dwa testy. Zobaczcie jakie ładne i klarowne. Widać same dobre rzeczy. Całe przekręty, szachrajstwa zostały schowane w klasie dodatkowej: CreateObservableTestsFixture.

Pierwszy teścik weryfikuje działanie gorącego tworzenia dystrybutora.

1
2
3
4
5
6
7
8
9
10
11
[Fact]
public void return_sequence_of_notifications__when__subscribe_to_hot_observable()
{
  //arrange
  _fixture.SetSimulationDataForHotObservable();

  //act
  var result = act();

  //assert
  _fixture.assert__simulation_notification_of_hot_observable(result);

Reactive Extensions - CreateColdObservable

Jak i w poprzednich postach wykorzystałem tutaj AAA:

  • Arange - przygotowanie do testowania,
  • Act - odpalenie funkcjonalności jaką należy przetestować,
  • Assert - dowodzimy, że test przeszedł.

Drugi test bardzo podobny do pierwszego, w zasadzie różni się tylko użyciem metod do przygotowania, i weryfikacji wyników testu.

1
2
3
4
5
6
7
8
9
10
11
[Fact]
public void return_sequence_of_notifications__when__subscribe_to_cold_observable()
{
  //arrange
  _fixture.SetSimulationDataForColdObservable();

  //act
  var result = act();

  //assert
  _fixture.assert__simulation_notification_of_cold_observable(result);

To tyle o samych testach, pora zabrać się za krojenie mięcha.

W procesie przygotowania strumienia danych opartego o HotObservable należy wskazać parametr do metody wytwarzającej. Oczywiście metoda CreateHotObservable jest zawarta w klasie już poznanej: TestScheduler.

Tworzenie źródełka wykonujemy przy pomocy kreacji historii rekordów. W każdym z tworzonych rekordów Recorded określamy jakiego typu będzie powiadomienie, a następnie wypełniamy czasem w jakim ma nastąpić publikacja na strumień oraz danymi jakie mają być publikowane. Określamy także czym jest wpis:

  • dane - CreateOnNext,
  • wyjątek - CreateOnError,
  • koniec strumienia - CreateOnCompleted.
1
2
3
4
5
6
7
8
9
10
11
_sourceObservableInterval = _testScheduler.CreateHotObservable(
  new Recorded<Notification<int>>(TimeSpan.FromSeconds(second++).Ticks, Notification.CreateOnNext(value++)),
  new Recorded<Notification<int>>(TimeSpan.FromSeconds(second++).Ticks, Notification.CreateOnNext(value++)),
  new Recorded<Notification<int>>(TimeSpan.FromSeconds(second++).Ticks, Notification.CreateOnNext(value++)),
  new Recorded<Notification<int>>(TimeSpan.FromSeconds(second++).Ticks, Notification.CreateOnError<int>(new Exception())),
  new Recorded<Notification<int>>(TimeSpan.FromSeconds(second++).Ticks, Notification.CreateOnNext(value++)),
  new Recorded<Notification<int>>(TimeSpan.FromSeconds(second++).Ticks, Notification.CreateOnNext(value++)),
  new Recorded<Notification<int>>(TimeSpan.FromSeconds(second++).Ticks, Notification.CreateOnError<int>(new ArgumentException())),
  new Recorded<Notification<int>>(TimeSpan.FromSeconds(second++).Ticks, Notification.CreateOnNext(value++)),
  new Recorded<Notification<int>>(TimeSpan.FromSeconds(second++).Ticks, Notification.CreateOnNext(value++)),
  new Recorded<Notification<int>>(TimeSpan.FromSeconds(second++).Ticks, Notification.CreateOnCompleted<int>())

Dla poszczególnych metod należy dodatkowo podać wymagane parametry np: wyjątek jaki ma zostać rzucony w danym miejscu. Doskonale tutaj widać potencjał. Skoro możemy konkretnie przygotować sekwencję występujących po sobie publikacji danych na strumień, to wówczas możemy także to zweryfikować.

Drugi przykład jest bardzo podobny, dotyczy jednak CreateColdObservable

1
2
3
_sourceObservableInterval = _testScheduler.CreateColdObservable(
  new Recorded<Notification<int>>(TimeSpan.FromSeconds(second++).Ticks, Notification.CreateOnNext(value++)),
  new Recorded<Notification<int>>(TimeSpan.FromSeconds(second++).Ticks, Notification.CreateOnNext(value++)),

Metoda act będzie bardzo podobna do tej z poprzedniego postu. Odpalamy funkcjonowanie strumienia, i określamy w jakich kryteriach będzie wykonywana dystrybucja danych.

Metoda korzysta z wcześniej przygotowanego strumienia: _sourceObservableInterval.

1
2
3
4
5
6
7
8
9
public IList<Recorded<Notification<int>>> act()
{
  var notificationRecorded = _testScheduler.Start(() => _sourceObservableInterval,
    0,
    0,
    TimeSpan.FromSeconds(_disposed).Ticks);

  return notificationRecorded.Messages;
}

Reactive Extensions - TestScheduler

Dla obu przypadków będzie to to samo działanie.

Pora zweryfikować działanie. Sprawdzić czy stworzony strumień zostanie prawidłowo rozesłany. Weryfikujemy w tym celu kilka właściwości nagranych wiadomości:

  • czy ilość dodanych etapów na strumień jest równa ilości otrzymanych, z pominięciem OnCompleted (-1)
1
2
3
4
private void assert__for_messages_count(IList<Recorded<Notification<int>>> messages)
{
  var shouldBeValue = _disposed - 1;
  messages.Count.ShouldBe(shouldBeValue);
  • czy łączny czas jaki strumień żył jest odpowiedni, ilość elementów razy jednostka czasu na element,
1
_testScheduler.Clock.ShouldBe(_disposed * TimeSpan.FromSeconds(1).Ticks);
  • weryfikacja czasów poszczególnych publikacji, tutaj mamy dwie możliwości, dla hot i cold.
1
2
3
4
5
private void assert__for_messages_time_hot(IList<Recorded<Notification<int>>> messages)
{
  for (var i = 0; i < _disposed - 1; i++)
  {
    messages[i].Time.ShouldBe(TimeSpan.FromSeconds(i + 1).Ticks);

Wersja hot różni się tym, że czas wyliczany jest relatywny w odniesieniu do utworzenia strumienia. Natomiast cold odnosi się do momentu subskrypcji.

1
2
3
4
5
private void assert_for_messages_time_cold(IList<Recorded<Notification<int>>> messages)
{
  for (var i = 0; i < _disposed - 1; i++)
  {
    messages[i].Time.ShouldBe(TimeSpan.FromSeconds(i + 1).Ticks + 1);

Dodanie 1 tick-a, właśnie pochodzi z tej różnicy w funkcjonowaniu. Opóźnienie między utworzeniem, a subskrypcją w tym konkretnym przypadku gdzie obie wartością ustawione na 0 (metoda Start). Wynosi właśnie 1 tick.

  • weryfikowanie rodzaju rekordu, użyłem w tym kontekście metod: ShouldBeOnNext i ShouldBeOnError.
1
2
3
4
5
6
7
8
9
10
11
12
13
private void assert__for_messages(IList<Recorded<Notification<int>>> messages)
{
  var index = 0;
  var value = 0;
  messages[index++].Value.ShouldBeOnNext(value++);
  messages[index++].Value.ShouldBeOnNext(value++);
  messages[index++].Value.ShouldBeOnNext(value++);
  messages[index++].Value.ShouldBeOnError(typeof(Exception));
  messages[index++].Value.ShouldBeOnNext(value++);
  messages[index++].Value.ShouldBeOnNext(value++);
  messages[index++].Value.ShouldBeOnError(typeof(ArgumentException));
  messages[index++].Value.ShouldBeOnNext(value++);
  messages[index++].Value.ShouldBeOnNext(value++);

Należy tutaj oczywiście zweryfikować całą zawartość strumienia, czy publikowane dane są odpowiednie.

Oczywiście wszystkie te weryfikacje warto zebrać w całość. I stosując się do zasady DRY, wykorzystać co się da do niecnych celów.

1
2
3
4
5
6
7
8
public void assert__simulation_notification_of_cold_observable(IList<Recorded<Notification<int>>> messages)
{
  assert_for_messages_time_cold(messages);

  assert__for_messages_count(messages);
  assert__for_test_scheduler_clock();
  assert__for_messages(messages);
}

Różnica pomiędzy obiema metodami w odniesieniu do gorącego i zimnego jest w metodach: assert_for_messages_time_cold, assert__for_messages_time_hot jak same nazwy wskazują.

1
2
3
public void assert__simulation_notification_of_hot_observable(IList<Recorded<Notification<int>>> messages)
{
  assert__for_messages_time_hot(messages);

Ludzie powiedzą:

  • jak to przecież ShouldBeOnNext i ShouldBeOnError nie ma w bibliotece Shouldly
  • racja niema. To sobie napisałem je sam :p.

DRY mi tak kazał. Połączyłem weryfikowanie w całość.

1
2
3
4
5
6
public static void ShouldBeOnNext<T>(this Notification<T> value, T expectedValue)
{
  value.Kind.ShouldBe(NotificationKind.OnNext);
  value.HasValue.ShouldBeTrue();
  value.Exception.ShouldBeNull();
  value.Value.ShouldBe(expectedValue);

Pierwsza sprawdza czy powiadomienie jest typu OnNext. W tym celu sprawdzam:

  • Kind - typ to NotificationKind.OnNext,
  • HasValue - OnNext powinien mieć wartość,
  • Exception - nie powinno być wyjątku,
  • Value - i oczywiście wartość powinna być taka jak zapodałem w metodzie.
1
2
3
4
5
public static void ShouldBeOnError<T>(this Notification<T> value, Type type)
{
  value.Kind.ShouldBe(NotificationKind.OnError);
  value.HasValue.ShouldBeFalse();
  value.Exception.ShouldBeOfType(type);

Druga metoda sprawdza czy jest to OnError:

  • Kind - typ to NotificationKind.OnError,
  • HasValue - OnError nie powinien zawierać żadnej wartości,
  • Exception - za to powinien rzucić jakiś wyjąteczek.

Zakończenie

A poco ci tyle asercji, powinna być jedna na klasę. No tak jedna, i jest jedna w kontekście weryfikacji strumienia jest jedna asercja, a że składa się na nią tyle czynników to już nie moja wina.

Ale poco zaglądasz do pomocniczej klasy, zobacz co robi test i czy działa, nie grzeb tam bo tam jest bałagan.

Kończąc na dzisiaj, mam nadzieje, że moje przemyślenia i wypociny do czegoś się komuś przydadzą.

Ja dzięki cało miesięcznej pracy z Rx-ami nauczyłem się bardzo wiele, nie tylko z samej biblioteki. Ale także z samego pisania i wytrwałości.

Jestem obecnie skłonny choć w małym stopniu poczuć się jak Gutek w jego rocznym celu pisania postów co dzień. Wielki szacunek, za zrealizowanie tego celu!


Jest to post wchodzący w skład podjętego wyzwania ogłoszonego przez MIROBURN we vlogu z dnia 3 lutego 2018 roku.

Celem wyzwania jest systematyczne działanie w ciągu 30 dni.

Postanowiłem pisać post dziennie o tematyce Programowania Reaktywnego dla platformy .NET.

Wszelkie źródła związane z postami znajdują się na repozytorium GitHub.

Stan obecny wyzwania: 30 z 30 dni.


Referencje:


Wcześniejszy: Programowanie Reaktywne - Bileciki do kontroli - Unit Tests of Observer Interval

Następny: Programowanie Reaktywne - Szpryca - AutoFac