Internal konstruktor a testowanie

1

Ostatnio był tu wątek dotyczący tego, jak sprawdzać unikalność NIPu klienta. Jednym z rozwiązań było stworzenie CustomerFactory, która miałaby wstrzyknięte ICustomerRepository (albo inny serwis domenowy) i zwracałaby encję Customer po sprawdzeniu, czy NIP na pewno jest unikalny. Ale najważniejszym punktem było to, że Customer miałby konstruktor oznaczony jako internal. I teraz pojawia się problem: jak w testach tworzyć klientów, skoro konstruktor Customer jest niewidoczny w projekcie z testami? Chciałbym mieć takiego DSLa: https://github.com/asc-lab/better-code-with-ddd/blob/ef_core/LoanApplication.TacticalDdd/LoanApplication.TacticalDdd.Tests/Builders/CustomerBuilder.cs Przychodzą mi do głowy następujące rozwiązania:

  1. Użycie w metodzie Build klasy CustomerBuilder instancji CustomerFactory mającej wstrzyknięte zmockowane ICustomerRepository. Jeśli tylko konstruktor jest internal, to raczej nie powinno być problemu. Gorzej, jeśli metody zmieniające stan Customer też będą internal i żeby je wywołać trzeba będzie korzystać z innych serwisów domenowych. To już będzie niewygodne.
  2. Zamiana wszystkich metod i konstruktorów w projekcie domenowym na public. To dość kontrowersyjne i radykalne.
  3. Udostępnienie jakiegoś publicznego konstruktora jedynie na potrzeby testów, za pomocą którego dałoby się wprowadzić encję Customer w dowolny stan. Taki konstruktor nie miałby żadnej walidacji, po prostu ustawiałby wszystkie pola i właściwości na to, co zostało przekazane do tego konstruktora.
  4. Użycie InternalsVisibleTo. Znów kontrowersyjnie, ale używalibyśmy tych internali tylko w DSLach. W samych testach używalibyśmy wyłącznie publicznego API (serwisów domenowych).

Co byście zrobili? Może jakieś zupełnie inne rozwiązanie? @somekind @Aventus

0

Jeśli musisz to gdzieś użyć, zrób to publiczne. Jeśli nie wystawiasz tego jako biblioteka dla innych klientów, gdzie rzeczywiście chciałbyś więcej kontroli nad tym co jest dostępne, to po co kombinować? Jakie zalety daje Ci w tym przypadku ten internal? Sam kiedyś miałem takie dylematy, chciałem żeby wszystko było zrobione "czysto". Nauczyłem się jednak że czasem nie ma nic lepszego niż zimny pragmatyzm ;) także nie uważam aby zamiana na public była kontrowersyja czy radykalna, chyba tylko dla tych co sztywno i ślepo trzymają się zasad "bo tak jest i się z tym nie dyskutuje". Chociaż weź pod uwagę że nie musisz od razu zamieniać WSZYSTKIEGO na public.

Ewentualnie zmień podejście do testowania i testuj logikę biznesową jako workflow a nie jednostkę. Już kilka razy opisywałem na forum takie hybrydowe podejście.

5

W testach powinno się tworzyć obiekty tak jak w produkcyjnym kodzie. Możesz zrobić sobie nad tym (nie zamiast) jakieś pomocnicze testowe buildery.

1

@Aventus: już kiedyś chciałem o to zapytać. Powiedzmy, że testujemy aplikację hybrydowo (in memory test server plus testowe implementacje repozytoriów i innych serwisów komunikujących się ze światem zewnętrznym). Chcemy przetestować przypadek użycia, który jest w dość późnym etapie jakiegoś złożonego procesu biznesowego. Np. mamy proces realizacji zamówienia i chcemy sprawdzić, czy faktura się poprawnie generuje. Zanim sprawdzimy, czy system poprawnie generuje faktury, musimy wprowadzić system w stan, w którym tę fakturę w ogóle możemy wygenerować. Widzę 2 opcje:

  1. Wysłać N żądań HTTP (prawdopodobnie jako różni użytkownicy) po to, żeby dodać produkt do systemu, ustawić rabat dla produktu, dodać produkt do koszyka, zmienić ilość produktów, stworzyć testowe zamówienie, przyjąć zamówienie do realizacji. To dość dużo kodu, no i do tego każdy przypadek użycia chcemy pewnie przetestować kilka razy, za każdym razem potrzebując innego stanu początkowego systemu (inny stan jest potrzebny do sprawdzenia happy path, a inny do sprawdzenia, czy obsługa błędów działa poprawnie).

  2. Zapisać ręcznie w testowych repozytoriach odpowiednie dane i wysłać od razu żądanie generujące fakturę.

Wydaje mi się, że w każdym przypadku potrzebujemy jakiegoś DSLa, żeby było to elastyczne. Czy można jakoś inaczej?

1

o k****a

Domain/Core:

public class User
{
    public User(int age)
    {
        this.Age = age;
    }

    // LOL
    protected User()
    {
    }

    protected int _age;

    public int Age
    {
        get { return _age; }
        set
        {
            if (value < 0)
            {
                throw new Exception("incorrect value");
            }

            _age = value;
        }
    }

}

Test project:

public class UserForTest : User
{
    public UserForTest(int unconstrained_age)
    {
        this._age = unconstrained_age;
    }

    public void SetInternalAge(int unconstrained_age)
    {
        this._age = unconstrained_age;
    }
}

Usage:

var user = new User(5);

try
{
    var user2 = new User(-1);
}
catch (Exception ex)
{
    Console.WriteLine(ex.Message);
}

var user_for_tests = new UserForTest(-50);
user_for_tests.SetInternalAge(-100);
4

Ja stosuję podejście 2, bo chodzi o to żeby przetestować jakiś konkretny workflow, a nie cały cykl życia aplikacji. Jeśli konkretny workflow wymaga aby w systemie istniały już dane X, Y i Z, to wstrzykuję te dane do testowego repo (w pamięci). Wysyłanie rządań jak w punkcie 1 mija się z celem, w kontekście testowania konkretnego workflow nie przynosi żadnego pożytku.

Co do przykładu z generowaniem faktur, to zakładając że masz tam różne możliwe wyniki na podstawie danych wejściowych dla takiej faktury, wydaje mi się że taki generator faktur powinien być jednak przetestowany jednostkowo. Oczywiście nie znaczy to że wtedy test hybrydowy traci sens- nadal jest potrzebny, ale do bardziej rozdrobnionych przypadków jaki właśnie jakaś fabryka, generator czy parser, warto dodatkowo użyć testów jednostkowych.

0

Jeszcze zawołam @Shalom , bo to podejście z DSLem wziąłem z: https://github.com/Pharisaeus/almost-s3/blob/master/test/src/test/java/net/forprogrammers/almosts3/test/dsl/TestFile.java Tutaj jest chyba inne podejście, bo o ile rozumiem, to DSL nie operuje na encjach domenowych, tylko na specjalnych encjach do testów.

3

@nobody01 oczywiście że tak, bo przecież te moje testowe encje mają inne zachowania niż te "prawdziwe". Np. nie mam tu żadnej walidacji, bo mogę chcieć testować "niepoprawne dane" i musze być w stanie generować taki stan. Dodatkowo większość tych testowych obiektów ma w sobie jakaś logikę do ustawiania "domyślnych wartości" albo generowania "losowego" stanu. Jedyne co te obiekty maja wspólnego z encjami domenowymi to niektóre pola.

Przy takim podejściu twój początkowy problem z tego wątku w ogóle nie istnieje, bo nigdy nie tworzysz w teście ręcznie jakiegoś obiektu domenowego. Na podstawie testowego DSLa wprowadzasz aplikacje w taki czy inny stan (np. wrzucasz dane do jakiejś bazy, konfigurujesz jakie jsony maja zwrócić wiremocki) i tyle. Obiekty domenowe będzie tworzyć twoja warstwa domeny, w odpowiedzi na przychodzące requesty.

Chcemy przetestować przypadek użycia, który jest w dość późnym etapie jakiegoś złożonego procesu biznesowego. Np. mamy proces realizacji zamówienia i chcemy sprawdzić, czy faktura się poprawnie generuje. Zanim sprawdzimy, czy system poprawnie generuje faktury, musimy wprowadzić system w stan, w którym tę fakturę w ogóle możemy wygenerować.

No to moje pytanie brzmi: chcesz przetestować ten przypadek użycia (cały!) czy chcesz przetestować tylko fragment odpowiedzialny za generowanie faktury? Różnica jest dość spora. Ja osobiście jestem zwolennikiem pisania testów "całego use case", nawet jeśli to oznacza ze test będzie dość długi. Plus jest taki że taki test faktycznie coś ci mówi o działaniu systemu, a jak masz sensowny DSL to mimo wszystko test będzie czytelny.
Oczywiście jak ten twój generator ma milion możliwości, to warto go też otestować jednostkowo, tak samo jak testujesz jakiś parser czy coś w tym stylu.

1

Często warto jest odejść od testowania klasy na rzecz testowania tego co może zrobić użytkownik twojego API. Skoro użytkownik twojego modułu nie może sam stworzyć usera to czemu testy tworzą takiego usera w "magiczny" sposób?
Jeżeli testy wchodzą w detale tego jak coś jest zaimplementowane i używaja niepublicznych elementów twojego API to utrudniasz sobie w przyszłości refactoring - testy powinny pomagać w tym żeby moduł działał jak się oczekuje i żeby dało się refactorowac a nie sprawdzać implementacja jest napisana tak jak wymyśliliśmy

0

@sirazure: No... tak. Ale mimo wszystko w testach API nie zawsze wysyłasz 100 żądań HTTP, żeby wprowadzić system w odpowiedni stan, tylko wrzucasz te dane jakoś "bokiem" do bazy danych (użytkownik nie może dodać usera z pominięciem API, a jednak w testach API tak właśnie robimy). Tak samo jak chcę przetestować API jakiegoś wewnętrznego modułu, to też chcę ten moduł móc jakoś łatwo wprowadzić w taki stan, żebym mógł wykonać dany test. No i do tego dochodzi ten problem, że np. dane w systemie nie są poprawne, jak już zostało wspomniane. Np. zaimportowaliśmy dane z zewnętrznego systemu i te dane nie są kompletne. Chcemy sprawdzić, jak system zareaguje, gdy będziemy chcieli np. edytować dane kontrahenta niemającego NIPu.

2

@nobody01: mówiąc o API myślałem o API modulu, libki - niekoniecznie o HTTP.

Jezeli chcemy przetestować dziwne błędne dane które wiemy że są w systemie to jak @Aventus pisał można je na starcie dodać do mocka/in memory repo czy czego się używa.

Nie jestem przekonany co do magicznych (backdoor owych)
sposobów uzyskania specyficznych stanów w systemie które można uzyskać przez sekwencje standardowych akcji. Jeżeli to unit testy to infrastruktura może być lekka więc raczej nie zajmuje to długo a dłuższy kod można właśnie w DSL schować.

0

Odnośnie testowania internalsów i używania InternalsVisibleTo, to na stacku znalazłem taki wątek: https://stackoverflow.com/questions/7580710/is-it-considered-bad-practice-to-use-internalsvisibleto-for-unit-test-code
Jon Skeet pisze, że:

Personally I think it's fine. I've never gone along with the dogma of "only test public methods". I think it's good to also have black box testing, but white box testing can let you test more scenarios with simpler tests, particularly if your API is reasonably "chunky" and the public methods actually do quite a lot of work.
Likewise, in a well-encapsulated project you may well have several internal types with only internal methods. Now presumably those will have a public impact so you could do all the testing just through the public types - but then you may well need to go through a lot of hoops to actually test something that's really simple to test using InternalsVisibleTo.

Nie powiem, że wydaje się to mieć sens. Jeśli mamy jakiś moduł, gdzie spośród kilkudziesięciu klas tylko kilka jest publiczne, to jak mamy napisać do niego testy? Możemy przepuszczać wszystkie testy przez te kilka publicznych klas, ale tu jest ten sam problem, co przy testowaniu wyłącznie API systemu. Mimo że możemy wszystko sprawdzić wysyłając requesty HTTP, to jednak z jakiegoś powodu te bardziej skomplikowane elementy systemu (jakieś obliczenia, parsery, walidatory) testujemy w izolacji.

1

Można użyć InternalsVisibleTo albo tak jak proponowałem wcześniej po prostu pójść drogą zimnego pragmatyzmu i zrobić to co chcesz przetestować publiczne. Jeśli Twój kod nie jest wystawiany w formie biblioteki dla innych użytkowników to praktycznie nie ma żadnych przeciwwskazań.

2

Zaraz, a czy my tu do Javy jesteśmy przyspawani, czy to jednak dział ogólny? W Javie bym zrobił publiczny lub protected i napisał adnotacje z @VisibleForTesting, ale to kulawe rozwiązanie godne kulawego systemu pakietów.

W innych językach (np Scali, Rust, C++) można zrobić konstruktor prywatny dla pakietu, podpakietu, całej biblioteki albo nawet upublicznić go tylko dla wybranego innego modułu (np friend w C++) tak aby testy mogły go wywołać, bez konieczności upubliczniania go dla reszty kodu.

Co do tego, że powinniśmy przejść cała ścieżkę i korzystać tylko z ogólnego API to się nie zgodzę - testy end-to-end mają swoje zalety, ale mają też kilka poważnych wad, m.in. długo się wykonują i dają mało precyzyjne informacje o błędzie. Testy które testują mniejsze kawałki systemu na raz wykonują się szybciej i szybciej się je debuguje, no i wtedy czasem muszą sięgać do rzeczy normalnie prywatnych z punktu widzenia użytkownika.

1
nobody01 napisał(a):

Nie powiem, że wydaje się to mieć sens. Jeśli mamy jakiś moduł, gdzie spośród kilkudziesięciu klas tylko kilka jest publiczne, to jak mamy napisać do niego testy?

Napisałbym prosty test dla publicznych metod/klas a potem przez testowanie mutacyjne napisał resztę. Po co chcesz testować klasy/metody prywatne? Trochę w dół i zaczniesz testować czy repozytoria springowe działają zgodnie z opisem? Z mockami też byłbym ostrożny: testujesz X i potrzebujesz 10 mocków, które musisz ustawić. Tymczasem ktoś zmienił zachowanie jednej z mockowanych klas. Może się okazać, że test X opiera się na złych założeniach, mimo, że ciągle przechodzi.

4
nobody01 napisał(a):

Co byście zrobili? Może jakieś zupełnie inne rozwiązanie? @somekind @Aventus

Nie do końca rozumiem, czemu konstruktor Customera musi być internal? Bo może problemu tak naprawdę wcale nie ma?

Ja osobiście nie uznaję zmieniania kodu produkcyjnego na potrzeby testów. Wszelkie "konstruktory do testów" i "klasy do testów" to złe praktyki, które prowadzą do tego, że testowany jest nie ten kod, który znajduje się na produkcji. A InternalsVisibleTo świadczy o chęci testowania klas zamiast jednostek lub o złym podziale na moduły.
Jeśli test potrzebuje wprowadzenia domeny w jakiś dziwny stan, to niech to zrobi po swojej stronie, a nie psując kod produkcyjny. Ale należy się też dobrze zastanowić, czy to, że potrzebujemy robić coś dziwnego nie wynika z błędnego projektu albo z tego, że chcemy testować coś, czego nie powinniśmy.

nobody01 napisał(a):

@sirazure: No... tak. Ale mimo wszystko w testach API nie zawsze wysyłasz 100 żądań HTTP, żeby wprowadzić system w odpowiedni stan, tylko wrzucasz te dane jakoś "bokiem" do bazy danych

Ale czemu właściwie kombinować z bazą, zamiast użyć API?

Np. zaimportowaliśmy dane z zewnętrznego systemu i te dane nie są kompletne. Chcemy sprawdzić, jak system zareaguje, gdy będziemy chcieli np. edytować dane kontrahenta niemającego NIPu.

Powinien się wywalić, bo gdzieś tam konstruktor encji powinien ten NIP wymuszać.
Pytanie zasadnicze, czy powinniście zezwolić na taki import? Czy leci z nami BA?

nobody01 napisał(a):

Nie powiem, że wydaje się to mieć sens. Jeśli mamy jakiś moduł, gdzie spośród kilkudziesięciu klas tylko kilka jest publiczne, to jak mamy napisać do niego testy?

Normalnie - jeżeli coś nie jest publiczne, to widocznie nie musi być testowane, bo zostanie pokryte testami API modułu. A jeżeli jakieś klasy internalowe w module X są tak skomplikowane, że wymagają oddzielnego testowania (albo testowanie X po prostu ich nie pokryje w całości), to najprawdopodobniej znaczy, że powinny być publicznymi klasami modułu Y.

0

@somekind:

somekind napisał(a):

Nie do końca rozumiem, czemu konstruktor Customera musi być internal? Bo może problemu tak naprawdę wcale nie ma?

Hmm, mogę zrobić konstruktor publiczny, tylko co z innymi metodami, które są internal? Musiałbym je wszystkie zrobić publiczne, bo publicznym konstruktorem nie wprowadzę encji w taki stan, jaki potrzebuję, żeby przeprowadzić dany test.

Ale czemu właściwie kombinować z bazą, zamiast użyć API?

W sensie zrobić DSLa, który pod spodem wysyła te 100 requestów do API?

2

Chyba zaszło pewne nieporozumienie. Nie chodzi o to żeby wysyłać N requestów do API, ani żeby wysyłać cokolwiek do bazy danych. Dane wystarczy wprowadzić do testowych implementacji repozytoriów.

2
nobody01 napisał(a):

Hmm, mogę zrobić konstruktor publiczny, tylko co z innymi metodami, które są internal? Musiałbym je wszystkie zrobić publiczne, bo publicznym konstruktorem nie wprowadzę encji w taki stan, jaki potrzebuję, żeby przeprowadzić dany test.

Ja tam nie widzę problemu w doprowadzeniu obiektu do dowolnego stanu w testach, nawet jeśli wszystkie konstruktory i metody ma prywatne. Zresztą, może nawet nie mieć żadnych metod, konstruktorów ani pól, testy mogą je sobie dodać.

Tylko pytanie, czemu właściwie encja ma mieć wszystko internal? To brzmi jakby moduł logiki biznesowej nie miał żadnego API. Czyli utrudnienie w używaniu tego będzie nie tylko z testami, ale i w ogóle z kodem produkcyjnym. Jak to w ogóle ma działać, reagować na jakieś operacje użytkownika, robić cokolwiek?

Aventus napisał(a):

Chyba zaszło pewne nieporozumienie. Nie chodzi o to żeby wysyłać N requestów do API, ani żeby wysyłać cokolwiek do bazy danych. Dane wystarczy wprowadzić do testowych implementacji repozytoriów.

Można, tylko to strata czasu, bo taki test nigdy nie wykryje żadnych potencjalnych realnych błędów, więc właściwie niczego nie przetestuje. Lepiej się skupić na faktycznych testach - jednostkowych dla algorytmów i integracyjnych/e2e dla ficzerów.

0

@somekind:

somekind napisał(a):

Ja tam nie widzę problemu w doprowadzeniu obiektu do dowolnego stanu w testach, nawet jeśli wszystkie konstruktory i metody ma prywatne. Zresztą, może nawet nie mieć żadnych metod, konstruktorów ani pól, testy mogą je sobie dodać.

Masz na myśli ustawianie stanu obiektu refleksją? I co to znaczy, że test może sobie dodać pola do obiektu?

Tylko pytanie, czemu właściwie encja ma mieć wszystko internal? To brzmi jakby moduł logiki biznesowej nie miał żadnego API. Czyli utrudnienie w używaniu tego będzie nie tylko z testami, ale i w ogóle z kodem produkcyjnym. Jak to w ogóle ma działać, reagować na jakieś operacje użytkownika, robić cokolwiek?

No właściwie mogę zrobić publiczne wszystko, co jest obecnie internalowe. Po prostu wtedy moduł logiki aplikacji będzie miał wybór, czy chce użyć bezpośrednio konstruktora/metody obiektu, czy jakiegoś serwisu domenowego. Chyba w sumie najprostsze rozwiązanie.

Można, tylko to strata czasu, bo taki test nigdy nie wykryje żadnych potencjalnych realnych błędów, więc właściwie niczego nie przetestuje. Lepiej się skupić na faktycznych testach - jednostkowych dla algorytmów i integracyjnych/e2e dla ficzerów.

Czyli wstawiać od razu dane do jakiegoś postgresa poprzez requesty HTTP? Jak szybko takie testy się u Ciebie wykonują? Ile ich jest?

1
nobody01 napisał(a):

No właściwie mogę zrobić publiczne wszystko, co jest obecnie internalowe. Po prostu wtedy moduł logiki aplikacji będzie miał wybór, czy chce użyć bezpośrednio konstruktora/metody obiektu, czy jakiegoś serwisu domenowego. Chyba w sumie najprostsze rozwiązanie.

O, o, o. I doszliśmy do miejsca, które z mojego punktu widzenia jest najistotniejszym - odpowiedz sobie na pytanie dlaczego ukryłeś całość za serwisem domenowym.
Bo dla mnie, jeżeli encja/agregat/obiekt domenowy/jak to sobie nazwiemy sama z siebie ma spójne API i dobrze przemyślaną wewnętrzną logikę, to dopóki nie pojawi się konkretny powód, fasadowanie tego jest nadmierną abstrakcją.
Jeżeli masz jakieś dodatkowe powiązania, które się logicznie nie mieszczą w samej encji, to serwis domenowy jest git, ale inaczej wydaje mi się, że niepotrzebnie sobie życie komplikujesz.

0
somekind napisał(a):

Można, tylko to strata czasu, bo taki test nigdy nie wykryje żadnych potencjalnych realnych błędów, więc właściwie niczego nie przetestuje. Lepiej się skupić na faktycznych testach - jednostkowych dla algorytmów i integracyjnych/e2e dla ficzerów.

Chodzi tak ogólnie czy w tym konkretnym przypadku? Jeśli tak ogólnie to nie zgadzam się. Poza oczywistymi wadami testów integracyjnych, stosując taką odwróconą piramidę testowania mamy 2 wyjścia:

  • Albo większy ciężar odpowiedzialności przenosimy na testy integracyjne, przez co muszą one być bardziej ziarniste i pokrywać więcej przypadków edge case. Wtedy potęgujemy wady testów integracyjnych (ich prędkość oraz większa możliwość niepowodzenia).
  • Nie przenosimy tego ciężaru, przez co tracimy możliwość testowania części edge cases bo powstaje nam luka. No chyba że decydujemy aby takie przypadki testować jednostkowo, ale wtedy znów angażujemy wady takich testów jakie dobrze znamy- testowanie czy mock zwraca to co powiedzieliśmy żeby zwrócił itp.

Do tego dochodzi produktywność programisty- lokalne (lub jakiś serwer do użytku przez wielu) środowisko testowe programisty musi być ustawione (np. baza danych), trzeba to zazwyczaj jeszcze czyścić itp. W przypadku testów hybrydowych z kolei tego problemu nie ma, ale nadal ma się pewność testowania całego workflow dla danego przypadku (oczywiście poza integracjami z bazą itp). W to wchodzi wyłapywanie błędów w kontekście HTTP, jakieś sprawy dotyczące uwierzytelniania, mapowanie, no i oczywiście sama logika biznesowa. Także twierdzenie że taki test "niczego nie przetestuje" jest ewidentnie na wyrost i sugeruje że integracje to jedyne co może się nie powieść. Tym bardziej że nadal testujemy cały kontekst danego workflow, w odróżnieniu od np. testów jednostkowych. Ponadto zaletą takich testów jest również to że często programista pracujący np. nad nowym endpointem nawet nie musi go odpalać i testować manualnie- wystarczy że napisze test.

Rzecz w tym żeby wszystko się uzupełniało. Testy hybrydowe nie są zamiennikiem testów integracyjnych, i vice versa.

W moim przypadku i tego co stosujemy w mojej pracy testy hybrydowe pod względem ziarnistości są gdzieś pośrodku piramidy testów, ale paradoksalnie pod względem ilości tychże testów są u podstaw piramidy- większość testowania opiera się właśnie na tych testach.

5
Aventus napisał(a):

Do tego dochodzi produktywność programisty- lokalne (lub jakiś serwer do użytku przez wielu) środowisko testowe programisty musi być ustawione (np. baza danych), trzeba to zazwyczaj jeszcze czyścić itp.

Proszę przestańcie i nie róbcie tego więcej.
W .net teź przecież są wynalazki typu testcontainers. A docker działa ostatnich pare lat na windows całkiem dobrze.

EDIT:
a co do tematu -
w takim Kotlinie twórcy podjęli decyzję, że komponenty internal są też widoczne w powiązanej z danym modułem paczce testów.
Więc niejako jest niejawne InternalsVisibleTo zrobione. Można na to patrzeć tak, że testy danego modułu należą do modułu.

1
nobody01 napisał(a):

Masz na myśli ustawianie stanu obiektu refleksją?

No na przykład. Skoro test czegoś chce od domeny, to niech sobie to zrobi, to nie jest powód do zmieniania kodu produkcyjnego.
Ale to podejście może zadziałać na małą skalę i bardzo krótką metę, bo testy szybko stają się zbyt magiczne, więc potem trzeba się wycofywać i robić normalnie (czyli przez interfejs obiektów).

I co to znaczy, że test może sobie dodać pola do obiektu?

No normalnie, kod można w locie generować. Tylko takie testy to raczej się nie przydadzą w domenie. ;)

No właściwie mogę zrobić publiczne wszystko, co jest obecnie internalowe. Po prostu wtedy moduł logiki aplikacji będzie miał wybór, czy chce użyć bezpośrednio konstruktora/metody obiektu, czy jakiegoś serwisu domenowego. Chyba w sumie najprostsze rozwiązanie.

Jaka jest zatem rola serwisu domenowego w tym przypadku? Może jest zbędny, albo robi zbyt wiele?

Czyli wstawiać od razu dane do jakiegoś postgresa poprzez requesty HTTP?

Tak. Wstawianie danych testowych bokiem do bazy niczym się od refleksji nie różni. Po prostu magicznie omijasz w ten sposób kod, i nie masz żadnej gwarancji, że dane, które tak wstawisz będą miały jakikolwiek sens. Czy takie testy zatem będą miały sens?

Jak szybko takie testy się u Ciebie wykonują? Ile ich jest?

Kilkadziesiąt testów, przeciętnie pewnie kilka minut. Co za różnica tak naprawdę, skoro to testy integracyjne?

Aventus napisał(a):

Chodzi tak ogólnie czy w tym konkretnym przypadku? Jeśli tak ogólnie to nie zgadzam się. Poza oczywistymi wadami testów integracyjnych, stosując taką odwróconą piramidę testowania mamy 2 wyjścia:

Ogólnie.
Nie pisałem nic o odwróconej piramidzie. Jeśli masz bogatą logikę domenową, a nie cruda, to nie będzie żadnej odwróconej piramidy, większość testów nadal będzie jednostkowa, tylko będzie operowała na danych w pamięci, bez odwoływania się do zamockowanych (bądź nie) źródeł danych.
To kwestia wyłącznie designu, ustrukturyzowania kodu, wydzielenia abstrakcji.

  • Albo większy ciężar odpowiedzialności przenosimy na testy integracyjne, przez co muszą one być bardziej ziarniste i pokrywać więcej przypadków edge case. Wtedy potęgujemy wady testów integracyjnych (ich prędkość oraz większa możliwość niepowodzenia).

Muszą obejmować przepływ pozytywny, i kilka negatywnych. Średnio kilka testów na endpoint.

Do tego dochodzi produktywność programisty- lokalne (lub jakiś serwer do użytku przez wielu) środowisko testowe programisty musi być ustawione (np. baza danych), trzeba to zazwyczaj jeszcze czyścić itp.

Nawet jak się tej bazy SQL używa, to jej odświeżenie lokalne to linijka kodu i trwa ułamki sekundy. (Tylko pytanie jak często ma to faktycznie sens.)

jarekr000000 napisał(a):

W .net teź przecież są wynalazki typu testcontainers. A docker działa ostatnich pare lat na windows całkiem dobrze.

Żre RAM, więc pewnie działa, tylko z migracją softu na dockera różnie bywa. :P

w takim Kotlinie twórcy podjęli decyzję, że komponenty internal są też widoczne w powiązanej z danym modułem paczce testów.
Więc niejako jest niejawne InternalsVisibleTo zrobione. Można na to patrzeć tak, że testy danego modułu należą do modułu.

No może jeśli jest to wbudowane w język, to jest to nieco mniej obrzydliwe, ale nadal mi się nie podoba. Zaprojektowanie publicznego API modułu pozwala nam zdefiniować, czego od tego modułu chcemy, co ma zawierać i co robić. Jeśli tego nie wiemy, to czy powinniśmy się w ogóle zabierać do roboty?

0

@somekind:

somekind napisał(a):

Jaka jest zatem rola serwisu domenowego w tym przypadku? Może jest zbędny, albo robi zbyt wiele?

No mam regułę biznesową, że w systemie nie może istnieć 2 klientów z tym samym NIPem. Weryfikację tej reguły chciałbym mieć w warstwie logiki biznesowej, a nie w warstwie aplikacji, dlatego zrobiłem CustomerFactory a konstruktor Customera ustawiłem na internal, żeby warstwa aplikacja nie mogła sobie od tak tworzyć tych klientów jak jej się podoba. Tworzenie kontrahentów to niezbyt jaskrawy przykład, więc weźmy np. tworzenie zamówień. W pierwszym lepszym ERP jest z 10 ścieżek tworzenia zamówienia. Chciałbym mieć logikę związaną z tworzeniem zamówień w projekcie domenowym, żeby mi było łatwiej weryfikować reguły biznesowe i dodawać nowe przypadki użycia. Nie chciałbym wypuszczać logiki biznesowej do warstwy aplikacji.

1

Więc chcesz przenieść regułę biznesową - każdy klient ma unikatowy NIP na ograniczenie technicznie/cykl życia obiektów:
Może być tylko jedna instancja obiektu Customer z danym NIP.
To generalnie średni pomysł z wielu względów.

Głównie dlatego, że w większej skali niemożliwy do wykonania (o ile nie chcemy, aby system działał jak rzygi).

Ale sam pomysł, aby ograniczyć dostęp do metod i konstruktorów jest OK.

1
nobody01 napisał(a):

@somekind:

somekind napisał(a):

Jaka jest zatem rola serwisu domenowego w tym przypadku? Może jest zbędny, albo robi zbyt wiele?

No mam regułę biznesową, że w systemie nie może istnieć 2 klientów z tym samym NIPem. Weryfikację tej reguły chciałbym mieć w warstwie logiki biznesowej, a nie w warstwie aplikacji, dlatego zrobiłem CustomerFactory a konstruktor Customera ustawiłem na internal, żeby warstwa aplikacja nie mogła sobie od tak tworzyć tych klientów jak jej się podoba.

Wiesz co, ja właśnie patrzyłbym na to w drugą stronę - to jest kwestia warstwy aplikacyjnej żeby to uwalić zanim dojdziesz do domeny.
Jeśli masz unikalny, naturalny identyfikator, to próba utworzenia nowego bytu powinna być ubita jak najszybciej, w tym momencie na poziomie weryfikacji poprawności wywołanej przez użyszkodnika akcji/wysłanej komendy, zanim zaczniesz wołać logikę domenową.

I tutaj, nie wiem dokładnie jak wygląda całość domeny, ale powiedziałbym, że to raczej warunek poniekąd "zewnętrzny" w stosunku do domeny, którą faktycznie modelujesz.
Trochę jak próba podziału zysku firmy na zerową liczbę udziałowców. (Tak, to akurat strasznie dziadowy przykład)

No chyba, że @jarekr000000 ma rację i to kwestia pilnowania ilości instancji.

1

Scenariusz z NIP aż prosi się o pragmatyzm i wykorzystanie do tego np. unikalności klucza głównego w bazie danych. Ja bym nawet "zaryzykował" rozwiązanie z próbą stworzenia rekordu (lub jakieś compare exchange w przypadku niektórych bez NoSQL) i odrzucenie operacji jeśli zapis rzucił błędem o naruszeniu unikalności. Po prostu wykorzystanie możliwości bazy jako zapewnienie unikalności NIPu. Obstawiam że próba utworzenia nowego użytkownika z tym samym NIP to i tak jest edge case.

0

Eh, to ja już naprawdę nie wiem. Kiedy mam robić te serwisy domenowe? Przykład z NIPem/emailem jest wszędzie podawany jako klasyczna sytuacja, gdzie potrzebny jest serwis domenowy (jakieś factory). Nawet w tamtym wątku, co @WeiXiao założył. jak modelowaç szybko i źle | jak zapewnić poprawność obiektu

Unikalność NIPu to warunek techniczny, OK. No to powiedzmy, że mam wymaganie, żeby się nie dało stworzyć zamówienia dla kontrahenta, jeśli kontrahent ma już zamówienia będące w trakcie na kwotę łączną większą niż 100 000. To też sprawdzać w warstwie aplikacji?

1

Dla sprostowania- ja nie uważam że taka logika nie powinna być w serwisie domenowym. W sensie- unikalność NIPu jak najbardziej pasuje jako wymóg biznesowy, a więc jego miejsce jest w warstwie domeny. Rzecz w tym że Twoj serwis domenowy będzie potrzebował użyć jakiejś zależność (abstrakcję) która będzie rezerwowała NIP (lub zwracała błąd jeśli taki NIP już istnieje), natomiast implementacja tego to może być właśnie coś korzystajacego z bazy, używając podejścia które opisałem wyżej.

1 użytkowników online, w tym zalogowanych: 0, gości: 1