Beispiel: Unit-Test


Inhalt:

JBoss-Test
Vorbereiten der Application
Anlegen des Application Clients
Vorbereitung des Tests
Erstellen eines Tests
Test ausführen
Unit-Test mit Security

Beispiel für einen Unit-Test.
Dieses Projekt baut auf dem KuchenZutatNM-Beispiel auf.
Hier gibt es das Projekt zum Download (dies ist ein EAR-Export, die Importanleitung findet man im Stateless-Beispiel): KuchenZutatNM.ear
Nach dem Import müssen die JBoss-Test-Libraries zum Classpath zugefügt werden.

JBoss-Test

Dieses Beispiel verwendet das JBoss-Test-Framework, dieses wiederum ist eine Spezialisierung des JUnit-Frameworks.
Es ist auf der JBoss-Homepage zu finden, oder auch gespiegelt hier:
jboss-test-1.0.0.CR1.zip.
Ein wenig Info findet sich in der Readme des Pakets, außerdem gibt es ein einfaches Beispiel. Im Jboss-Wiki gibt es auch noch einige weitere Infos: http://wiki.jboss.org/wiki/Wiki.jsp?page=HowToWriteAUnitTest.

Die Datei wird irgendwohin auf Festplatte entpackt.

In Eclipse müssen jetzt einige JARs bekannt gemacht werden. Statt die JAR-Dateien im Projekt absolut zu referenzieren bevorzuge ich den Weg über "Classpath-Variablen". Dazu gehen wir in die Eclipse-Preferences unter "Java" - "Build Path" - "Classpath Variables" und fügen folgende drei Variablen zu:
"jboss-test.jar":
jboss-test.jar
"junit.jar":
junit.jar
"log4j.jar":
log4j.jar


Vorbereiten der Application

Es wird die Enterprise Application "KuchenZutatNM" aus dem KuchenZutatNM-Beispiel verwendet. Dies wird in Eclipse importiert.

Am Code des EJB-Projekts sind folgende Änderungen vorzunehmen:
Ein Remote-Interface für den KuchenZutatNMWorker muss zugefügt werden:
@Remote()
public interface KuchenZutatNMWorker
{
  public void saveKuchen(KuchenNMBean kuchen);

  public List<KuchenNMBean> getKuchen();

  public KuchenNMBean findKuchenById(Integer int_Id);

  public void deleteKuchen(KuchenNMBean kuchen);

  public void addZutatToKuchen (KuchenNMBean kuchen, ZutatNMBean zutat);
  
  public void removeZutatFromKuchen (KuchenNMBean kuchen, ZutatNMBean zutat);
  
  public ZutatNMBean findZutatById(Integer int_Id);

  public void deleteZutat(ZutatNMBean zutat);

  public void saveZutat(ZutatNMBean zutat);

  public List<ZutatNMBean> getZutaten();
} 
Die Bean muss natürlich auch dieses Interface implementieren:
@Stateless
public class KuchenZutatNMWorkerBean implements KuchenZutatNMWorkerLocal, KuchenZutatNMWorker
{ 


Anlegen des Application Clients

Wir fügen der Enterprise Application ein Application Client-Projekt zu:
Application Client (1)
Das Projekt soll "KuchenZutatNMTestClient" heißen. Wichtig ist dass wir es zur Enterprise Application "KuchenZutatNM" zufügen:
Application Client (2)
Alle weiteren Einstellungen können wir auf dem Default lassen.

Jetzt den Deployment-Deskriptor "application-client.xml" auf JavaEE5 bringen.

Das Projekt muss in den "J2EE Module Dependencies" das EJB-Modul referenzieren.

Als letzten Schritt hängen wir die JBoss-Test-JARs ins Projekt. Dazu in die Properties gehen, und unter "Java Build Path" auf der Karteikarte "Libraries" die drei oben definierten Classpath-Variablen zufügen:
Add Classpath Variable
Das Ergebnis sollte so aussehen:
Java Build Path

Jetzt fügen wir eine Referenz auf das Remote Interface der "KuchenZutatNMWorkerBean" zu:
"application-client.xml":
<?xml version="1.0" encoding="UTF-8"?>
<application-client id="Application-client_ID" version="5"
	xmlns="http://java.sun.com/xml/ns/javaee"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/application-client_5.xsd">
	<display-name>
	KuchenZutatNMTestClient</display-name>
	
	<ejb-ref>
		<ejb-ref-name>ejb/KuchenZutatNMWorker</ejb-ref-name>
		<ejb-ref-type>Session</ejb-ref-type>
		<!-- A senseless "home" item is needed, otherwises JBoss would complain...-->
		<home>java.lang.Object</home>
		<remote>de.fhw.swtvertiefung.knauf.kuchenzutatnm.KuchenZutatNMWorker</remote>
	</ejb-ref>
</application-client> 
"jboss-client.xml":
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE jboss-client PUBLIC "-//JBoss//DTD Application Client 4.0//EN" "http://www.jboss.org/j2ee/dtd/jboss-client_4_0.dtd" >
<jboss-client>
  <jndi-name>KuchenZutatNMTestClient</jndi-name>
	<ejb-ref>
		<ejb-ref-name>ejb/KuchenZutatNMWorker</ejb-ref-name>
		<jndi-name>KuchenZutatNM/KuchenZutatNMWorkerBean/remote</jndi-name>
	</ejb-ref>
</jboss-client> 


Vorbereitung des Tests

Ich habe mir eine Util-Klasse "KuchenZutatTestUtil" gebaut, die einige Konstanten deklariert und Hilfsmethoden bietet.
public class KuchenZutatTestUtil
{
  /**Name des ersten Kuchens in unseren Tests. */
  public static final String KUCHENNAME1 = "TESTKUCHEN1";
  
  /**Name des zweiten Kuchens in unseren Tests. */
  public static final String KUCHENNAME2 = "TESTKUCHEN2";
  
  /**Name der ersten Zutat in unseren Tests. */
  public static final String ZUTATNAME1 = "TESTZUTAT1";
  
  /**Name der zweiten Zutat in unseren Tests. */
  public static final String ZUTATNAME2 = "TESTZUTAT2";
  
  public static KuchenNMBean getKuchenByName (KuchenZutatNMWorker kuchenZutatWorker, String strKuchenName)
  {
    List<KuchenNMBean> listKuchen = kuchenZutatWorker.getKuchen();
    
    for (KuchenNMBean kuchenAktuell : listKuchen)
    {
      if (strKuchenName.equals(kuchenAktuell.getName() ) )
      {
        return kuchenAktuell;
      }
    }
    //Nichts gefunden:
    return null;
  }
  
  public static ZutatNMBean getZutatByName (KuchenZutatNMWorker kuchenZutatWorker, String strZutatName)
  {
    List<ZutatNMBean> listZutaten = kuchenZutatWorker.getZutaten();
    
    for (ZutatNMBean zutatAktuell : listZutaten)
    {
      if (strZutatName.equals(zutatAktuell.getZutatName() ) )
      {
        return zutatAktuell;
      }
    }
    //Nichts gefunden:
    return null;
  }
} 


Erstellen eines Tests

Wir legen einen Test im Application Client an: Rechtsklick, "New", "Other...", in der Rubrik "Java" - "JUnit" finden wir den "JUnit Test case".
New JUnit Test
Wichtig ist dass wir einen JUnit-3.8.1-Test zufügen (das JBoss-Test-Framework ist nicht ganz aktuell). Basisklasse für den Test soll "org.jboss.test.JBossTestCase" sein. Wir lassen uns den Konstruktor sowie die Methoden "setUp" und "tearDown" generieren.
New JUnit Test
Wir fügen eine public Methode "testKuchen" zu. Anmerkung: alle public Methoden die mit "test..." beginnen werden vom JUnit-Framework per Reflection als Tests erkannt.
public void testKuchen() throws Exception
  {
    Object objRemote = this.getInitialContext().lookup("java:comp/env/ejb/KuchenZutatNMWorker");
    KuchenZutatNMWorker kuchenZutatWorker = (KuchenZutatNMWorker) PortableRemoteObject.narrow(objRemote, KuchenZutatNMWorker.class);
   
    KuchenNMBean kuchenNeu = new KuchenNMBean ();
    kuchenNeu.setName(KuchenZutatTestUtil.KUCHENNAME1);
    kuchenZutatWorker.saveKuchen(kuchenNeu);
    
    //Laden. Geht hier nicht über ID !
    KuchenNMBean kuchenLoad = KuchenZutatTestUtil.getKuchenByName(kuchenZutatWorker, KuchenZutatTestUtil.KUCHENNAME1);
    assertNotNull("Kuchen " + KuchenZutatTestUtil.KUCHENNAME1 + " nicht gefunden !", kuchenLoad); 
    
    //Löschen: 
    kuchenZutatWorker.deleteKuchen(kuchenLoad);

    //Prüfen dass wir den nicht mehr finden...
    kuchenLoad = KuchenZutatTestUtil.getKuchenByName(kuchenZutatWorker, KuchenZutatTestUtil.KUCHENNAME1);
    assertNull("Kuchen " + KuchenZutatTestUtil.KUCHENNAME1 + " nach Löschen gefunden !", kuchenLoad);
  } 
Die JBoss-Test-Suite bietet uns die Möglichkeit über getInitialContext an den InitialContext zu kommen.

Die Basisklasse class junit.framework.Assert von JBossTestCase bietet uns diverse Methoden um eine Bedingung abzuprüfen und im Fehlerfall den Test mit einer entsprechenden Meldung als "Fehlgeschlagen" zu deklarieren.

Unser obiger Test enthält eine potentielle Fehlerquelle: wenn bereits ein Kuchen mit dem Namen KuchenZutatTestUtil.KUCHENNAME1 vorhanden wäre, dann würde der Zugriff auf den Kuchen nach dem Löschen trotzdem erfolgreich sein. Eine Lösung wäre im "setUp" bzw. "tearDown" dafür zu sorgen dass die Datenbank explizit geleert wird.


Das Beispiel enthält einen identisch aussehenden Test für die Zutat-Bean.

Ein komplexerer Test ist "TestKuchenZutat":
 public class TestKuchenZutat extends JBossTestCase
{
  /**Der Worker für den Test. Wird im "setUp" geholt. */
  private KuchenZutatNMWorker kuchenZutatWorker = null;
 
  /**Mit diesen Kuchen wird gearbeitet. */
  private Integer intKuchenId1 = null, intKuchenId2 = null;
  
  /**Mit diesen Zutaten wird gearbeitet. */
  private Integer intZutatId1 = null, intZutatId2 = null;
  
  public TestKuchenZutat(String name)
  {
    super(name);
  }

  protected void setUp() throws Exception
  {
    super.setUp();
    
    //Worker holen:
    Object objRemote = this.getInitialContext().lookup("java:comp/env/ejb/KuchenZutatNMWorker");
    this.kuchenZutatWorker = (KuchenZutatNMWorker) PortableRemoteObject.narrow(objRemote, KuchenZutatNMWorker.class);
    
    //Zwei Kuchen und zwei Zutaten anlegen:
    KuchenNMBean kuchenNeu = new KuchenNMBean ();
    kuchenNeu.setName(KuchenZutatTestUtil.KUCHENNAME1);
    kuchenZutatWorker.saveKuchen(kuchenNeu);
    //Direkt wieder rausholen:
    //TODO Hier erfolgt keinerlei Prüfung !
    this.intKuchenId1 = KuchenZutatTestUtil.getKuchenByName(this.kuchenZutatWorker, KuchenZutatTestUtil.KUCHENNAME1).getId();
    
    kuchenNeu = new KuchenNMBean ();
    kuchenNeu.setName(KuchenZutatTestUtil.KUCHENNAME2);
    kuchenZutatWorker.saveKuchen(kuchenNeu);
    this.intKuchenId2 = KuchenZutatTestUtil.getKuchenByName(this.kuchenZutatWorker, KuchenZutatTestUtil.KUCHENNAME2).getId();
    
    ZutatNMBean zutatNeu = new ZutatNMBean ();
    zutatNeu.setZutatName(KuchenZutatTestUtil.ZUTATNAME1);
    kuchenZutatWorker.saveZutat(zutatNeu);
    this.intZutatId1 = KuchenZutatTestUtil.getZutatByName(this.kuchenZutatWorker, KuchenZutatTestUtil.ZUTATNAME1).getId();
    
    zutatNeu = new ZutatNMBean ();
    zutatNeu.setZutatName(KuchenZutatTestUtil.ZUTATNAME2);
    kuchenZutatWorker.saveZutat(zutatNeu);
    this.intZutatId2 = KuchenZutatTestUtil.getZutatByName(this.kuchenZutatWorker, KuchenZutatTestUtil.ZUTATNAME2).getId();
    
  }

  protected void tearDown() throws Exception
  {
    super.tearDown();
    
    //Kuchen und Zutaten löschen:
    this.kuchenZutatWorker.deleteKuchen( this.kuchenZutatWorker.findKuchenById(this.intKuchenId1 ));
    this.kuchenZutatWorker.deleteKuchen( this.kuchenZutatWorker.findKuchenById(this.intKuchenId2 ));
    
    this.kuchenZutatWorker.deleteZutat( this.kuchenZutatWorker.findZutatById(this.intZutatId1 ));
    this.kuchenZutatWorker.deleteZutat( this.kuchenZutatWorker.findZutatById(this.intZutatId2 ));
    
    //Worker wegwerfen:
    this.kuchenZutatWorker = null;
  }

  public void testKuchenZuZutat() throws Exception
  {
    //Kuchen1 als echtes Objekt holen. Geht hier nicht über ID !
    KuchenNMBean kuchenLoad = this.kuchenZutatWorker.findKuchenById(this.intKuchenId1);
    assertNotNull("Kuchen " + KuchenZutatTestUtil.KUCHENNAME1 + " nicht gefunden !", kuchenLoad);
    
    //Zutat 1 und 2 holen:
    ZutatNMBean zutat1 = this.kuchenZutatWorker.findZutatById(this.intZutatId1);
    assertNotNull("Zutat mit ID " + this.intZutatId1 + " nicht gefunden !", zutat1);
    ZutatNMBean zutat2 = this.kuchenZutatWorker.findZutatById(this.intZutatId2);
    assertNotNull("Zutat mit ID " + this.intZutatId2 + " nicht gefunden !", zutat2);
    
    //In den Kuchen hängen:
    kuchenZutatWorker.addZutatToKuchen(kuchenLoad, zutat1);
    kuchenZutatWorker.addZutatToKuchen(kuchenLoad, zutat2);
    
    //Den Kuchen erneut holen und sicherstellen dass er zwei Zutaten hat:
    kuchenLoad = this.kuchenZutatWorker.findKuchenById(this.intKuchenId1);
    assertNotNull("Kuchen mit ID " + this.intKuchenId1 + " nicht gefunden !", kuchenLoad);
    assertTrue("Kuchen hat keine zwei Zutaten", kuchenLoad.getZutaten().size() == 2);
    
    //Jetzt die Zutat 2 löschen. Die muss ich erstmal als echtes Objekt suchen.
    zutat1 = this.kuchenZutatWorker.findZutatById(this.intZutatId1);
    assertNotNull("Zutat mit ID " + this.intZutatId1 + " nicht gefunden !", zutat1);
    
    this.kuchenZutatWorker.removeZutatFromKuchen(kuchenLoad, zutat1);
    
    //Der Kuchen darf jetzt nur noch eine Zutat haben:
    kuchenLoad = this.kuchenZutatWorker.findKuchenById(this.intKuchenId1);
    assertNotNull("Kuchen mit ID " + this.intKuchenId1 + " nicht gefunden !", kuchenLoad);
    
    assertTrue("Kuchen hat nicht genau eine Zutat", kuchenLoad.getZutaten().size() == 1);
  }
}
Hier sieht man die Verwendung von "setUp" / "tearDown": die Methoden werden benutzt um die Datenbank mit einem definierten Datenbestand zu initialisieren.


Test ausführen

Vor dem Ausführen des Tests müssen wir eine Datei "jndi.properties" anlegen, aus der sich das Test-Framework die Informationen für die JBoss-Verbindung holt. Diese Datei habe ich im Projektverzeichnis abgelegt, für die Ausführung muss sie allerdings in "build\classes" kopiert werden !
java.naming.factory.initial=org.jnp.interfaces.NamingContextFactory
java.naming.factory.url.pkgs=org.jboss.naming.client
java.naming.provider.url=jnp://localhost:1099
j2ee.clientName=KuchenZutatNMTestClient
Anmerkung: beim Re-Import des Projekts wird diese Datei ins Verzeichnis "appClientModule" kopiert. Macht aber nix ;-).

Jetzt wird der Server gestartet und die Enterprise Application einschließlich des Application Clients deployed.

Endlich sind die Vorbereitungen abgeschlossen und wir können den Test starten. Dazu: Rechtsklick, "Run", "Run As..." wählen. In dem Dialog "Create, manage, and run configurations" erzeugen wir uns unter "JUnit" eine neue "Launch Configuration". Wir geben dem Test einen schönen Namen, wählen die Option "Run all tests in the selected project, package or source folder" und wählen als TestRunner "JUnit 3".
Configuration
Es erscheint ein neues Fenster mit den Ausgaben der Tests. Hier erkennen wir auch Exceptions oder fehlgeschlagene Tests:
Testergebnisse
Der unterste, mit einer Warnung versehene Test "org.jboss.test.JBossTestCase" wird scheinbar beim Durchsuchen des Verzeichnisses gefunden, es handelt sich allerdings nur um die Basisklasse unserer Testsuite.


Falls hier folgende Fehlermeldung erscheint, dann wurde "jndi.properties" nicht gefunden:
javax.naming.NoInitialContextException: Need to specify class name in environment or system property, or as an applet parameter, or in an application resource file:  java.naming.factory.initial
	at javax.naming.spi.NamingManager.getInitialContext(Unknown Source)
	at javax.naming.InitialContext.getDefaultInitCtx(Unknown Source)
	at javax.naming.InitialContext.getURLOrDefaultInitCtx(Unknown Source)
	at javax.naming.InitialContext.lookup(Unknown Source)
	at de.fhw.swtvertiefung.kuchenzutatnm.TestKuchen.testKuchen(TestKuchen.java:46)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
	... 


Unit-Test mit Security

Soll der Unit-Test auf Beans zugreifen die gesichert sind, so sind einige kleine Änderungen nötig. Folgendes Beispiel entstand aufgrund des Security-Beispiels, indem ich im Application Client einen Testcase zugefügt habe !

Variante 1:
Im Testcase muss die Methode "setUp" überladen werden, und es wird ein Login am Server durchgeführt:

  /**Testcase initialisieren. Hier: Login durchführen !
   * @exception Exception Jeder Fehler
   */
  @Override
  protected void setUp() throws Exception
  {
    super.setUp();

    AppCallbackHandler callbackHandler = new AppCallbackHandler("ADMIN", "ADMIN".toCharArray() );
    LoginContext loginContext = new LoginContext ("knaufclientsecurity", callbackHandler);
    loginContext.login(); 
  }
Die Klasse AppCallbackHandler stammt aus dem Package org.jboss.security.auth.callback.AppCallbackHandler, ihr können direkt Login und Passwort übergeben werden. Der Name des Login-Contexts ("knaufclientsecurity") stammt aus dem Security-Beispiel.

Das Ausführen des Tests gestaltet sich jetzt ebenfalls ein wenig schwieriger, denn ein Rechtsklick -> "Run as" -> "Unit Test" ist nicht mehr direkt möglich. Stattdessen legt man sich die Run-Konfiguration am besten manuell an (siehe weiter oben). Auf der Karteikarte "Arguments" muss man folgendes eintragen:

-Djava.security.auth.login.config=appClientModule/META-INF/auth.conf

Security konfigurieren

Variante 2:
Diese Variante ist in der Verwendung einfacher, da die Authentifizierung gegen das JNDI erfolgt (sprich: beim JNDI-Zugriff führt der Server eine Authentifizierung durch). User und Passwort werden in "jndi.properties" gesetzt, die Naming-Factory wird geändert.
    java.naming.factory.initial=org.jboss.security.jndi.JndiLoginInitialContextFactory
    java.naming.factory.url.pkgs=org.jboss.naming.client
    java.naming.provider.url=jnp://localhost:1099
    j2ee.clientName=SecurityClient
    java.naming.security.principal=ADMIN
    java.naming.security.credentials=ADMIN 
Nachteil dieser Methode: sie erlaubt nur einen Login für alle Unit-Tests.
Deshalb kann der Login auch programmatisch durchgeführt werden, dann muss allerdings der InitialContext per Hand (z.B. in setUp) erzeugt werden:
    Properties props = new Properties();
    props.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.jnp.interfaces.NamingContextFactory");
    props.setProperty(Context.URL_PKG_PREFIXES, "org.jboss.naming.client");
    props.setProperty(Context.PROVIDER_URL, "jnp://localhost:1099");
    props.setProperty(org.jboss.naming.client.java.javaURLContextFactory.J2EE_CLIENT_NAME_PROP, "SecurityClient");
    props.setProperty(Context.SECURITY_PRINCIPAL, "ADMIN");
    props.setProperty(Context.SECURITY_CREDENTIALS, "ADMIN");
    props.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.jboss.security.jndi.JndiLoginInitialContextFactory");

    InitialContext ic = new InitialContext(props); 



Stand 18.02.2007
Historie:
23.10.2006: Erstellt
17.01.2007: Beispiel für Security-Test
18.02.2007: Weitere Login-Methode für Security-Test