Testowanie metod Java final || C# non-virtual

Testy testy i po...

Testowanie jest (a przynajmniej powinno być) nieodłączną częścią programowania. Bez testów kod nie jest kompletny i takie tam. Jednak często bywa tak że przetestować się czegoś nie da... Programiści niektórych bibliotek zabezpieczając się  przed użytkownikami, żeby utrzymać w ryzach projekt i nie hamować jego rozwoju uniemożliwiają ich użytkownikom łatwe testowanie. Języki którymi obecnie się posługuję C# i Java nie są w tej sytuacji wyjątkiem i chciałbym pokazać jak niektóre decyzje projektowania oprogramowania, lub nawet samego języka wpływają na późniejsze jego testowanie.

Final i virtual

W języku java mamy słówko kluczowe którym można opatrzyć metodę final, z kolei w C# posiada robiącą to samo lecz na odwrót virtual. Final mówi o tym że metody nie można już nadpisać w klasach dziedziczących, z kolei virtual mówi że można to zrobić. Tak więc domyślne zachowania obu języków są odwrotne - co za tym idzie w kontekście testów?

Mocki

Bez mocków generalnie nie można nic przetestować. Mock naszym przyjacielem. Jednak jak zamockować metodę finalną czy niewirtualną skoro nie można jej nadpisać? Nie da się - po prostu. Bez straszliwych i podstępnych czynów jakim jest mieszanie w kodzie bajtowym nie jest to możliwe. Jednak są biblioteki i możliwości - posiada to standardowe mockito w javie, a w C#... sprawa wygląda inaczej. Ale może o tym później - w każdym razie biblioteki są.

Mockowanie metody final w Java

Oto bardzo ekscytująca i skomplikowana klasa z metodą final:
1
2
3
4
5
public class ClassWithFinals {
    public final int go(){
        return 1;
    }
}
A oto prosty test w którym próbujemy zamockować tę metodę aby zwróciła coć innego niż 1.
1
2
3
4
5
6
7
8
9
10
11
public class ClassWithFinalsTest {
    @Rule
    public MockitoRule mockitoRule = MockitoJUnit.rule();
    @Mock
    ClassWithFinals underTest;
    @Test
    public void go_should_return_mock_value() {
        when(underTest.go()).thenReturn(3);
        assertThat(underTest.go(), is(equalTo(3)));
    }
}

Rezultat?

1
2
3
4
5
6
7
8
9
org.mockito.exceptions.misusing.MissingMethodInvocationException:
when() requires an argument which has to be 'a method call on a mock'.
For example:
when(mock.getArticles()).thenReturn(articles);
Also, this error might show up because:
1. you stub either of: final/private/equals()/hashCode() methods.
Those methods *cannot* be stubbed/verified.
Mocking methods declared on non-public parent classes is not supported.
2. inside when() you don't call method on mock but on some other object.
Hmmm... mockito nie daje rady. Ale my się nie poddamy! Wystarczy stworzyć pliczek w lokalizacji test/resources. O jak tutaj:
Mickito plugin final methods
Lokalizacja pliku konfiguracyjnego dla pluginów mockito
A w środku wpisać:
mock-maker-inline
Po tym zabiegu jest już zielono! Bez zmiany kodu testu. Po prostu od tej pory nie musimy się przejmować czy metoda jest finalna czy też nie.

Mockowanie metody non-virtual w C#

Analogicznie jak poprzednio
1
2
3
4
5
public class ClassWithoutVirtual {
    public int Go() {
        return 1;
    }
}
Oraz test z użyciem mojego ulubionego xUnit, oraz NSubstitute:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ClassWithoutVirtualTest
{
    [Fact]
    public void go_should_return_mock_value()
    {
        // arrange
        var underTest = Substitute.For<ClassWithoutVirtual>();
        underTest.Go().Returns(3);
        // act
        var result = underTest.Go();
        // assert
        Assert.Equal(3, result);
    }
}

Rezultat?

1
2
WeirdMocks.UnitTests.ClassWithoutVirtualTest.go_should_return_mock_value
NSubstitute.Exceptions.CouldNotSetReturnDueToNoLastCallException : Could not find a call to return from.
NSubstitute zwraca exception. Szukałem i nie znalazłem rozwiązania w stylu poprzedniego. Można użyć innego frameworku do mocków, a istnieją z tego co wiem dwa : JustMock i TypeMock Isolator... Oba kosztują i to sporo! Taki typemock na chwilę obecną około 500€ rocznie... Oczywiście w podstawowej wersji.

Rozwiązanie?

Rozwiązanie nasuwa się szybko. Trzeba zrobić dodatkową warstwę pomiędzy używaną klasą a testem. Nie obejdzie się też bez ingerencji w logikę aplikacji o ile taka istnieje. Spotkałem się z tym problemem używając w jednym z projektów HttpClient'a. O ile unit testy nie powstały to pojawił się problem gdy chcieliśmy z zespołem wprowadzić testy behawioralne. Wcale nie prosto było to usunąć - pozostało postawić mały serwisik z mockami do którego strzelała nasza aplikacja. Jednak testy były bardzo wolne i nieco kłopotliwe w utrzymaniu.

Po co to porównanie?

Nie chodzi o to że C# jest be. Nie znam przyczyny dla której wykorzystano dwa odwrotne podejścia w javie i c# do nadpisywnia metod, wydajność? To co możliwe w mockito możliwe jest też w płatnych narzędziach .NETowych. C# wymusza na użytkownikach lepsze przemyślenie kodu, wprowadzanie interfejsów i abstrakcji, gdzie w javie ktoś mógłby na to nie wpaść. Prowadzi to do bardziej przemyślanej architektury aplikacji. Oczywiście w javie można postąpić identycznie, jednakże zmniejszony nakład pracy kusi, a wprowadzanie dodatkowego kodu nie jest korzystne - im go mniej tym mniej w nim błędów.

Jak często tworzysz kod, by dopiero po jakimś czasie gdy zaczną się problemy, lub jakiś "świeżak" w zespole nie ma co robić - dopisujesz testy?

Niedawno zacząłem pisać kod używając TDD - dzięki temu musi być testowalny oraz posiada szczegółową dokumentację, wiadomo co jest zaimplementowane, a co nie - wiadomo co ten kod robi bo nie napiszemy go nie pisząc testów. Choć mam z tym niewiele doświadczenia to widzę w tym niesamowity potencjał tworzenia niezawodnych aplikacji, a z tego co widzę ich struktura jest o wiele prostsza. Bez architektury z rozmachem, która przeszkadza zamiast pomagać. Co więcej pisząc kod w ten sposób, niemalże cały ten artykuł nie ma kompletnego sensu, bo nie użyjesz finalnej metody, nie rzucisz wyjątku w konstruktorze, ani (ku mojej uciesze) ograniczysz paskudne statyczne metody 🙂Po prostu najpierw napisz prosty test, a później równie prosty kod - gdy nowy test zalśni na zielono - refaktoryzuj. To tyle.

Close Menu