Testy - obywatele pierwszej kategorii

Mam wrażenie, że to może być jakieś fatum, że trafiam na blogi pisane przez ludzi, którzy poddali się negatywnemu działaniu nieszczęsnych dyscyplin, które z kolei doprowadziły ich do całkowitego zaprzestania pisania testów jednostkowych.
Ten blog jest przykładem jednego z takich.


Poniższy tekst jest luźnym tłumaczeniem wpisu z bloga Roberta Cecila Wujka Boba Martina stąd:
Proszę o komentarze, gdyby ta luźność była zbyt daleko posunięta.


Autor opowiada o tym, jak jego testy są wrażliwe, ponieważ mockuje wszystkie współpracujące odwołania zewnętrzne. (ech). Każdorazowo, gdy odwołania zewnętrzne się zmienią trzeba zmieniać mocki. I to oczywiście czyni testy jednostkowe wrażliwymi.

Autor opowiada nam, jak porzucił testowanie jednostkowe i zaczął robić coś, co nazywa “Testowaniem Systemowym”. W jego słowniku, “Testy Systemowe” były po prostu testami, które były bardziej end-to-end niż “testy jednostkowe”.

A więc najpierw trochę definicji. Wybacz mi moją nieposkromioną pychę, ale jest teraz tyle różnych definicji “testów jednostkowych”, “testów systemowych” czy “testów akceptacyjnych”, że ktoś powinien podać jedną autorytarną definicję. Nie wiem, czy te definicje się przyjmą; ale mam szczerą nadzieję, że jakimś definicjom uda się to przyszłości.

  • Test Jednostkowy: Test napisany przez programistę, aby upewnić się, że kod produkcyjny robi to, co powinien. (Na ten moment pomińmy fakt, że testy jednostkowe także ulepszają projekt aplikacji, itd.)
  • Test Akceptacyjny: Test napisany przez “biznes”, aby upewnić się, że produkcyjny kod robi to, co powinien. Autorami tych testów są ludzie od strony biznesowej, albo ludzie od strony technicznej, którzy reprezentują biznes, np. Analitycy Biznesowi i Testerzy.
  • Test Integracji: Test napisany przez architektów i/lub liderów technologicznych, aby upewnić się, że podzespół składników systemu działa prawidłowo. To są testy kanalizacyjne. Nie są zestawem testów reguł biznesowych. Ich celem jest potwierdzić, że podzespół jest zintegrowany i skonfigurowany właściwie.
  • Test Systemowy: Test integracyjny napisany , aby upewnić się, że bebechy całego zintegrowanego systemu działają tak jak wcześniej zaplanowano. Są to testy kanalizacyjne. Nie są zestawem testów reguł biznesowych. Ich celem jest potwierdzić, że system jest zintegrowany i skonfigurowany właściwie.
  • Mikro-test: Termin ukuty przez Mike’a Hill’a (@GeePawHill). Test Jednostkowy napisany w bardzo małym kontekście. Celem tego testu jest sprawdzenie jednej funkcji albo małej grupy funkcji.
  • Test Funkcjonalny: Test Jednostkowy napisany dla szerszego kontekstu z odpowiednim zestawem mocków dla powolnych komponentów.

Biorąc pod uwagę te definicje autor wcześniej wspomnianego bloga zrezygnował z pisania źle napisanych mikro testów, na korzyść źle napisanych testów funkcjonalnych. Dlaczego źle napisanych? Ponieważ w obu przypadkach autor opisuje te testy jako połączone do rzeczy, do których nie powinny być połączone.
W przypadku mikro-testów używał za dużo mocków i mocno powiązał swoje testy z kodem produkcyjnym. To oczywiście powoduje, że testy są kruche.

W przypadku testów funkcjonalnych, autor opisuje je jako całą drogę od Interfejsu Użytkownika do Bazy Danych i odnosi się do faktu, że były powolne. Cieszy się kiedy jego testy czasami działają tylko 15 minut. 15 minut to o wiele za dużo, jak na szybki feedback, który testy jednostkowe powinny nam dostarczać. Praktycy TDD nie mają w zwyczaju czekać na system ciągłej integracji, tylko po to, żeby stwierdzić, że testy przechodzą.
Doświadczeni praktycy TDD rozumieją, że ani mikro-testy, ani testy funkcjonalne (ani nawet testy akceptacyjne) nie powinny być mocno połączone z implementacją systemu. Powinny (tak jak autor bloga zachęca nas) być rozważone, jako część systemu i “... traktowane jako obywatele pierwszej kategorii: [One] powinny być traktowane w taki sam sposób, jak kod produkcyjny”.

To, czego wydaje się, autor bloga nie dostrzega, to to, że obywatele pierwszej kategorii systemu nie powinni być ściśle połączeni. Ktoś, kto traktuje swoje testy jako obywateli pierwszej kategorii podejmie wiele starań, aby zapewnić, że te testy nie są mocno powiązane z kodem produkcyjnym.

Rozluźnianie powiązań mikro-testów i testów funkcjonalnych z kodem produkcyjnym nie jest szczególnie trudne. Rzeczywiście wymaga trochę zdolności w projektowaniu oprogramowania i trochę wiedzy na temat technik rozłączania. Zasadniczo spora dawka Projektowania Zorientowanego Obiektowo i odwrócenia zależności, razem z rozsądnym użyciem Wzorców Projektowych (jak Fasada czy Strategia) wystarczą, aby rozluźnić nawet najbardziej szkodliwy zestaw testów.

Niestety, zbyt dużej liczbie programistów wydaje się, że zasady dla testów jednostkowych są inne - że testy mogą być “śmieciowym kodem” napisanym przy użyciu dowolnego sposobu, wybranego ad hoc, jaki tylko ci programiści uznają za stosowny. Także zbyt wielka liczba programistów czyta książki na temat mockowania i kupili te książki w przekonaniu, że narzędzia do mockowania są nieodłączną i potrzebną częścią testowania jednostkowego. Nic nie może być dalej od prawdy.

Ja, dla przykładu, rzadko używam narzędzia do mockowania. Kiedy potrzebuję mocka (albo, raczej Dublera Testowego) piszę go sam. Nie jest trudno pisać takie dublery testowe. Moje IDE bardzo mi w tym pomaga. Co więcej, pisanie Dublera Testowego zachęca mnie do pisania testów jednostkowych bez użycia Dublerów Testowych, no chyba że to jest bardzo potrzebne. Zamiast używać Dublerów Testowych, odchodzę na chwilę od pisania mikro-testów i piszę testy, które są ciut bliższe testom funkcjonalnym. To także pomaga mi rozdzielić testy od wnętrzności kodu produkcyjnego.

Podsumowując, powodem tego, że ludzie poddali się z pisaniem testów jednostkowych jest zwykle to, że nie podążali za powyższą radą autora. Nie traktowali testów jako obywateli pierwszej kategorii. Nie traktowali testów w taki sposób, aby były one nieodłączną częścią systemu. Nie utrzymywali standardów dla tych testów jakich utrzymywali dla innych części systemu. Zamiast tego pozwolili testom się psuć, stać się mocno powiązanymi z kodem, stać się niezmienialnymi, kruchymi i powolnymi. I wtedy, w wielkiej frustracji, porzucili testy.

Morał: Zachowaj swoje testy czyste. Traktuj je jako obywateli pierwszej kategorii w Twoim systemie.


Koniec konkursu Daj Się Poznać 2017

Nadszedł 31 maja 2017, a więc koniec konkursu Daj Się Poznać 2017.
Jestem bardzo wdzięczny. Wdzięczny Społeczności konkursu i Maćkowi Aniserowiczowi. Społeczności za możliwość czytania fantastycznych wpisów. Za burzliwe dyskusje na Slacku o konkursie, programowaniu, czy polityce. Maćkowi za to, że zorganizował pierwszy i najlepszy konkurs dla bloggerów technologicznych w Polsce.

Wiele się nauczyłem przez ten czas od Was - uczestników DSP. Mój dev i blog skill poszybował dzięki Wam o kilka poziomów do góry. Życzę każdemu z Was, abyście prowadzili regularnie Wasze blogi przynajmniej do następnej edycji konkursu. Dzięki za wszystko. Do następnego.

Pozdrawiam
Michał Kuliński



Wzorzec Obserwator - otrzymywanie subskrybowanych informacji.

Po tym jak mogę dodawać obiekty do listy subskrybentów zabieram się za otrzymywanie wiadomości.

Poprzedni post tu.

Napisałem sobie krótki test w Ruby:
def test_receiving_information                                                                                                                                                                                                             
                                                                                                                                                                                                                                             
    @client = Client.new                                                                                                                                                                                                                     
    @media.add_advertising("5 55 555")                                                                                                                                                                                                       
    @media.add_to_subscribers @client                                                                                                                                                                                                        
    #@media.broadcast                                                                                                                                                                                                                        
    #assert_not_empty @client.ads                                                                                                                                                                                                            
                                                                                                                                                                                                                                             
end




Na razie wykomentowałem dalsze akcji i puszczam rake:

coola@sv26 [/home/coola/dsp2017/xp-simulator]# rake                                                                                                                                                                                          
/usr/share/ruby-2.3.1/bin/ruby ./test/all_tests.rb                                                                                                                                                                                           
Loaded suite ./test/all_tests                                                                                                                                                                                                                
Started                                                                                                                                                                                                                                      
......E                                                                                                                                                                                                                                      
=============================================================================================================================================================================================================================================
Error: test_receiving_information(SubscriptionTests): RuntimeError: Argument should have publish method                                                                                                                                      
/home/coola/dsp2017/xp-simulator/src/Media.rb:17:in `add_to_subscribers'                                                                                                                                                                     
/home/coola/dsp2017/xp-simulator/test/subscription_tests.rb:47:in `test_receiving_information'                                                                                                                                               
     44:                                                                                                                                                                                                                                     
     45:     @client = Client.new                                                                                                                                                                                                            
     46:     @media.add_advertising("5 55 555")                                                                                                                                                                                              
  => 47:     @media.add_to_subscribers @client                                                                                                                                                                                               
     48:     #@media.broadcast                                                                                                                                                                                                               
     49:     #assert_not_empty @client.ads                                                                                                                                                                                                   
     50:                                                                                                                                                                                                                                     
=============================================================================================================================================================================================================================================
                                                                                                                                                                                                                                             
                                                                                                                                                                                                                                             
Finished in 0.002118131 seconds.                                                                                                                                                                                                             
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
7 tests, 9 assertions, 0 failures, 1 errors, 0 pendings, 0 omissions, 0 notifications                                                                                                                                                        
85.7143% passed                                                                                                                                                                                                                              
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
3304.80 tests/s, 4249.03 assertions/s                                                                                                                                                                                                        
rake aborted!                                                                                                                                                                                                                                
Command failed with status (1): [/usr/share/ruby-2.3.1/bin/ruby ./test/all_...]                                                                                                                                                              
/home/coola/dsp2017/xp-simulator/Rakefile:4:in `block in <top (required)>'                                                                                                                                                                   
/home/coola/.gems/2.3/gems/rake-12.0.0/exe/rake:27:in `<top (required)>'                                                                                                                                                                     
Tasks: TOP => default => test                                                                                                                                                                                                                
(See full trace by running task with --trace)                                   


Okazuje się, że mój klient nie "implementuje" metody publish, potrzebnej dla obiektu Media do publikacji.

Dopisuję dwie linijki:

class Client                                                                                                                                                                                                                                 
                                                                                                                                                                                                                                             
  def publish                                                                                                                                                                                                                                
  end  
                                                                                                                                                                                                                                      
end 

Dla rake to wystarczy:

coola@sv26 [/home/coola/dsp2017/xp-simulator]# rake                                                                                                                                                                                          
/usr/share/ruby-2.3.1/bin/ruby ./test/all_tests.rb                                                                                                                                                                                           
Loaded suite ./test/all_tests                                                                                                                                                                                                                
Started                                                                                                                                                                                                                                      
.......                                                                                                                                                                                                                                      
                                                                                                                                                                                                                                             
Finished in 0.00171854 seconds.                                                                                                                                                                                                              
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
7 tests, 9 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications                                                                                                                                                        
100% passed                                                                                                                                                                                                                                  
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
4073.22 tests/s, 5237.00 assertions/s               


Odkomentowuję następną linijkę:

def test_receiving_information                                                                                                                                                                                                             
                                                                                                                                                                                                                                             
    @client = Client.new                                                                                                                                                                                                                     
    @media.add_advertising("5 55 555")                                                                                                                                                                                                       
    @media.add_to_subscribers @client                                                                                                                                                                                                        
    @media.broadcast                                                                                                                                                                                                                         
    #assert_not_empty @client.ads                                                                                                                                                                                                            
                                                                                                                                                                                                                                             
end 


Tutaj przede wszystkim nie ma metody broadcast i jej implementacji.

coola@sv26 [/home/coola/dsp2017/xp-simulator]# rake                                                                                                                                                                                          
/usr/share/ruby-2.3.1/bin/ruby ./test/all_tests.rb                                                                                                                                                                                           
Loaded suite ./test/all_tests                                                                                                                                                                                                                
Started                                                                                                                                                                                                                                      
......E                                                                                                                                                                                                                                      
=============================================================================================================================================================================================================================================
Error: test_receiving_information(SubscriptionTests): NoMethodError: undefined method `broadcast' for #<Media:0x007f3f412768c0>                                                                                                              
/home/coola/dsp2017/xp-simulator/test/subscription_tests.rb:48:in `test_receiving_information'                                                                                                                                               
     45:     @client = Client.new                                                                                                                                                                                                            
     46:     @media.add_advertising("5 55 555")                                                                                                                                                                                              
     47:     @media.add_to_subscribers @client                                                                                                                                                                                               
  => 48:     @media.broadcast                                                                                                                                                                                                                
     49:     #assert_not_empty @client.ads                                                                                                                                                                                                   
     50:                                                                                                                                                                                                                                     
     51:   end                                                                                                                                                                                                                               
=============================================================================================================================================================================================================================================
                                                                                                                                                                                                                                             
                                                                                                                                                                                                                                             
Finished in 0.002872458 seconds.                                                                                                                                                                                                             
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
7 tests, 9 assertions, 0 failures, 1 errors, 0 pendings, 0 omissions, 0 notifications                                                                                                                                                        
85.7143% passed                                                                                                                                                                                                                              
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
2436.94 tests/s, 3133.21 assertions/s                                                                                                                                                                                                        
rake aborted!                                                                                                                                                                                                                                
Command failed with status (1): [/usr/share/ruby-2.3.1/bin/ruby ./test/all_...]                                                                                                                                                              
/home/coola/dsp2017/xp-simulator/Rakefile:4:in `block in <top (required)>'                                                                                                                                                                   
/home/coola/.gems/2.3/gems/rake-12.0.0/exe/rake:27:in `<top (required)>'                                                                                                                                                                     
Tasks: TOP => default => test                                                                                                                                                                                                                
(See full trace by running task with --trace)          


Dodaję zmiany w klasie Media:

class Media                                                                                                                                                                                                                                  
                                                                                                                                                                                                                                             
  attr_accessor :ads, :subscribers                                                                                                                                                                                                           
                                                                                                                                                                                                                                             
  def initialize                                                                                                                                                                                                                             
    @ads = Array.new                                                                                                                                                                                                                         
    @subscribers = Array.new                                                                                                                                                                                                                 
  end                                                                                                                                                                                                                                        
                                                                                                                                                                                                                                             
  def add_advertising telephone_number                                                                                                                                                                                                       
    @ads.push telephone_number                                                                                                                                                                                                               
  end                                                                                                                                                                                                                                        
                                                                                                                                                                                                                                             
  def add_to_subscribers client                                                                                                                                                                                                              
    if !client.respond_to? :publish                                                                                                                                                                                                          
      raise 'Argument should have publish method'                                                                                                                                                                                            
    end                                                                                                                                                                                                                                      
                                                                                                                                                                                                                                             
    @subscribers.push client                                                                                                                                                                                                                 
                                                                                                                                                                                                                                             
  end                                                                                                                                                                                                                                        
                                                                                                                                                                                                                                             
  def broadcast                                                                                                                                                                                                                              
    @subscribers.each { |subscriber| subscriber.publish @ads }                                                                                                                                                                               
  end                                                                                                                                                                                                                                        
                                                                                                                                                                                                                                             
end 


Używam metody publish z atrybutem i rzeczywiście rake mówi, że w klasie Client nie ma atrybutu metody publish:

                                                                                                                                                                                         
Loaded suite ./test/all_tests                                                                                                                                                                                                                
Started                                                                                                                                                                                                                                      
......E                                                                                                                                                                                                                                      
=============================================================================================================================================================================================================================================
Error: test_receiving_information(SubscriptionTests): ArgumentError: wrong number of arguments (given 1, expected 0)                                                                                                                         
/home/coola/dsp2017/xp-simulator/src/Client.rb:3:in `publish'                                                                                                                                                                                
/home/coola/dsp2017/xp-simulator/src/Media.rb:25:in `block in broadcast'                                                                                                                                                                     
/home/coola/dsp2017/xp-simulator/src/Media.rb:25:in `each'                                                                                                                                                                                   
/home/coola/dsp2017/xp-simulator/src/Media.rb:25:in `broadcast'                                                                                                                                                                              
/home/coola/dsp2017/xp-simulator/test/subscription_tests.rb:48:in `test_receiving_information'                                                                                                                                               
     45:     @client = Client.new                                                                                                                                                                                                            
     46:     @media.add_advertising("5 55 555")                                                                                                                                                                                              
     47:     @media.add_to_subscribers @client                                                                                                                                                                                               
  => 48:     @media.broadcast                                                                                                                                                                                                                
     49:     #assert_not_empty @client.ads                                                                                                                                                                                                   
     50:                                                                                                                                                                                                                                     
     51:   end                                                                                                                                                                                                                               
=============================================================================================================================================================================================================================================
                                                                                                                                                                                                                                             
                                                                                                                                                                                                                                             
Finished in 0.002341671 seconds.                                                                                                                                                                                                             
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
7 tests, 9 assertions, 0 failures, 1 errors, 0 pendings, 0 omissions, 0 notifications                                                                                                                                                        
85.7143% passed                                                                                                                                                                                                                              
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
2989.32 tests/s, 3843.41 assertions/s                                                                                                                                                                                                        
rake aborted!                                                                                                                                                                                                                                
Command failed with status (1): [/usr/share/ruby-2.3.1/bin/ruby ./test/all_...]                                                                                                                                                              
/home/coola/dsp2017/xp-simulator/Rakefile:4:in `block in <top (required)>'                                                                                                                                                                   
/home/coola/.gems/2.3/gems/rake-12.0.0/exe/rake:27:in `<top (required)>'                                                                                                                                                                     
Tasks: TOP => default => test                                                                                                                                                                                                                
(See full trace by running task with --trace)           

Dopisuję argument ads do klasy Client i jest OK.

Teraz odkomentowuję ostatnią linijkę testu:

def test_receiving_information                                                                                                                                                                                                             
                                                                                                                                                                                                                                             
    @client = Client.new                                                                                                                                                                                                                     
    @media.add_advertising("5 55 555")                                                                                                                                                                                                       
    @media.add_to_subscribers @client                                                                                                                                                                                                        
    @media.broadcast                                                                                                                                                                                                                         
    assert_not_empty @client.ads                                                                                                                                                                                                             
                                                                                                                                                                                                                                             
  end  

Rake krzyczy, że nie ma zmiennej klasowej ads i jest ona pusta:

Started                                                                                                                                                                                                                                      
......E                                                                                                                                                                                                                                      
=============================================================================================================================================================================================================================================
Error: test_receiving_information(SubscriptionTests): NoMethodError: undefined method `ads' for #<Client:0x007f1a96ef2420>                                                                                                                   
/home/coola/dsp2017/xp-simulator/test/subscription_tests.rb:49:in `test_receiving_information'                                                                                                                                               
     46:     @media.add_advertising("5 55 555")                                                                                                                                                                                              
     47:     @media.add_to_subscribers @client                                                                                                                                                                                               
     48:     @media.broadcast                                                                                                                                                                                                                
  => 49:     assert_not_empty @client.ads                                                                                                                                                                                                    
     50:                                                                                                                                                                                                                                     
     51:   end                                                                                                                                                                                                                               
     52:                                                                                                                                                                                                                                     
=============================================================================================================================================================================================================================================
                         

Dodaję zmiany:

class Client                                                                                                                                                                                                                                 
                                                                                                                                                                                                                                             
  def publish ads                                                                                                                                                                                                                            
                                                                                                                                                                                                                                             
    @ads = ads                                                                                                                                                                                                                               
                                                                                                                                                                                                                                             
  end                                                                                                                                                                                                                                        
                                                                                                                                                                                                                                             
  def ads                                                                                                                                                                                                                                    
    @ads                                                                                                                                                                                                                                     
  end                                                                                                                                                                                                                                        
                                                                                                                                                                                                                                             
end

I teraz projekt przechodzi wszystkie testy :)


coola@sv26 [/home/coola/dsp2017/xp-simulator]# rake                                                                                                                                                                                          
/usr/share/ruby-2.3.1/bin/ruby ./test/all_tests.rb                                                                                                                                                                                           
Loaded suite ./test/all_tests                                                                                                                                                                                                                
Started                                                                                                                                                                                                                                      
.......                                                                                                                                                                                                                                      
                                                                                                                                                                                                                                             
Finished in 0.001713314 seconds.                                                                                                                                                                                                             
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
7 tests, 10 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications                                                                                                                                                       
100% passed                                                                                                                                                                                                                                  
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
4085.65 tests/s, 5836.64 assertions/s 

Commit do tego tu.

To jest ostatni projektowy wpis w konkursie Daj się Poznać 2017. Dzięki Maćku i Społeczności :)

Wstęp do wzorca Obserwator, wyjątki i kacze typowanie w Ruby

Tymczasem w projekcie... Na razie klient może sobie dodać swój numer telefonu do obiektu mediów. Chciałbym teraz, żeby każdy z klientów mógł zasubskrybować medium i w dalszej perspektywie medium mogło informować klientów o egzemplarzach swoich periodyków. Jak zawsze, gdy mamy fazę green procesu TDD piszemy test jednostkowy:

def test_if_media_have_subscibers_structure
 
    assert_not_nil @media.subscribers                                                                    
                                                                                                          
end

Od razu jestem na czerwono:

...E                                                                                                      
==========================================================================================================
Error: test_if_media_have_subscibers_structure(SubscriptionTests): NoMethodError: undefined method `subscr
ibers'

Wchodzę w fazę red procesu TDD i piszę kod produkcyjny. I tak w kółko. Test Driven Development jest dyscypliną. Nie musi być mądre to, co robię, ważne żeby przyniosło oczekiwany efekt. Porównując to do koksika ćwiczącego na siłowni - podnoszenie 1000 razy tego samego ciężaru, jako takie, jest głupie, nic nie wnoszące. Ale z punktu widzenia rozwoju mięśni - takie ciągłe, zdawałoby się nieefektywne działanie, w dłuższej perspektywie prowadzi do rozwoju bica i trica.

No więc, żeby ten test przechodził - napisałem następujący kod produkcyjny:

class Media                                                                                               
                                                                                                          
  attr_accessor :ads, :subscribers                                                                   
                                                                                                          
  def initialize                                                                                          
    @ads = Array.new                                                                                      
    @subscribers = Array.new                                                                       
  end                                                                                                     
                                                                                                          
  def add_advertising telephone_number                                                                    
    @ads.push telephone_number                                                                            
  end                                                                                                     
                                                                                                          
end 

Szybkie uruchomienie rake wskazuje, że jestem w fazie green.

Started                                                                                                   
....                                                                                                      
                                                                                                          
Finished in 0.000670749 seconds.                                                                          
4 tests, 8 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications---------------------
                                                                                                          
100% passed

Zielono, więc piszę test, który sprawdza czy mogę dodać aktualnego klienta do listy subskrybentów w klasie mediów.


def test_if_client_can_be_added_to_subscribers_list                                                      
                                                                                                          
    it_people = ItPeople.new                                                                              
                                                                                                          
    @media.add_to_subscibers it_people                                                                    
                                                                                                          
  end 

Czerwono i do kodu produkcyjnego:

def add_to_subscribers client                                                                           
    @subscribers.push client                                                                              
end

Dooobra i zielono, ale chciałbym mieć w tej tablicy subscribers tylko takie obiekty, które implementują interfejs Publicable zawierający metodę Publish.

A więc najpierw test. Gdy do metody add_to_subscribers wpadnie coś, co nie implementuje Publicable poproszę o wyjątek:

def test_if_there_an_exception_if_there_was_no_publicable_implementation_on_entry                       
                                                                                                          
    assert_raises do                                                                                      
      @media.add_to_subscribers 125                                                                       
    end                                                                                                   
                                                                                                          
  end

Blok assert_raises sprawdza, czy w danym bloku kodu - pomiędzy do i end poleciał wyjątek. Spodziewam się, że nie będzie można dodać liczby 125 do tej kolekcji.
I rzeczywiście poleciał wyjątek (inny), bo nie poleciał wyjątek (w kodzie produkcyjnym):

/usr/share/ruby-2.3.1/bin/ruby ./test/all_tests.rb
Loaded suite ./test/all_tests
Started
.....F
==========================================================================================================
Failure: <[]> exception expected but none was thrown.
test_if_there_an_exception_if_there_was_no_publicable_implementation_on_entry(SubscriptionTests)
/home/coola/dsp2017/xp-simulator/test/subscription_tests.rb:37:in `test_if_there_an_exception_if_there_was
_no_publicable_implementation_on_entry'
     34: 
     35:   def test_if_there_an_exception_if_there_was_no_publicable_implementation_on_entry
     36: 
  => 37:                assert_raises do
     38:       @media.add_to_subscribers 125
     39:                end
     40:        end
==========================================================================================================
    

Jestem w fazie czerwonej, więc zawijam kiecę i lecę do kodu produkcyjnego:

def add_to_subscribers client                                                                           
  if client.instance_of? Publicable                                                                     
      @subscribers.push client                                                                            
  end                                                                                              
end

Metoda .instance_of? {nazwa_klasy} sprawdza, czy dany obiekt jest danego typu. Ok, nadal mam error - tym razem kompilacji, bo nie ma takiego bytu jak Publicable. Tworzymy odpowiedni interfejs i importujemy go.

Ale zaraz zaraz Ruby podobno nie ma w sobie interfejsów!

Jak to?

Okazuje się, że rzeczywiście nie ma interfejsów w Ruby, ponieważ Ruby jest językiem dynamicznie typowanym. W Ruby rozpoznajemy typ obiektu nie na podstawie deklaracji typu, ale poprzez badanie metod na tym obiekcie. To tak zwane kacze typowanie. Nazwa ta pochodzi od powiedzenia, że "Jeżeli coś chodzi jak kaczka i kwacze jak kaczka, to musi to być kaczką". Hmmm, ciekawe.



Ok, czyli moje sprawdzenie czy client jest obiektem pewnej klasy, zamieniam na sprawdzenie czy client ma konkretne metody. Dzieje się to przy użyciu:

.respond_to? :nazwa_metody

def add_to_subscribers client                                                                           
    if !client.respond_to? :publish                                                                       
      raise 'Argument should have publish method'                                       
    end                                                                                                   
                                                                                                          
    @subscribers.push client                                                                              
                                                                                                          
  end 

Czyli jeśli nie posiadasz metody publish to rzuć wyjątkiem.
Odpalam testy i okazuje się, że poprzedni test przestał działać:

Started                                                                                                   
..E                                                                                                       
==========================================================================================================
Error: test_if_client_can_be_added_to_subscribers_list(SubscriptionTests): RuntimeError: Argument should h
ave publish method                                                                                        
/home/coola/dsp2017/xp-simulator/src/Media.rb:17:in `add_to_subscribers'                                  
/home/coola/dsp2017/xp-simulator/test/subscription_tests.rb:31:in `test_if_client_can_be_added_to_subscrib
ers_list'                                                                                                 
     28:                                                                                                  
     29:   def test_if_client_can_be_added_to_subscribers_list                                            
     30:                                                                                                  
  => 31:                @media.add_to_subscribers @it_people                                              
     32:                                                                                                  
     33:        end                                                                                       
     34:     

A to dlatego, że rzeczywiście klasa ItPeople nie ma metody publish. Jeszcze nie ma ;)


class ItPeople                                                                                            
                                                                                                          
  attr_accessor :telephone_number                                                                         
                                                                                                          
  def publish                                                                                             
  end                                                                                                     
                                                                                                          
end 

No i jestem na zielono. Jestem tym zaskoczony, że w języku dynamicznym tak mało mam do czynienia z konkretnymi typami. Wydaje mi się, że taki język jest wygodniejszy do testowania jednostkowego. A już, że wyjątek rzuciłem przy użyciu słówka kluczowego raise i opisowego stringa to jest bajka :)  Ciekawe czy dynamiczne typowanie doprowadzi mnie do jakiejś katastrofy, czy nadal będę mógł jechać na autopilocie testów jednostkowych, mając zawiązane oczy na konkretne typy.

Kommit do tego TU.

Profesjonalny Programista

Kim jest profesjonalny programista?


Jedną z najważniejszych cech profesjonalnego programisty jest osobista odpowiedzialność.

Profesjonalni programiści biorą odpowiedzialność za swoją karierę, swoje szacowania, swoje zaplanowane zobowiązania, swoje błędy i za swój kunszt pracy. Profesjonalny programista nie zrzuca odpowiedzialności na innych.


Poniższy tekst jest kopią tłumaczenia Pawła Łukasika ze strony DevBlogi.pl. Oryginał wujka Boba tu:
Ostatnio chętnie czytana przez mnie w przeszłości stronka DevBlogi.pl podupada na technologicznym duchu, więc pozwoliłem sobie umieścić tu ten tekst, tak żeby nie zniknął.


  • Jeśli jesteś profesjonalistą znaczy to, że to Ty jesteś odpowiedzialny za swoją karierę. Jesteś odpowiedzialny za czytanie i uczenie się. Jesteś odpowiedzialny za bycie na bieżąco z branżą i technologią. Wielu programistów uważa, że doszkalanie to obowiązek ich pracodawców. To jest kompletna bzdura. Sądzisz, że lekarze zachowują się w ten sposób? Sądzisz, że prawnicy robią podobnie? Nie, sami szkolą się we własnym czasie i za własne pieniądze. Spędzają sporą ilość czasu po godzinach na czytaniu magazynów i orzeczeń sądów. Są na bieżąco. My także musimy tak robić. Związek pomiędzy Tobą a Twoim pracodawcą jest nakreślony w twoim kontrakcie. W skrócie: pracodawca obiecuje Ci płacić a Ty obiecujesz robić dobrą robotę.
  • Profesjonaliści biorą odpowiedzialność za kod, który piszą. Nie wypuszczają oprogramowania zanim nie upewnią się, że działa. Pomyśl o tym przez chwilę. Jak możesz nazywać siebie profesjonalistą, jeśli masz zamiar oddać kod, którego nie jesteś pewny? Profesjonalni programiści oczekują, że QA nie znajdzie żadnych błędów, ponieważ nie wypuszczają kodu, zanim go dokładnie nie przetestują. Oczywiście QA znajdzie błędy, ponieważ nikt nie jest doskonały. Jednakże jako profesjonaliści powinniśmy stosować takie podejście, że nie dajemy QA okazji od znalezienia błędów.
  • Profesjonaliści są graczami drużynowymi. Biorą odpowiedzialność za wynik całego zespołu a nie tylko za swój. Pomagają sobie wzajemnie, uczą się od siebie a nawet zastępują się, kiedy zachodzi taka potrzeba. Jeśli jeden członek zespołu nie jest w stanie wykonać swojego zadania, drugi wchodzi na jego miejsce, wiedząc, że pewnego dnia on też będzie potrzebował pomocy.
  • Profesjonaliści nie tolerują długich list błędów. Długa lista błędów jest niedopuszczalna. Tysiące zgłoszeń w systemie do śledzenia błędów to tragedia. W rzeczy samej, w większości projektów sam fakt potrzeby systemu do śledzenia błędów jest oznaką niedbałości. Jedynie wielkie systemy powinny mieć listy błędów tak długie, aby był potrzebny system do zarządzania nimi.
  • Profesjonaliści nie robią bałaganu. Chlubią się swoją pracą. Tworzą czysty, dobrze ustrukturyzowany i łatwy do czytania kod. Używają ustalonych standardów i najlepszych praktyk. Nigdy, ale to nigdy się nie śpieszą. Wyobraź sobie, że wychodzisz ze swojego ciała i obserwujesz lekarza przeprowadzającego na Tobie operację. Lekarz ma ostateczny termin. Musi skończyć, zanim płucoserce zniszczy zbyt wiele Twoich krwinek. Jak chciałbyś, aby się on zachowywał? Chciałbyś, aby działał podobnie jak typowy programista, śpiesząc się i robiąc bałagan? Chciałbyś, aby po wszystkim powiedział: “Wrócimy do tego i poprawimy to później”? Czy wolałbyś, aby trzymał się ściśle swojej dyscypliny, nie śpiesząc się, i aby był pewny, że jego podejście jest najlepsze w danej sytuacji. Wolisz bałagan czy porządek?

Profesjonaliści są odpowiedzialni. Biorą odpowiedzialność za swoją własną karierę. Biorą odpowiedzialność za to, że ich kod działa poprawnie. Biorą odpowiedzialność, za jakość swojej pracy. Nie porzucają swoich zasad w obliczu nadciągającego terminu projektu. W rzeczy samej, kiedy ciśnienie wzrasta, profesjonaliści jeszcze bardziej trzymają się zasad, o których wiedzą, że są dobre.




Kursy IT na Pluralsight. Dlaczego warto?


Bardzo sobie cenię kursy na Pluralsight. Mam wrażenie, że każdy kurs, który przeszedłem na tej platformie, w dużym stopniu podniósł moje zdolności. Wiem, dostęp do tej platformy nie jest tani, ale w mojej ocenie warty swojej ceny. To nie jest reklama, ale forma entuzjazmu jaki mam do tej formy samodoskonalenia. O to kilka punktów pokazujących ofertę tego serwisu i dlaczego warto skorzystać:
  1. Pluralsight to kursy z Javascript, C#, Java, Angular, Python, MySQL i wielu innych technologii i umiejętności.
  2. Kursy na Pluralsight w większości mają wyższą jakość niż te, które możemy znaleźć na przykład na YouTube. Są wyselekcjonowane, mają wysoką jakość dźwięku i obrazu. Często wgryzają się głęboko w dany problem daleko poza standardowe „Hello World” danej technologii.
  3. Twórcy Pluralsight to często osoby znane ze świata IT i konferencji branżowych, jak:
    1. Scott Hanselman, Microsoft
    2. John Somnez, SimpleProgrammer.com
    3. John Skeet, Google
  4. Pluralsight udostępnia funkcjonalność ścieżek – paths. Na przykład „ścieżka C#”, „ścieżka JavaScript”. Najpierw sprawdza przez testy, jak sobie radzisz w danej technologii, a potem proponuje kursy w danej technologii. Cały czas trackuje postępy w formie wykresów.
  5. Pluralsight to także dostęp do serwisu CodeSchool (od 2014) umożliwiającego naukę kodowania bezpośrednio w przeglądarce, bez instalowania niezbędnych środowisk programistycznych.
    1. CodeSchool dostarcza kursy w atrakcyjnej wizualnie formie. Każdy kurs wyróżnia się specyficzną czołówką, motywem przewodnim np. (Rails for Zombies)
    2.  Po każdym filmie szkoleniowym uczestnik ma okazję zmagać się z serią zadań, które sprawdzają opanowanie materiału
    3. CodeSchool dostarcza bogatą bazę Screencastów, na których autorzy kursów kodują na żywo.
  6.  Kursy Pluralsight to nie tylko kursy:
    1. dla programistów:
      1. Web Development, Mobile Development, JavaScript, C#, Python, Node.js, C++, Ruby on Rails, Java, React
    2. Ale także dla DevOpsów:
      1. IT Certifications, Security, Database Administration, Virtualization, IT Networking, Servers, Microsoft Office, Windows Server, Microsoft Windows OS, Linux, Cloud Computing, Docker
    3. Czy grafików komputerów i twórców interaktywnych:
      1.  Game Design, Game Art, Game Programming, 3D Design, VFX, Graphic Design, Photoshop, Web Design
  7. Kursy Pluralsight możesz oglądać w przeglądarce, na smartfonach, na smart TV z dostępem do Internetu, czy w aplikacji Windows offline bez dostepu do Internetu po wcześniejszym ściągnięciu kursu.
  8.  Jeżeli męczysz się z jakimś problemem za dodatkową dopłatą (około 1$/minutę) Pluralsight dostarcza możliwość zdzwonienia się 1 na 1 z ekspertem z danej dziedziny.
Dla niezdecydowanych lub mniej zasobnych w gotówkę, chętnych do samokształcenia, podaję link do alternatywnych platform: