Archiv für die Kategorie ‘Qualitätssicherung’

Randomisierte parametrisierte Tests in JUnit

Montag, 04. Mai 2009

JUnit kennt neben dem Testrunner für einfache Testfälle auch noch den Testrunner für Theorien, den ich bereits vorgestellt habe, und den für parametrisierte Tests. Möchte man einen dieser beiden Testrunner verwenden, steht man unweigerlich vor dem Problem, welche Eingabedaten verwendet werden sollen. Insbesondere deren Erzeugung ist eine immer wiederkehrende und monotone Arbeit, die man sich sparen möchte.

Die in dem Artikel über  parametrisierte Tests vorgestellte Herangehensweise, ermöglicht es Zufallsdaten für parametrisierte Tests und Theorien zu liefern. Sie lässt auch viel Raum für weitere Ideen. Für gute Vorschläge und Denkanstöße wären wir dankbar.


Alexander Draeger


Transaktionssteuerung mit Spring AOP

Freitag, 24. April 2009

Spring bietet uns mit AOP die Möglichkeit innerhalb der Spring Konfiguration die Behandlung der Datenbanktransaktionen zu steuern. Hierfür benötigen wir lediglich eine AOP Konfiguration, die auf alle Methoden zeigt, die innerhalb einer Transaktion ablaufen sollen und den Spring Tag .

In diesem Beispiel gehen wir von zwei Service Klassen aus, deren Methoden von einer Transaktion umschlossen werden sollen.

applicationContext.xml:

<-- service beans -->
<bean id="fooService" class="a.b.service.FooService" />
<bean id="barService" class="a.b.service.BarService" />
 
<-- hibernate transaction manager -->
<bean id="txManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager">
    <property name="sessionFactory" ref="sessionFactory" />
</bean>
 
<-- hibernate session factory -->
<bean id="sessionFactory" class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean">
    <property name="dataSource">
        <ref bean="dataSource" />
    </property>
    <property name="annotatedClasses">
        <list>....</list>
    </property>
    <property name="hibernateProperties">
        <props>
            <prop key="hibernate.dialect">org.hibernate.dialect.PostgreSQLDialect</prop>
        </props>
    </property>
</bean>

Innerhalb des tx:advice definieren wir das Verhalten für die einzelnen Methoden der Services. Für alle Methodenaufrufe werden die gleichen Isolation- und Propagation-Level benutzt. Für Methoden die mit „find“ oder „get“ beginnen setzen wir die Transaktion noch auf read-only. In diesem Methoden dürfen jetzt nur noch Datenbank SELECTs ausgeführt werden, anderenfalls würde es eine Exception geben. Die Datenbank kann so das Locking auf Zeilen, die von diesem Methodenaufrufen betroffen, sind optimieren.

applicationContext.xml:

<tx:advice id="defaultTranscationAdvice" transaction-manager="txManager">
    <tx:attributes>
        <tx:method name="*" isolation="READ_COMMITTED" propagation="REQUIRED" />
        <tx:method name="find*" read-only="true" />
        <tx:method name="get*" read-only="true" />
    </tx:attributes>
</tx:advice>

Als letztes brauchen wir jetzt noch die AOP Konfiguration, die das defaultTransactionAdvice mit den Service Klassen verbindet. Durch die Definition „a.b.service.*.*(..)“ wird um jede Methode in jeder Klasse innerhalb des Paketes a.b.service eine Transaktion gelegt. Es können so weitere Service Klassen hinzugefügt werden, ohne dass das Transaktion Handling angepasst werden muss.

applicationContext.xml:

<aop:config>
    <aop:pointcut id="allServiceMethods" expression="execution(* a.b.service.*.*(..))" />
    <aop:advisor advice-ref="defaultTranscationAdvice" pointcut-ref="allServiceMethods" />
</aop:config>

Innerhalb der Service Methoden brauchen wir uns nun keine Gedanken mehr um offene oder geschlossene Transaktionen machen. Wir bewegen uns in jedem Fall innerhalb einer Transaktion. Sollte es zu einem Fehler kommen, so wird ein Rollback auf allen Änderungen der Methode durchgeführt.
Erst beim Verlassen der Service Methode müssen wir berücksichtigen, dass Objekte ggf. nicht mehr an eine Session gebunden sind (hier kann es dann zu Problemen mit LazyLoading o.ä. kommen).


Felix Breske


Organisation von Testklassenhierarchien

Freitag, 27. März 2009

Jeder weiß, dass frühzeitiges und intensives Testen die Entwicklungszeit und -kosten gering hält und trotzdem: Der Softwaretest ist ein gern vernachlässigtes Kapitel der Softwareentwicklung. Es mag vielleicht daran liegen, dass viele Unit-Tests zu viel sich wiederholenden Quelltext haben. Dabei bringen die objektorientierten Programmiersprachen Konzepte mit, die man in den Tests so einsetzen kann wie in dem getesteten System (SUT, system under test).

Die Konzepte Vererbung, Polymorphie und Generizität erlauben es, klassenorientierte Testfälle zu schreiben, die dann auf alle Unterklassen der CUT (class under test) angewandt werden können. In der Literatur gibt es dazu den Begriff des symmetrischen Testtreibers („Symmetric Driver“), was nichts anderes heißt, als dass parallel zu einer Klassenhierarchie eine Testklassenhierarchie geschrieben wird. Der Name resultiert daher, dass die Testklassenhierarchie symmetrisch zur Klassenhierarchie des SUT ist. Das beigefügte Dokumente zeigt, wie die Testfallhierarchie zu einer Klassenhierarchie aussehen kann. Strukturelemente wie die Testobjekte (OUTs, OUT = object under test) sollten protected sein, sodass Unterklassen darauf zugreifen können. Die Testmethoden sind public, wie es das Testframework, z. B. JUnit, erfordert.

Illustration der allgemeinen Testklassenhierarchie

Dass Testfälle auf Unterklassenmethoden wiederholt werden, klingt vielleicht nach Redundanz, ist aber pure Notwendigkeit. Die kürzlich mit dem Turing-Preis ausgezeichnete Barbara Liskov postulierte, dass Vorbedingungen (oder auch: der Definitionsbereich) einer überschriebenen Methode nicht eingeschränkt werden dürfen. Die Nachbedingungen dürfen eingeschränkt werden, aber nicht erweitert werden. Solche Eigenschaften sollten nach Möglichkeit durch die gesamte Klassenhierarchie erhalten bleiben und eignen sich hervorragend für der „Symmetric Driver“.

Der Aufbau der Testklassenhierarchie demonstriert die Stärke objektorientierter Modellierung. Objektorientierte Softwareentwicklung hat viele Fehlerquellen beseitigt, aber es kommen auch neue hinzu, wie z. B. eingeschränkte Vorbedingungen von überschriebenen Methoden. Die Testklassenhierarchie ist hilfreich beim systematischen Aufdecken von Fehlern des objektorientierten Entwurfes.

Die Verletzung des Liskovschen Substitutionsprinzips zeigt folgendes Beispiel:

class A {
  public void method(int n) {
     if (n < 0) throw new IllegalArgumentException();
  }
}
 
class B extends A {
 public void method(int n) {
     if (n < 1) throw new IllegalArgumentException();
  }

Die folgende Testklassenhierarchie findet den Fehler, vorausgesetzt, es werden die richtigen Datenpunkte definiert.

@RunWith(Theories.class)
class TestA {
 
   @Datapoint
   public static final Integer int1 = Integer.valueOf(0);
   protected A out;
  @Before
   public void setUp() {
     out = new A();
   }
 
   @Theory
   public smokeTest(int n) {
     assumeTrue(n>=0);
     out.method(n);
   }
}
 
class TestB extends TestA {
  @Before
   public void setUp() {
      out = new B();
   }
}

Alexander Draeger


“Theorien” in JUnit 4.4 – Anmerkung

Montag, 17. November 2008

Man kann Theorien sehr effektiv auf alle möglichen Klassen des System ausdehnen, ohne viel Zusatzarbeit. Die im Beispiel getesteten Eigenschaften bzgl. equals und hashcode müssen auch für alle Klassen gelten. Der Schlüssel hierzu ist eine generische, abstrakte Klasse, von der abgleitet wird.

package theoryexample;
 
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeTrue;
 
import org.junit.experimental.theories.Theories;
import org.junit.experimental.theories.Theory;
import org.junit.runner.RunWith;
 
@RunWith(Theories.class)
public abstract class ObjectTest &lt;ClassUnderTest&gt; {
 
	@Theory
	public void symmetrie(ClassUnderTest a, ClassUnderTest b) {
		assumeTrue(a.equals(b));
		assertTrue(b.equals(a));
	}
 
	@Theory
	public void reflexivitaet(ClassUnderTest a) {
		// assumeTrue(true);
		assertTrue(a.equals(a));
 
	}
 
	@Theory
	public void transitivaet(ClassUnderTest a, ClassUnderTest b, ClassUnderTest c) {
		assumeTrue(a.equals(b));
		assumeTrue(b.equals(c));
		assertTrue(a.equals(c));
	}
 
	@Theory
	public void equalsHashcode(ClassUnderTest a, ClassUnderTest b) {
		assumeTrue(a.equals(b));
		assertEquals(a.hashCode(), b.hashCode());
	}
 
}
package theoryexample;
 
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeTrue;
 
import org.junit.Test;
import org.junit.experimental.theories.DataPoint;
import org.junit.experimental.theories.DataPoints;
import org.junit.experimental.theories.Theories;
import org.junit.experimental.theories.Theory;
import org.junit.runner.RunWith;
 
public class DreieckTest extends ObjectTest  {
	@DataPoint
	public static final Dreieck d1 = new Dreieck(0.0, 0.0, 1.0, 1.0, 2.0, 2.0);
	@DataPoint
	public static final Dreieck d2 = new Dreieck(0.5, 0.5, 1.5, 1.5, 2.5, 2.5);
	@DataPoint
	public static final  Dreieck d3 = new Dreieck(0.0, 0.0, 1.0, 1.0, 3.0, 3.0);
 
	@DataPoint
	public static final  Dreieck d4 = new Dreieck(0.0, 0.0, -1.0, -2.0, -2.0, -2.0);
	@DataPoint
	public static final  Dreieck d5 = new Dreieck(0.5, 0.5, 1.5, 1.5, -2.5, -2.5);
	@DataPoint
	public static final  Dreieck d6 = new Dreieck(-1.0, 0.0, -2.0, -2.0, -3.0, -2.0);
 
	@DataPoints
	public static final Dreieck[] nochmehrDreiecke = {
		new Dreieck(-1.0, -2.0, -2.0, -2.0, -3.0, 2.0),
		new Dreieck(-1.0, -1.0, -2.0, -2.0, -4.0, 2.0),
		new Dreieck(-1.0, 0.0, -2.0, -2.0, -5.0, 2.0),
		new Dreieck(-1.0, 2.0, -2.0, -2.0, -7.0, 2.0)
	};
 
}

Alexander Draeger