Integrationstests mit Spring

Integrationstests testen im Gegensatz zu Unittests nicht einzelne Methoden, sondern das Zusammenspiel verschiedener Methoden, die von einander abhängig sind. Das Framework Spring bietet hierfür viele verschiedene Funktionalitäten. Voraussetzung ist eine korrekte Injizierung der einzelnen Klassen in einer Webanwendung, wie ich es im Kapitel Dependency Injection for Beginners beschrieben habe. Außerdem benötigen wir eine separate applicationContextTest.xml wie sie im Kapitel Konfiguration in Spring mit der applicationContext.xml zu finden ist.

Für den Fall, dass Sie noch nicht die Testbibliothek von Spring zu Ihren Maven-Dependencies hinzugefügt haben, holen wir dies jetzt nach:

<dependency>
           <groupId>org.springframework</groupId>
           <artifactId>spring-test</artifactId>   
</dependency>

Da das Testen auf dem JUnit-Framework basiert, benötigen wir auch diese Dependency:

<dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.5</version>
            <scope>test</scope>
</dependency>

Wie kann man in Spring Tests verwenden? Man braucht einen Hinweis auf den SpringJUnit4ClassRunner und auf die Konfigurationsdatei. Die Klasse, die Tests enthält, muss wie folgt annotiert werden:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "classpath:applicationContextTest.xml" })

Im Folgenden möchte ich Tests durchführen, die Testdaten nur für die Zeit, während der Test läuft, einlesen. Am Ende werden die Inserts wieder zurückgerollt. Sprich die Testdaten werden nicht dauerhaft in der Datenbank gespeichert. Um einen Rollback am Ende eines Tests realisieren zu können, benötigen wir die unten stehenden Annotations für die Klasse:

@TransactionConfiguration(transactionManager = "transactionManager", defaultRollback = false)
@Transactional

Außerdem muss jeder einzelne Test noch zusätzlich annotiert werden:

@Test
@Rollback(true)

Tests auf Service-Ebene

Lassen Sie uns mit unseren Datenbank- und Integrationstests auf Ebene der Service-Klasse beginnen. Hier unsere ersten zwei Tests:

import static org.junit.Assert.*;

import org.junit.Test;

import org.junit.runner.RunWith;

import org.springframework.test.annotation.Rollback;

import org.springframework.test.context.ContextConfiguration;

import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import org.springframework.test.context.transaction.TransactionConfiguration;

import org.springframework.transaction.annotation.Transactional;



@RunWith(SpringJUnit4ClassRunner.class)

@ContextConfiguration(locations = { "classpath:applicationContextTest.xml" })

@TransactionConfiguration(transactionManager = "transactionManager", defaultRollback = false)

@Transactional

public class FreigabeVerguetungServiceImplTest extends FreigabeVerguetungTest {



    @Test

    @Rollback(true)

    public void testFreigabeVergutungSetNextProcess(){



        freigabeVerguetungService.save(setFreigabeVerguetungWithOutF("BO", "VWBO00"));

        FreigabeVerguetungBODB2 freigabeAusDatenbank

                         = freigabeVerguetungService.getFreigabeVerguetungBODB2ById(getTimestamp());

        freigabeAusDatenbank.setNextProcess("F");

        freigabeVerguetungService.update(freigabeAusDatenbank);

        FreigabeVerguetungBODB2 freigabeAusDatenbankMitNextProcess

                         = freigabeVerguetungService.getFreigabeVerguetungBODB2ById(getTimestamp());

        assertEquals("F", freigabeAusDatenbankMitNextProcess.getNextProcess());

    }


    @Test

    @Rollback(true)

    public void testFreigabeVergutungRemoveNextProcess(){



        freigabeVerguetungService.save(setFreigabeVerguetungWithF("BO", "VWBO00"));

        FreigabeVerguetungBODB2 freigabeAusDatenbank
                           = freigabeVerguetungService.getFreigabeVerguetungBODB2ById(getTimestamp());

        freigabeAusDatenbank.setNextProcess(" ");

        freigabeVerguetungService.update(freigabeAusDatenbank);

        FreigabeVerguetungBODB2 freigabeAusDatenbankOhneNextProcess

                           = freigabeVerguetungService.getFreigabeVerguetungBODB2ById(getTimestamp());

        assertEquals(" ", freigabeAusDatenbankOhneNextProcess.getNextProcess());
    }
 }

Wir aktualisieren jeweils das Feld nextProcess aus der Datenbank und überprüfen anschließend, ob diese Änderung auch durchgeführt worden ist. Die Funktion assertEquals() stellt fest, ob das Feld nextProcess nach dem Update dem Wert  ” ” oder „F” entspricht.

Die Methoden, die die Testdaten erstellen, habe ich in einer Superklasse ausgelagert.  Ich benötige sie auch als Testdaten für eine weitere Testklasse weiter unten.

public class FreigabeVerguetungTest {

    static Logger logger = Logger.getRootLogger();

    @Autowired

    FreigabeVerguetungService freigabeVerguetungService;

    @Autowired

    FreigabeVerguetungController freigabeVerguetungController;


    public FreigabeVerguetungBODB2 setFreigabeVerguetungWithOutF(String actionType, String action){

        FreigabeVerguetungBODB2 freigabeVerguetung = new FreigabeVerguetungBODB2();

        freigabeVerguetung.setFreigabeVerguetungId(getTimestamp());

        freigabeVerguetung.setActionType(actionType);

        freigabeVerguetung.setAction(action);

        freigabeVerguetung.setBookingDate(getDate());

        freigabeVerguetung.setVoucherFirst(100);

        freigabeVerguetung.setVoucherLast(200);

        freigabeVerguetung.setFreigabeStatus(" ");

        freigabeVerguetung.setFreigabeStatusDatum(getDate());

        freigabeVerguetung.setNextProcess(" ");

        freigabeVerguetung.setModTime(getTimestamp());

        freigabeVerguetung.setModUser("eexbre1");

        freigabeVerguetung.setModTrans("WEB");

        return freigabeVerguetung;

    }



    public FreigabeVerguetungBODB2 setFreigabeVerguetungWithF(String actionType, String action){

        FreigabeVerguetungBODB2 freigabeVerguetung = new FreigabeVerguetungBODB2();

        freigabeVerguetung.setFreigabeVerguetungId(getTimestamp());

        freigabeVerguetung.setActionType(actionType);

        freigabeVerguetung.setAction(action);

        freigabeVerguetung.setBookingDate(getDate());

        freigabeVerguetung.setVoucherFirst(100);

        freigabeVerguetung.setVoucherLast(200);

        freigabeVerguetung.setFreigabeStatus(" ");

        freigabeVerguetung.setFreigabeStatusDatum(getDate());

        freigabeVerguetung.setNextProcess("F");

        freigabeVerguetung.setModTime(getTimestamp());

        freigabeVerguetung.setModUser("eexbre1");

        freigabeVerguetung.setModTrans("WEB");

        return freigabeVerguetung;

    }

    public Timestamp getTimestamp() {

        DateTestHelper dateTestHelper = new DateTestHelper();

        Timestamp ts = dateTestHelper.getTimestamp();

        return ts;

    }

    public Date getDate() {

        DateTestHelper dateTestHelper = new DateTestHelper();

        Date date = dateTestHelper.getDate();

        return date;

    }
}

Ich erstelle 2 Testdatensätze, die sich durch den Freigabestatus unterscheiden. Das Feld, das in diesen Methoden verändert wird, ist das Feld nextProcess(). Steht in diesem Feld ein F wird eine Vergütung freigegeben. Steht dort ein Leerzeichen ist die Zahlung nicht freigegeben. Mit unseren obigen Tests überprüfen wir, ob dieser Wert mit der Methode update() gesetzt wird. Diese Tests betreffen nur eine Klasse und eine Datenbanktabelle. Als nächstes erstellen wir einen Test, der Veränderungen in mehreren Datenbanktabellen überprüft. Wir überprüfen, ob die Methode saveStatusAndFreigabeVerguetung() sowohl die Freigabe korrekt setzt als auch den Status in der Tabelle Bonus.

@Test

@Rollback(true)

public void testSaveStatusAndFreigabeVerguetungMitStatus6(){



       freigabeVerguetungService.save(setFreigabeVerguetungWithF("GS", "AUD011"));

       FreigabeVerguetungBODB2 freigabeVerguetungAusDatenbank

                         = freigabeVerguetungService.getFreigabeVerguetungBODB2ById(getTimestamp());

       setBonusArt();

       BonusBODB2 bonus = setBonus("6");

       freigabeVerguetungService.saveStatusAndFreigabeVerguetung("7", freigabeVerguetungAusDatenbank);

       FreigabeVerguetungBODB2 freigabeVerguetungMitStatus7

                         = freigabeVerguetungService.getFreigabeVerguetungBODB2ById(getTimestamp());

       BonusBODB2 bonusAusDatenbank
                         = freigabeVerguetungService.getBonusBODB2ById(bonus.getBonusId());


       assertEquals("F", freigabeVerguetungMitStatus7.getNextProcess());

       assertEquals("7", bonusAusDatenbank.getBonusStatus());

}

Die Klasse FreigabeVerguetungTest ergänze ich um die Methoden setBonus() und setBonusArt():

public BonusBODB2 setBonus(String bonus) {

       BonusBODB2 bonusBODB2 = new BonusBODB2();

       bonusBODB2.setBonusId(getTimestamp());

       bonusBODB2.setBonusSet("GKSTRA02");

       bonusBODB2.setBonusVariante(2);

       bonusBODB2.setBonusStatus(bonus);

       bonusBODB2.setDescription("Ina's Testbonus");

       bonusBODB2.setBonusDate(getDate());

       bonusBODB2.setDateControl("N1");

       bonusBODB2.setMonthFrom("201201");

       bonusBODB2.setMonthUntil("201212");

       bonusBODB2.setModTime(getTimestamp());

       bonusBODB2.setModUser("eexbre1");

       bonusBODB2.setModTrans("WEB");

       bonusBODB2.setSbVersion(1);

       bonusBODB2.setFinals("A");

       freigabeVerguetungService.save(bonusBODB2);

       return bonusBODB2;

   }



   public BonusArtBODB2 setBonusArt() {

       BonusArtBODB2 bonusArtBODB2 = new BonusArtBODB2();

       BonusArtBODB2Id bonusArtBODB2Id = new BonusArtBODB2Id();

       bonusArtBODB2Id.setBonusSet("GKSTRA02");

       bonusArtBODB2Id.setBonusVariante(2);

       bonusArtBODB2.setId(bonusArtBODB2Id);

       bonusArtBODB2.setActionType("GS");

       bonusArtBODB2.setAction("AUD011");

       bonusArtBODB2.setModTime(getTimestamp());

       bonusArtBODB2.setModUser("eexbre1");

       bonusArtBODB2.setModTrans("QMF");

       freigabeVerguetungService.save(bonusArtBODB2);

       return bonusArtBODB2;

   }

Tests auf Controller-Ebene

Bis hier hin war alles einfach. Jetzt haben wir mehrere Probleme, die es zu lösen gilt: Im nächsten Schritt würde ein Servlet-Container (z.B. Tomcat) benötigt werden, der den Faces-Context mit hoch lädt. Der wird aber leider an dieser Stelle nicht mit gestartet. Die Lösungsansätze hierzu scheinen im Fluß zu sein. Außerdem benötigen wir einen CustomScope.

Unser FreigabeVerguetungController ist mit der Annotation @Scope(value=”session”) versehen. Diese Annotation führt im Test zu einer Fehlermeldung. Lösung  ist ein CustomScope. Für den Dummy-CustomScope benötigen wir unten stehende Klasse, die in der applicationContextTest.xml registriert werden muss (Konfiguration in Spring mit der applicationContext.xml).

import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.config.Scope;

public class CustomScope implements Scope {

    private final Map<String , Object> beanMap = new HashMap<String , Object>();

    public Object get(String name, ObjectFactory<?> factory) {

        Object bean = beanMap.get(name);

        if (null == bean) {

            bean = factory.getObject();

            beanMap.put(name, bean);

        }

        return bean;

    }

    public String getConversationId() {

        // not needed

        return null;

    }

    public void registerDestructionCallback(String arg0, Runnable arg1) {

        // not needed

    }

    public Object remove(String obj) {

        return beanMap.remove(obj);

    }

    public Object resolveContextualObject(String arg0) {

        // not needed

        return null;

    }
}

 

Ein weiteres Problem: Im Test gibt es weder einen SessionContext noch einen FacesContext. So wirft die Methode getSessionBean() aus der Klasse BeanFassade eine NullPointerException.

public static SessionBean getSessionBean() {



       FacesContext context = FacesContext.getCurrentInstance();

       ValueBinding binding = context.getApplication().createValueBinding("#{sessionBean}");

       return (SessionBean) binding.getValue(context);

}

Diese NullPointerException müssen wir in der Klasse FreigabeVerguetungController auffangen. Wir erstellen unten stehende Methode init() und eine Methode initForTest(). Für den Test ersetzen wir die SessionBean durch eine Art Mockobjekt. Sprich wir erstellen im Test selbst ein SessionBean-Objekt, dem wir Werte nur für den Test zuweisen. Zu Testzwecken instanziieren wir einen Dummy. Aber dazu weiter unten mehr.

private SessionBean sessionBean;

@PostConstruct

public void init() {
     try {
          sessionBean = BeanFassade.getSessionBean();
     } catch (NullPointerException e) {
          getSessionBean();
     }
}



public void initForTest(SessionBean sessionBean) {
     this.sessionBean = sessionBean;

}

public SessionBean getSessionBean() {
     return sessionBean;
}

Die Annotation @PostConstruct funktioniert leider nicht immer. Wollen wir beispielsweise auf eine Properties-Datei zugreifen mit unten stehender Methode getRessourceBundle(), muss dies an jeder Stelle separat erfolgen.Wozu dient die Annotation @PostConstruct? Die Methode init() wird direkt nach dem Konstruktor aufgerufen, so müssen wir diese Methode nur einmal aufrufen und nicht an allen Stellen, an denen sie benötigt wird. Warum greifen wir nicht im Konstruktor auf die SessionBean zu? Weil eine Fehlermeldung geworfen werden würde.

public static ResourceBundle getRessourceBundle(String bundle) {      

           FacesContext ctx = FacesContext.getCurrentInstance();      

           ResourceBundle resourceBundle =           

                         ResourceBundle.getBundle(bundle, ctx.getViewRoot().getLocale());      

           return resourceBundle;  

}

Wir erstellen die folgenden Methoden in der Klasse ApplyAllowanceServiceImpl.java, die die Bezeichnung für eine Belegnummer aus einer Datei auslesen. Die Methode muss dann jedes Mal im Code aufgerufen werden, wenn sie benötigt wird.

public void initVoucherNo(){       

     try{           
          voucherNo =
             FacesUtil.getRessourceBundle(GlobalConstants.BUNDLE_MESSAGES).getString("notVoucherNo");

     }catch(NullPointerException e){           
          getVoucherNo();       
     }  
}       



public String getVoucherNo() {       
     return voucherNo;   
}   



public void setVoucherNo(String voucherNo) {       
     this.voucherNo = voucherNo;   
}

Kommen wir zu unserem Test: Die Testdaten, die in den Methoden der Klasse FreigabeVerguetungTest erzeugt werden, können wir wieder verwenden. Wir instanziieren die SessionBean, übergeben ihr einen Dealer, der eine Marke und eine ID hat, und setzen diesen Wert an die Stelle, an der die Daten des Händlers durch das Händlerportal übergeben werden würden, wäre dies eine „normale Session”.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "classpath:applicationContextTest.xml" })
@TransactionConfiguration(transactionManager = "transactionManager", defaultRollback = true)
@Transactional

public class FreigabeVerguetungControllerTest extends FreigabeVerguetungTest{

    static Logger logger = Logger.getRootLogger();
    @Autowired
    FreigabeVerguetungService freigabeVerguetungService;



    @Autowired

    FreigabeVerguetungController freigabeVerguetungController;

    @Test
    @Rollback(true)
    public void testSaveActionGrossKunde(){

        //Da der FacesContext im Test nicht existiert, erstellen wir eine SessionBean

        //nur für den Test.

        SessionBean sessionBean = new SessionBean();

        Dealer dealer = new Dealer();

        dealer.setOuBrandsAsString("V");

        dealer.setUserHostID("eexbre1");

        sessionBean.setDealer(dealer);

        freigabeVerguetungController.initForTest(sessionBean);



        FreigabeVerguetungBODB2 freigabeVerguetungInDatenbank

             = setFreigabeVerguetungWithOutF("GS", "AUD011");

        freigabeVerguetungService.save(freigabeVerguetungInDatenbank);

        FreigabeVerguetungBODB2 freigabeVerguetungAusDatenbank

             = freigabeVerguetungService.getFreigabeVerguetungBODB2ById(getTimestamp());

        setBonusArt();

        BonusBODB2 bonus = setBonus("6");

        freigabeVerguetungController.saveActionGrossKunde

             (freigabeVerguetungController.

              setFromBeanToFreigabeVerguetungBO(freigabeVerguetungAusDatenbank));

        FreigabeVerguetungBODB2 freigabeVerguetungMitStatus7

             = freigabeVerguetungService.getFreigabeVerguetungBODB2ById(getTimestamp());

        BonusBODB2 bonusAusDatenbank = freigabeVerguetungService.getBonusBODB2ById(bonus.getBonusId());


        assertEquals("F", freigabeVerguetungMitStatus7.getNextProcess());
        assertEquals("7", bonusAusDatenbank.getBonusStatus());
    }
}

Oben stehende Ergänzung durch die drei Methoden init(), initForTest() und getSessionBean() verändert den Code sehr. Ziel ist es, den Code möglichst nur geringfügig zu Testzwecken anzupassen. Je mehr ein Code verändert wird, desto größer ist die Gefahr zusätzliche Fehler einzubauen. Leider habe ich in Spring keinen Weg gefunden wie dies möglich wäre. Diese Methoden werden auch im folgenden benötigt. Ich möchte trotzdem zwei weitere Wege vorstellen: Der Einsatz von Reflections und der Einsatz von Anonymen Klassen. Als Erstes überschreiben wir die Methode init() durch eine Anonyme Klasse. Was ist eine anonyme Klasse? Bei anonymen Klassen werden zwei Schritte in einem durchgeführt: Es wird eine Klasse ohne Namen definiert und gleichzeitig eine Instanz dieser Klasse erstellt. Wir setzen den Wert der SessionBean in unserem Test wie folgt:Da wir sicherlich noch mehr Tests erstellen, für die wir eine SessionBean mit Dealer benötigen, würde es sich anbieten diese Funktionalität als Methode ebenfalls in die Superklasse auszulagern.

FreigabeVerguetungController freigabe = new FreigabeVerguetungController() {

           public void init() {
               SessionBean sessionBean = new SessionBean();
               Dealer dealer = new Dealer();
               dealer.setOuBrandsAsString("V");
               dealer.setUserHostID("eexbre1");
               sessionBean.setDealer(dealer);
           }
};//Die anonyme Klasse wird durch einen Strichpunkt beendet

Ich habe den entsprechende Test der Klasse FreigabeVerguetungControllerTest.java hinzugefügt:

@Test
 @Rollback(true)
 public void testSaveActionGrossKundeMitAnonymerKlasse() {
        //Da der FacesContext im Test nicht existiert, erstellen wir eine
        //SessionBean in einer anonymen Klasse nur für den Test.
        FreigabeVerguetungController freigabe = new FreigabeVerguetungController() {

            public void init() {

                SessionBean sessionBean = new SessionBean();

                Dealer dealer = new Dealer();

                dealer.setOuBrandsAsString("V");

                dealer.setUserHostID("eexbre1");

                sessionBean.setDealer(dealer);

            }

        };



        FreigabeVerguetungBODB2 freigabeVerguetungInDatenbank = setFreigabeVerguetungWithOutF(

                "GS", "AUD011");

        freigabeVerguetungService.save(freigabeVerguetungInDatenbank);

        FreigabeVerguetungBODB2 freigabeVerguetungAusDatenbank = freigabeVerguetungService

                .getFreigabeVerguetungBODB2ById(getTimestamp());

        setBonusArt();

        BonusBODB2 bonus = setBonus("6");

        freigabeVerguetungController

                .saveActionGrossKunde(freigabeVerguetungController

                        .setFromBeanToFreigabeVerguetungBO(freigabeVerguetungAusDatenbank));

        FreigabeVerguetungBODB2 freigabeVerguetungMitStatus7 = freigabeVerguetungService

                .getFreigabeVerguetungBODB2ById(getTimestamp());

        BonusBODB2 bonusAusDatenbank = freigabeVerguetungService

                .getBonusBODB2ById(bonus.getBonusId());

        assertEquals("F", freigabeVerguetungMitStatus7.getNextProcess());

        assertEquals("7", bonusAusDatenbank.getBonusStatus());

    }

}

Als Zweites setzen wir den Wert für die SessionBean mithilfe von Reflections. Eine schöne Seite zu diesem Thema ist die folgende: http://www.itblogging.de/java/java-reflection/. Die SessionBean wird von außen gesetzt, nachdem sie ansprechbar mit setAccessible(true) gemacht wurde. Es ist das gleiche Prinzip wie Dependency Injection.

//Die entsprechende Klasse wird instanziiert:
final FreigabeVerguetungController freigabe = new FreigabeVerguetungController();

//Die Klasse wird in ein Object vom Typ Class umgewandelt:
final Class<?> clazz = freigabe.getClass();

Field sessionBeanReflection = null;

try {          
       //Mit der Methode getDeclaredField() holen wir uns das Attribut
       //SessionBean der Klasse FreigabeVerguetungController:
        sessionBeanReflection = clazz.getDeclaredField("sessionBean");
} catch (SecurityException e) {

        e.printStackTrace();
} catch (NoSuchFieldException e) {

        e.printStackTrace();

}

SessionBean sessionBean = new SessionBean();
Dealer dealer = new Dealer();
dealer.setOuBrandsAsString("V");
dealer.setUserHostID("eexbre1");
sessionBean.setDealer(dealer);

//Wir verschaffen uns Zugriff auf das Attribut SessionBean
sessionBeanReflection.setAccessible(true);

try {          

        //und setzen den Wert dieses Attributs:

        sessionBeanReflection.set(freigabe, sessionBean);

} catch (IllegalArgumentException e) {

       e.printStackTrace();

} catch (IllegalAccessException e) {

       e.printStackTrace();
}

In den beiden soeben vorgestellten Lösungen wird die SessionBean von außen verändert, ohne dass der ursprüngliche Code wesentlich verändert werden muss.

Written by Tomasz Waszczyk

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *