Stopniowe podejście do wstrzykiwania zależności

12

Pracuję nad tym, aby moje klasy mogły być testowane jednostkowo przy użyciu wstrzykiwania zależności. Ale niektóre z tych klas mają wielu klientów i nie jestem jeszcze gotowy przefakturować ich wszystkich, aby zacząć przekazywać zależności. Więc staram się to robić stopniowo; zachowując na razie domyślne zależności, ale pozwalając na ich zastąpienie w celu przetestowania.

Jednym z moich podejść jest przeniesienie wszystkich „nowych” wywołań do własnych metod, np .:

public MyObject createMyObject(args) {
  return new MyObject(args);
}

Następnie w moich testach jednostkowych mogę po prostu podklasować tę klasę i zastąpić funkcje tworzenia, aby zamiast tego tworzyły fałszywe obiekty.

Czy to dobre podejście? Czy są jakieś wady?

Mówiąc bardziej ogólnie, czy dobrze jest mieć sztywno zakodowane zależności, o ile można je zastąpić do testowania? Wiem, że preferowanym podejściem jest wyraźne wymaganie ich od konstruktora i chciałbym w końcu tam dotrzeć. Ale zastanawiam się, czy to dobry pierwszy krok.

Jedna wada, która właśnie mi się przydarzyła: jeśli masz prawdziwe podklasy, które musisz przetestować, nie możesz ponownie użyć podklasy testowej, którą napisałeś dla klasy nadrzędnej. Będziesz musiał utworzyć podklasę testową dla każdej prawdziwej podklasy i będzie musiał zastąpić te same funkcje tworzenia.

JW01
źródło
Czy możesz wyjaśnić, dlaczego nie można ich teraz przetestować jednostkowo?
Austin Salonen
W kodzie jest wiele „nowych X”, a także wiele zastosowań klas statycznych i singletonów. Więc kiedy próbuję przetestować jedną klasę, naprawdę testuję całą ich grupę. Jeśli uda mi się wyodrębnić zależności i zastąpić je fałszywymi obiektami, będę mieć większą kontrolę nad testowaniem.
JW01

Odpowiedzi:

13

To dobre podejście na początek. Pamiętaj, że najważniejsze jest objęcie istniejącego kodu testami jednostkowymi; po przeprowadzeniu testów możesz dowolnie refaktoryzować, aby jeszcze bardziej ulepszyć swój projekt.

Tak więc początkowo nie chodzi o to, aby projekt był elegancki i błyszczący - wystarczyło przetestować jednostkę kodu przy najmniej ryzykownych zmianach . Bez testów jednostkowych musisz być bardzo konserwatywny i ostrożny, aby uniknąć złamania kodu. Te początkowe zmiany mogą nawet sprawić, że kod będzie wyglądał bardziej niezgrabnie lub brzydko w niektórych przypadkach - ale jeśli pozwolą ci napisać pierwsze testy jednostkowe, w końcu będziesz w stanie zreformować się w kierunku idealnego projektu, który masz na myśli.

Podstawową pracą, którą należy przeczytać na ten temat, jest Efektywna praca ze starszym kodeksem . Opisuje sztuczkę pokazaną powyżej i wiele, wiele innych, w kilku językach.

Péter Török
źródło
Tylko uwaga na ten temat: „po przeprowadzeniu testów możesz dowolnie refaktoryzować” - jeśli nie masz testów, nie refaktoryzujesz, po prostu zmieniasz kod.
ocodo
@ Slomojo Rozumiem, o co ci chodzi, ale nadal możesz przeprowadzić wiele bezpiecznych operacji refaktoryzacji bez testów, szczególnie zautomatyzowanych operacji IDE, takich jak na przykład (w Eclipse for Java) wyodrębnianie powielonego kodu do metod, zmiana nazwy wszystkiego, przenoszenie klas i wyodrębnianie pól, stałe itp.
Alb
Naprawdę powołuję się na pochodzenie terminu Refaktoryzacja, który modyfikuje kod w ulepszone wzorce, które są zabezpieczone przed błędami regresji przez środowisko testowe. Oczywiście refaktoryzacja oparta na IDE sprawiła, że ​​znacznie łatwiej jest „refaktoryzować” oprogramowanie, ale wolę unikać złudzeń, jeśli moje oprogramowanie nie ma testów, a ja „refaktoryzuję” po prostu zmieniam kod. Oczywiście to może nie być to, co mówię komuś innemu;)
ocodo
1
@Alb, refaktoryzacja bez testów jest zawsze bardziej ryzykowna. Zautomatyzowane refaktoryzacje z pewnością zmniejszają to ryzyko, ale nie do zera. Zawsze występują trudne przypadki krawędzi, z którymi narzędzia mogą nie być w stanie prawidłowo sobie poradzić. W lepszym przypadku wynikiem jest komunikat o błędzie z narzędzia, ale w najgorszym przypadku może on po cichu wprowadzić subtelne błędy. Dlatego zaleca się trzymanie się najprostszych i najbezpieczniejszych możliwych zmian, nawet w przypadku zautomatyzowanych narzędzi.
Péter Török
3

Guice-ludzie zalecają używanie

@Inject
public void setX(.... X x) {
   this.x = x;
}

dla wszystkich właściwości zamiast dodawania @Inject do ich definicji. Umożliwi to traktowanie ich jak zwykłych ziaren, co można zrobić newpodczas testowania (i ręcznego ustawiania właściwości) i @Inject w produkcji.


źródło
3

Kiedy mam do czynienia z tego rodzaju problemem, zidentyfikuję każdą zależność od mojej klasy do przetestowania i utworzę chronionego konstruktora, który pozwoli mi je przekazać. Jest to faktycznie to samo, co utworzenie ustawienia dla każdego, ale wolę deklarowanie zależności w moich konstruktorach, aby nie zapomnieć o tworzeniu ich w przyszłości.

Tak więc moja nowa klasa do testowania jednostkowego będzie miała 2 konstruktory:

public MyClass() { 
  this(new Client1(), new Client2()); 
}

public MyClass(Client1 client1, Client2 client2) { 
  this.client1 = client1; 
  this.client2 = client2; 
}
Seth M.
źródło
2

Możesz rozważyć użycie wyrażenia „getter tworzy”, dopóki nie będziesz w stanie użyć wstrzykniętej zależności.

Na przykład,

public class Example {
  private MyObject myObject=null;

  public MyObject getMyObject() {
    if (myObject==null) {
      myObject=new MyObject();
    } 
    return myObject;
  }

  public void setMyObject(MyObject myObject) {
    this.myObject=myObject;
  }

  private void myMethod() {
    if (getMyObject().doSomething()) {
      // Instance automatically created
    }
  }
}

Umożliwi to wewnętrzną refaktoryzację, dzięki czemu będzie można odwoływać się do mojego obiektu przez getter. Nigdy nie musisz dzwonić new. W przyszłości, gdy wszyscy klienci zostaną skonfigurowani tak, aby zezwalać na wstrzykiwanie zależności, wystarczy usunąć kod tworzenia w jednym miejscu - getterze. Seter pozwoli ci wstrzykiwać fałszywe obiekty zgodnie z wymaganiami.

Powyższy kod jest tylko przykładem i bezpośrednio ujawnia stan wewnętrzny. Powinieneś dokładnie rozważyć, czy to podejście jest odpowiednie dla twojej bazy kodów.

Polecam również przeczytanie „ Skutecznej pracy ze starszym kodem ”, która zawiera wiele przydatnych wskazówek, które pozwolą odnieść sukces w dużym refaktoryzacji.

Gary Rowe
źródło
Dzięki. Rozważam takie podejście w niektórych przypadkach. Ale co robisz, gdy konstruktor MyObject wymaga parametrów, które są dostępne tylko wewnątrz twojej klasy? Lub kiedy musisz stworzyć więcej niż jeden MyObject w trakcie życia swojej klasy? Czy musisz wstrzyknąć MyObjectFactory?
JW01
@ JW01 Możesz skonfigurować kod twórcy programu pobierającego, aby odczytał stan wewnętrzny z twojej klasy i po prostu go dopasował. Utworzenie więcej niż jednej instancji podczas życia będzie trudne do zorganizowania. Jeśli napotkasz taką sytuację, sugeruję przeprojektowanie, a nie obejście. Nie rób zbyt skomplikowanych pobieraczy, bo staną się kamieniem młyńskim na szyi. Twoim celem jest ułatwienie procesu refaktoryzacji. Jeśli wydaje się to zbyt trudne, przejdź do innego obszaru, aby to naprawić.
Gary Rowe,
to singleton. Po prostu nie używaj singletonów O_O
CoffeDeveloper
@DarioOO W rzeczywistości jest to bardzo kiepski singleton. Lepsza implementacja użyłaby wyliczenia według Josh Bloch. Singletony są prawidłowym wzorcem projektowym i mają swoje zastosowania (kosztowne jednorazowe tworzenie itp.).
Gary Rowe