2009-05-06 17 views
6

Dziękuję wszystkim za pomoc. Pewna liczba osób, które opublikowałeś (jak powinienem się spodziewać), wskazujące na to, że moje podejście było błędne, lub że kod niskiego poziomu nigdy nie powinien wiedzieć, czy działa w kontenerze. Zgodziłbym się. Mam jednak do czynienia ze złożoną aplikacją starszego typu i nie mam możliwości przeprowadzenia poważnego refaktoryzacji dla bieżącego problemu.W jaki sposób testy jednostek powinny konfigurować źródła danych, gdy nie są uruchomione na serwerze aplikacji?

Pozwól, że cofnę się i zadam pytanie, które umotywowało moje pierwotne pytanie.

Mam starszą aplikację działającą pod JBoss i dokonałem pewnych modyfikacji kodu niższego poziomu. Stworzyłem test jednostkowy dla mojej modyfikacji. Aby uruchomić test, muszę połączyć się z bazą danych.

Kod Legacy dostaje źródło danych w ten sposób:

(jndiName jest określony ciąg znaków)

Context ctx = new InitialContext(); 
DataSource dataSource = (DataSource) ctx.lookup(jndiName); 

Moim problemem jest to, że gdy uruchamiam ten kod pod testów jednostkowych, kontekst nie ma danych zdefiniowane źródła. Moim rozwiązaniem tego problemu było sprawdzenie, czy działa pod serwerem aplikacji, a jeśli nie, utworzenie testu DataSource i zwrócenie go. Jeśli używam serwera aplikacji, używam powyższego kodu.

Tak, moje prawdziwe prawdziwe pytanie brzmi: Jaki jest prawidłowy sposób to zrobić? Czy istnieje jakiś zatwierdzony sposób, w jaki test jednostki może ustawić kontekst, aby zwrócić odpowiednie źródło danych, tak aby testowany kod nie musiał wiedzieć, gdzie jest uruchomiony?


Dla Context: My oryginalne pytanie:

Mam niektóre kodu Java, który musi wiedzieć, czy nie jest uruchomiony pod JBoss. Czy istnieje kanoniczny sposób na określenie, czy kod działa w kontenerze?

Moje pierwsze podejście zostało rozwinięte poprzez eksperymenty i polega na uzyskaniu początkowego kontekstu i testowaniu, że może on wyobrazić sobie pewne wartości.

private boolean isRunningUnderJBoss(Context ctx) { 
     boolean runningUnderJBoss = false; 
     try { 
      // The following invokes a naming exception when not running under 
      // JBoss. 
      ctx.getNameInNamespace(); 

      // The URL packages must contain the string "jboss". 
      String urlPackages = (String) ctx.lookup("java.naming.factory.url.pkgs"); 
      if ((urlPackages != null) && (urlPackages.toUpperCase().contains("JBOSS"))) { 
       runningUnderJBoss = true; 
      } 
     } catch (Exception e) { 
      // If we get there, we are not under JBoss 
      runningUnderJBoss = false; 
     } 
     return runningUnderJBoss; 
    } 

Context ctx = new InitialContext(); 
if (isRunningUnderJboss(ctx) 
{ 
......... 

Teraz wydaje się działać, ale wydaje się, że jest hackerem. Jaki jest "właściwy" sposób na zrobienie tego? Idealnie byłoby, gdyby działał z różnymi serwerami aplikacji, nie tylko JBoss.

+0

Zakładam, że zamiast JBoss masz na myśli Tomcat? (Który jest osadzony w JBoss). – Eddie

+0

Czy próbujesz określić kontener kontra nie-kontener lub typ kontenera, w którym działa aplikacja? – Kapsh

+1

Czy możesz rozwinąć, dlaczego musisz wiedzieć, czy działa w kontenerze? To może pomóc w uzyskaniu odpowiedzi. –

Odpowiedz

2

Całe podejście wydaje mi się błędne. Jeśli Twoja aplikacja musi wiedzieć, który kontener jest uruchomiony, robisz coś nie tak.

Kiedy używam Springa, mogę przenieść się z Tomcat do WebLogic iz powrotem bez zmiany czegokolwiek. Jestem pewien, że przy prawidłowej konfiguracji mógłbym wykonać tę samą sztuczkę z JBOSS. To jest cel, do którego strzelałem.

+0

Wiele osób poczyniło pomocne sugestie, ale ta odpowiedź opisuje podejście, które podjąłem. Wyrzuciłem każdą logikę z testowanego kodu, który próbował sprawdzić, czy był w środowisku testowym. Następnie przeszukałem dokumentację i wymyśliłem, jak utworzyć własny początkowy kontekst i źródło danych, aby testowany kod dostał inne źródło danych, działając pod JUnit. Dziękuję wszystkim, którzy odpowiedzieli. –

+1

Mark - czy mógłbyś opublikować tę konfigurację? Byłoby pomocne dla innych, którzy znaleźliby się w podobnej sytuacji. –

1

Być może coś takiego (brzydki, ale może pracować)

private void isRunningOn(String thatServerName) { 

    String uniqueClassName = getSpecialClassNameFor(thatServerName); 
    try { 
     Class.forName(uniqueClassName); 
    } catch (ClassNotFoudException cnfe) { 
     return false; 
    } 
    return true; 
} 

getSpecialClassNameFor metoda zwróci klasy, który jest unikalny dla każdego serwera aplikacji (i może powrócić nowych nazw klas, gdy serwery kolejne aplikacje dodana)

Potem go używać jak:

if(isRunningOn("JBoss")) { 
     createJBossStrategy....etcetc 
    } 
-1

czysty sposób, aby zrobić to W powinny być skonfigurowane detektory cyklu życia w wersji web.xml. Mogą one ustawić globalne flagi, jeśli chcesz. Na przykład możesz zdefiniować ServletContextListener w swojej metodzie web.xml iw metodzie contextInitialized, ustawić globalną flagę, którą uruchamiasz wewnątrz kontenera. Jeśli globalna flaga nie jest ustawiona, oznacza to, że nie używasz kontenera.

+0

Od kiedy flagi globalne są czyste? –

+2

Globalne flagi nie zawsze są złe. Zbyt duży stan globalny zazwyczaj wskazuje na zły projekt, ale jedna globalna flaga jest uzasadniona dla tego rodzaju informacji. Jeśli masz lepszy pomysł, zasugeruj to sam. – Eddie

1

Istnieje kilka sposobów rozwiązania tego problemu. Jednym z nich jest przekazanie obiektu Kontekstu do klasy, gdy jest ona poddawana testowi jednostkowemu. Jeśli nie możesz zmienić podpisu metody, zmodyfikuj tworzenie kontekstu kontekstowego na metodę chronioną i przetestuj podklasę, która zwraca wyszydzony obiekt kontekstu, zastępując metodę. To może przynajmniej postawić klasę w teście, abyś mógł tam znaleźć lepsze alternatywy.

Następną opcją jest sprawienie, aby połączenia z bazą danych były fabryczne, które mogą stwierdzić, czy znajdują się w kontenerze, czy też nie, i za każdym razem postępuj właściwie.

Jedną z rzeczy do przemyślenia jest - kiedy już to połączenie z bazą danych zniknie z kontenera, co zamierzasz z nim zrobić? Jest to łatwiejsze, ale nie jest to test jednostkowy, jeśli musisz przenieść całą warstwę dostępu do danych.

Aby uzyskać dalszą pomoc w tym kierunku przenoszenia starszego kodu w ramach testu jednostkowego, sugeruję, aby spojrzeć na numer Working Effectively with Legacy Code Michaela Feathera.

5

Cała koncepcja jest z powrotem na pierwszym planie. Kod niższego poziomu nie powinien wykonywać tego rodzaju testów. Jeśli potrzebujesz innej implementacji, przepuść ją w odpowiednim miejscu.

+0

Być może próbuje ustalić, czy na początku potrzebuje innej implementacji. – OscarRyz

+0

Następnie musi umieścić to wyżej. –

4

Pewna kombinacja wtrysku zależności (czy to przez sprężynę, pliki konfiguracyjne lub argumenty programu), jak i wzorzec fabryczny zazwyczaj działają najlepiej.

Jako przykład przekazuję argument do moich skryptów Ant, które konfigurują pliki konfiguracyjne w zależności od tego, czy ucho lub wojna przechodzi w środowisko programistyczne, testowe lub produkcyjne.

+0

+1 dla DI, to podejście pozwala na łatwe przekazywanie pozorowanych obiektów w –

+0

Uzgodnione. Jeśli chcesz zachować prostotę, możesz po prostu użyć wzorca strategicznego, aby uzyskać źródło danych w czasie wykonywania. Plik konfiguracyjny określałby, która strategia ma być używana w oparciu o środowisko, podobnie jak sugerowana opcja w odpowiedzi. Będziesz potrzebował oczywiście źródła danych dev, ale to jest całkiem proste do napisania. – Robin

1
Context ctx = new InitialContext(); 
DataSource dataSource = (DataSource) ctx.lookup(jndiName); 

Kto konstruuje InitialContext? Jego konstrukcja musi znajdować się poza kodem, który próbujesz przetestować, inaczej nie będziesz mógł kpić z kontekstu.

Ponieważ powiedziałeś, że pracujesz nad starszą aplikacją, najpierw zmień kod, aby móc łatwo uzależnić wstrzyknięcie kontekstu lub źródła danych od klasy. Wtedy łatwiej będzie pisać testy dla tej klasy.

Możesz przenieść starszy kod, mając dwa konstruktory, tak jak w poniższym kodzie, dopóki nie przebudujesz kodu, który tworzy klasę. W ten sposób łatwiej można przetestować Foo i zachować kod, który używa Foo niezmieniony. Następnie możesz powoli refaktoryzować kod, tak aby stary konstruktor został całkowicie usunięty i wszystkie zależności są zależne od wstrzykniętej zależności.

public class Foo { 
    private final DataSource dataSource; 
    public Foo() { // production code calls this - no changes needed to callers 
    Context ctx = new InitialContext(); 
    this.dataSource = (DataSource) ctx.lookup(jndiName); 
    } 
    public Foo(DataSource dataSource) { // test code calls this 
    this.dataSource = dataSource; 
    } 
    // methods that use dataSource 
} 

Ale zanim zaczniesz robić to refaktoryzacja, powinieneś mieć kilka testów integracyjnych, aby pokryć plecy. W przeciwnym razie nie będziesz wiedział, czy nawet proste refaktoryzacje, takie jak przeniesienie wyszukiwania DataSource do konstruktora, coś zepsują. Następnie, gdy kod staje się lepszy, bardziej sprawdzalny, możesz pisać testy jednostkowe. (Z definicji, jeśli test dotyka systemu plików, sieci lub bazy danych, nie jest to test jednostkowy - jest to test integracji.)

Zaletą testów jednostkowych jest to, że działają szybko - setki lub tysiące na sekundę - i są bardzo skoncentrowane na testowaniu tylko jednego zachowania na raz. To sprawia, że ​​jest to możliwe, więc często (jeśli wahają się Państwo uruchomić wszystkie testy jednostkowe po zmianie jednej linii, biegną zbyt wolno), aby uzyskać szybką informację zwrotną. A ponieważ są bardzo skoncentrowani, będziesz wiedział, po prostu patrząc na nazwę testu awarii, gdzie dokładnie w kodzie produkcyjnym jest błąd.

Zaletą testów integracyjnych jest to, że upewniają się, że wszystkie części są prawidłowo połączone. Jest to również ważne, ale nie można ich często uruchamiać, ponieważ takie rzeczy jak dotykanie bazy danych powodują, że są one bardzo powolne. Ale nadal powinno się je uruchamiać co najmniej raz dziennie na serwerze ciągłej integracji.