Ostatni swój projekt realizuje w oparciu o platformę OSGI. Tym razem nie korzystałem z mavena, pax-* i tym podobnych narzędzi - skorzystałem z możliwości tworzenia pakunków jako projektów plug-in'ów eclipse (wskazując jako "Target Platform" Osgi framework zamiast Eclipse).
Utworzyłem pakunki, zacząłem je konfigurować i stwierdziłem że potrzebuję testów jednostkowych (zamiast sprawdzać czy aplikacja działa po prostu uruchamiając ją wolę pisać testy jednostkowe). I w tym miejscu pojawił się problem związany z platformą OSGI. Umieszczanie kodu testowego w tym samym projekcie w którym znajduje się kod aplikacji nie wydaje się ładnym rozwiązaniem. W dodatku moje pakunki byłyby zależne od bibliotek takich jak JUnit czy Mockito... Brrr... Naturalnym było więc utworzenie oddzielnych projektów zawierających testy dla każdego z pakunków. W ten sposób trafiłem z deszczu pod rynnę, gdyż umieszczając testy w oddzielnym pakunku mam dostęp jedynie do wyeksportowanych pakietów, czyli w przypadku gdy postępuję zgodnie ze sztuką (a postępuję ;) ) - samych interfejsów. W ten sposób testy jednostkowe przestają być jednostkowe. :|
Na pierwszy rzut oka sytuacja patowa. Na szczęście udało mi się znaleźć rozwiązanie. Polega ono na umieszczeniu kodu testowego w "Project Fragment" - specjalnym rodzaju projektu plug-in'u pozwalającego na jego dekompozycję. Tak więc mając projekt (typu "Plug-in Project") o nazwie abc zawierający pakunek tworzę nowy projekt (typu "Fragment Project") o nazwie abc.test wskazując jako Host Plug-in projekt abc. W ten sposób projekt abc.test posiada pełen dostęp do pakunku abc (włącznie z niewyeksportowanymi pakietami), a zarazem pozwala na wydzielenie testów (i ich zależności) z pakunku zawierającego logikę aplikacji.
poniedziałek, 21 grudnia 2009
środa, 9 grudnia 2009
Testowanie aplikacji Swing
Ostatnio postanowiłem uruchomić testy funkcjonalne dużej aplikacji napisanej w Swingu. Założenie było takie, aby testy naśladowały interakcję prawdziwego użytkownika.
Biblioteka Fest-Swing okazała się być idealna do tego celu. Co takiego umożliwia? Moim zdaniem dwie najważniejsze rzeczy to:
Najpierw uruchamiamy aplikację:
Najpierw tworzymy instancję klasy org.fest.swing.core.Robot - jest to wrapper na java.awt.Robot, gdyż to za jej pomocą odbywa się interakcja z aplikacją.
Następnie przy pomocy metody org.fest.swing.launcher.ApplicationLauncher.application tworzymy obiekt ApplicationLauncher jako parametr wskazując nazwę klasy, w której znajduje się metoda main naszej aplikacji, a następnie uruchamiamy go metodą start.
Następnie odnajdujemy obiekt klasy javax.swing.JFrame. Robimy to przy pomocy metody org.fest.swing.finder.WindowFinder.findFrame, jako argument przekazując instancję klasy org.fest.swing.core.GenericTypeMatcher, w której możemy określić dodatkowe warunki jakie ma spełniać obiekt JFrame.
Warto wspomnieć o klasach *Fixture - są to wrappery na standardowe elementy interfejsu graficznego takie jak przyciski, checkboxy, tabele, listy... to właśnie za ich pomocą możemy podejmować interakcję z komponentem (przykład to metoda click() w klasie ButtonFixture) lub wyszukiwać komponenty zawarte w kontenerze (przykladowo na obiekcie mainFrame możemy wywołać metode comboBox() w celu pobrania obiektu ComboboxFixture.
Jest tylko jedna uwaga: metoda wyszukująca obiekt musi odnaleść dokładnie jeden rezultat. W sytuacji, gdy kontener zabierałby więcej comboboxów, mysiałby zostać wybrany jeden z nich np. przy pomocy obiektu typu GenericTypeMatcher.
Biblioteka Fest-Swing okazała się być idealna do tego celu. Co takiego umożliwia? Moim zdaniem dwie najważniejsze rzeczy to:
- łatwe przeszukiwanie drzewa komponentów w celu znalezienia tego z którym chcemy podjąć interakcję
- możliwość symulowania akcji użytkownika
Najpierw uruchamiamy aplikację:
Robot robot = BasicRobot.robotWithCurrentAwtHierarchy();
application("pakiet.aplikacji.Main").start();
FrameFixture mainFrame = findFrame(new GenericTypeMatcher(JFrame.class) {
protected boolean isMatching(JFrame frame) {
return frame.getTitle().startsWith("Tytul aplikacji")
&& frame.isShowing();
}
}).using(robot);
Najpierw tworzymy instancję klasy org.fest.swing.core.Robot - jest to wrapper na java.awt.Robot, gdyż to za jej pomocą odbywa się interakcja z aplikacją.
Następnie przy pomocy metody org.fest.swing.launcher.ApplicationLauncher.application tworzymy obiekt ApplicationLauncher jako parametr wskazując nazwę klasy, w której znajduje się metoda main naszej aplikacji, a następnie uruchamiamy go metodą start.
Następnie odnajdujemy obiekt klasy javax.swing.JFrame. Robimy to przy pomocy metody org.fest.swing.finder.WindowFinder.findFrame, jako argument przekazując instancję klasy org.fest.swing.core.GenericTypeMatcher, w której możemy określić dodatkowe warunki jakie ma spełniać obiekt JFrame.
Warto wspomnieć o klasach *Fixture - są to wrappery na standardowe elementy interfejsu graficznego takie jak przyciski, checkboxy, tabele, listy... to właśnie za ich pomocą możemy podejmować interakcję z komponentem (przykład to metoda click() w klasie ButtonFixture) lub wyszukiwać komponenty zawarte w kontenerze (przykladowo na obiekcie mainFrame możemy wywołać metode comboBox() w celu pobrania obiektu ComboboxFixture.
Jest tylko jedna uwaga: metoda wyszukująca obiekt musi odnaleść dokładnie jeden rezultat. W sytuacji, gdy kontener zabierałby więcej comboboxów, mysiałby zostać wybrany jeden z nich np. przy pomocy obiektu typu GenericTypeMatcher.
czwartek, 3 grudnia 2009
Zmiękczanie wyjatków za pomocą AspectJ
Kolejną ciekawą możliwością jaką daje AspectJ jest automatyczna zamiana twardych (checked) wyjątków na miękkie (unchecked) - rozszerzających klasę RuntimeException.
Czemu ma służyć taki manewr? Oczywiście wygodzie programisty. ;) Czasem zdarza się, że pomiędzy miejscem powstania wyjątku, a miejscem jego obsługi jest po drodze wywoływanych wiele metod. Teraz jeśli rzucanym wyjątkiem jest wyjątek twardy to każda metoda po drodze musi zadeklarować ze może ewentualnie taki wyjątek rzucić.
Jeśli pisanie deklaracji throws jest dla nas uciążliwe możemy obejść problem opakowywując twardy wyjątek w jego miękki odpowiednik.
Kod w działaniu zachowuje się tak samo, ale metody test oraz test2 nie muszą deklarować NoSuchMethodException. W każdym razie programista musi ręcznie opakować wyjątek. I tutaj na scenę wkracza AspectJ. Posiada on deklarację
Natomiast klasa Wyjatki prezentuje się następująco:
Jak widać nie dość, że pozbyliśmy się deklaracji throws to jeszcze wyeliminowaliśmy opakowywanie NoSuchMethodException w metodzie test.
Na koniec dosyć ważna uwaga: twarde wyjątki po istnieją, aby były jawnie obsługiwane, dlatego ten mechanizm nie służy temu aby opakować wyjątek i o nim zapomnieć. Trzeba pamietać, aby go właściwie obsłużyć.
Czemu ma służyć taki manewr? Oczywiście wygodzie programisty. ;) Czasem zdarza się, że pomiędzy miejscem powstania wyjątku, a miejscem jego obsługi jest po drodze wywoływanych wiele metod. Teraz jeśli rzucanym wyjątkiem jest wyjątek twardy to każda metoda po drodze musi zadeklarować ze może ewentualnie taki wyjątek rzucić.
package wyjatki;
public class Wyjatki {
static void test() throws NoSuchMethodException{
Wyjatki.class.getMethod("abc", String.class);
}
static void test2() throws NoSuchMethodException{
test();
}
public static void main(String[] args) {
try {
test2();
} catch (NoSuchMethodException e) {
//obsluga wyjatku
e.printStackTrace();
}
}
}
Jeśli pisanie deklaracji throws jest dla nas uciążliwe możemy obejść problem opakowywując twardy wyjątek w jego miękki odpowiednik.
package wyjatki;
public class Wyjatki {
static void test(){
try{
Wyjatki.class.getMethod("abc", String.class);
}catch(NoSuchMethodException e){
throw new RuntimeException(e);
}
}
static void test2(){
test();
}
public static void main(String[] args) {
try {
test2();
} catch (RuntimeException e) {
Throwable e2 = e.getCause();
//obsluga wyjatku
e2.printStackTrace();
}
}
}
Kod w działaniu zachowuje się tak samo, ale metody test oraz test2 nie muszą deklarować NoSuchMethodException. W każdym razie programista musi ręcznie opakować wyjątek. I tutaj na scenę wkracza AspectJ. Posiada on deklarację
declare soft, która odpowiada za opakowanie twardego wyjątku jego miękkim odpowiednikiem. Dla naszego przykładu wygląda ona następująco:
package wyjatki;
public aspect WyjatkiAspect {
declare soft : NoSuchMethodException : execution(void wyjatki.Wyjatki.test());
}
Natomiast klasa Wyjatki prezentuje się następująco:
package wyjatki;
public class Wyjatki {
static void test(){
Wyjatki.class.getMethod("abc", String.class);
}
static void test2(){
test();
}
public static void main(String[] args) {
try {
test2();
} catch (RuntimeException e) {
Throwable e2 = e.getCause();
//obsluga wyjatku
e2.printStackTrace();
}
}
}
Jak widać nie dość, że pozbyliśmy się deklaracji throws to jeszcze wyeliminowaliśmy opakowywanie NoSuchMethodException w metodzie test.
Na koniec dosyć ważna uwaga: twarde wyjątki po istnieją, aby były jawnie obsługiwane, dlatego ten mechanizm nie służy temu aby opakować wyjątek i o nim zapomnieć. Trzeba pamietać, aby go właściwie obsłużyć.
środa, 2 grudnia 2009
Static crosscutting w AspectJ
Każdy kto cokolwiek słyszał o AspectJ wie, że można za jego pomocą modyfikować wywołania metod (dynamic crosscutting), jednak mało kto wie, że za jego pomocą można także modyfikować statyczne właściwości klas, np dodawać atrybuty lub metody. To jest właśnie static crosscutting. Jak wykorzystać static crosscutting zaprezentuję na prostym przykładzie.
Tak więc mamy klasę reprezentująca wpis w serwisie blogowym.
Dla celów demonstracyjnych utworzyliśmy także klasę Test demonstrującą użycie klasy Wpis.
Załóżmy teraz, że chcemy dodać do niej atrybut reprezentujący datę utworzenia, automatycznie inicjowany bieżącym czasem.
Najpierw definiujemy nowy atrybut w klasie Wpis - jak widać nie różni się to od zwykłego utworzenia atrybutu, z wyjątkiem tego, że nazwa atrybutu zawiera też nazwę typu w którym będzie zdefiniowany. Następnie do klasy Wpis dodajemy metodę umożliwiającą pobranie wartości nowo-utworzonego atrybutu. Dokonujemy też modyfikacji wszystkich konstruktorów, tak by go odpowiednio inicjowały. Ostatnią modyfikacją jest "nadpisanie" metody toString() własną implementacją, która uwzględnia atrybut dataUtworzenia.
Oto rezultat uruchomienia klasy test:
Jak widać wszystko działa zgodnie z naszymi zamierzeniami.
Do czego można wykorzystać static crosscutting?
Pierwszą rzeczą jaka się nasuwa w odpowiedzi jest możliwość modyfikacji kodu do którego nie posiadamy źródeł. W takiej sytuacji możemy dokonać modyfikacji w kodzie bez nieetycznej dekompilacji.
Drugim, chyba sensowniejszym przykładem jego użycia będzie dodanie do klasy elementów, które chcemy mieć, ale które są typowo technicznymi jej składnikami, np logger, czy połączenie z bazą danych.
Tak więc mamy klasę reprezentująca wpis w serwisie blogowym.
package blog;
import java.util.ArrayList;
import java.util.List;
public class Wpis {
private String tresc;
private Listkomentarze = new ArrayList ();
public Wpis() {
}
Wpis(String tresc){
this.tresc = tresc;
}
public String getTresc() {
return tresc;
}
public void setTresc(String tresc) {
this.tresc = tresc;
}
public ListgetKomentarze() {
return komentarze;
}
public void setKomentarze(Listkomentarze) {
this.komentarze = komentarze;
}
public void addKomentarz(String komentarz){
this.komentarze.add(komentarz);
}
@Override
public String toString() {
return "Wpis o tresci: " + tresc;
}
}
Dla celów demonstracyjnych utworzyliśmy także klasę Test demonstrującą użycie klasy Wpis.
package blog;
public class Test {
public static void main(String[] args) {
Wpis w = new Wpis("Hello world");
System.out.println(w.getTresc());
System.out.println(w.toString());
}
}
Załóżmy teraz, że chcemy dodać do niej atrybut reprezentujący datę utworzenia, automatycznie inicjowany bieżącym czasem.
package blog;
import java.text.MessageFormat;
import java.util.Date;
public aspect WpisAspect {
private Date blog.Wpis.dataUtworzenia;
public Date blog.Wpis.getDataUtworzenia(){
return this.dataUtworzenia;
}
after(Wpis wpis) returning() : execution(blog.Wpis.new(..)) && target(wpis) {
wpis.dataUtworzenia = new Date();
}
String around(Wpis wpis) : execution(public String blog.Wpis.toString()) && target(wpis) {
return MessageFormat.format("Wpis z dnia {0} o tresci: {1}", wpis.getDataUtworzenia(), wpis.getTresc());
}
}
Najpierw definiujemy nowy atrybut w klasie Wpis - jak widać nie różni się to od zwykłego utworzenia atrybutu, z wyjątkiem tego, że nazwa atrybutu zawiera też nazwę typu w którym będzie zdefiniowany. Następnie do klasy Wpis dodajemy metodę umożliwiającą pobranie wartości nowo-utworzonego atrybutu. Dokonujemy też modyfikacji wszystkich konstruktorów, tak by go odpowiednio inicjowały. Ostatnią modyfikacją jest "nadpisanie" metody toString() własną implementacją, która uwzględnia atrybut dataUtworzenia.
Oto rezultat uruchomienia klasy test:
Hello world
Wpis z dnia 02.12.09 19:33 o tresci: Hello world
Jak widać wszystko działa zgodnie z naszymi zamierzeniami.
Do czego można wykorzystać static crosscutting?
Pierwszą rzeczą jaka się nasuwa w odpowiedzi jest możliwość modyfikacji kodu do którego nie posiadamy źródeł. W takiej sytuacji możemy dokonać modyfikacji w kodzie bez nieetycznej dekompilacji.
Drugim, chyba sensowniejszym przykładem jego użycia będzie dodanie do klasy elementów, które chcemy mieć, ale które są typowo technicznymi jej składnikami, np logger, czy połączenie z bazą danych.
Subskrybuj:
Posty (Atom)