JUnit TestViewer Plugin

Aus Eclipse
Wechseln zu: Navigation, Suche

Einführung

Das hier vorgestellte Plugin TestViewer soll die Möglichkeiten demonstrieren, mit dem in Eclipse integrierten JUnit Framework durch eigene Views, Aktionen und Arbeitsabläufe zu interagieren. Hierzu stellt das Plugin eine View zur Verfügung, die alle gestarteten JUnit Tests anzeigt und die Möglichkeit bietet, alle JUnit Tests des Workspaces zu starten oder nur eine bestimmte Testklasse noch einmal auszuführen.

Es soll hier nicht erklärt werden, wie man ein Plugin schreibt, eine View, eine Extension oder ähnliches, hierzu gibt es eigene weitaus tiefer gehende Seminarthemen. Es sollen die Besonderheiten herausgearbeitet werden, denen man bei der Interaktion mit JUnit begegnet. Insofern ist das Plugin selber auch nicht als ein sofort einsetzbares Produkt zu verstehen, vielmehr als eine Art Sammlung von Ideen und Konzepten unter einer Oberfläche, so dass man sie im Zusammenspiel testen kann.

Klassenübersicht

Das Plugin enthält eine Vielzahl von Klassen, so dass eine Übersicht der Struktur mit kurzer Beschreibung der Aufgabe der Klasse den Einstieg erleichtern sollte.

  • Package name.noelke.testplugin1
    • Klasse Activator: Der Standard Activator des Plugin Generation Wizards
  • Package name.noelke.testplugin1.controller: Package für Klassen, die zum Controller des Patterns Model-View-Controller gehören.
    • Klasse Controller: Der eigentliche Controller. Er steuert den Zusammenhalt zwischen View und Model, reagiert auf Ereignisse und löst das Starten der Tests aus.
    • Klasse TestListener: Der Controller registriert ein Objekt dieser Klasse als Listener beim Eclipse JUnit Plugin. Dieses erkennt laufende Tests und übergibt die Daten ans Model.
  • Package name.noelke.testplugin1.model: Package für Klassen, die zum Model des Patterns Model-View-Controller gehören.
    • Klasse Model: Hält Informationen über die laufenden bzw. gelaufenen Tests.
    • Klasse TestCase: Ein einzelnes Datenelement des Models
  • Package name.noelke.testplugin1.runner: Infrasturktur zum Finden und Starten von Tests
    • Klasse JUnitInvoker: Diese Klasse von vom Controller angesprochen, um eine Liste von Tests zu starten
    • Klasse ConfigurationFactory: Erstelle Eclipse Launch Configurations, mit denen die Tests dann Eclipse zum Start übergeben werden.
    • Klasse TestFinder: Stellt Methoden zur Verfügung, um Tests suchen zu können.
    • Klasse PluginHelper: Ein paar ausgelagerte Methoden.
  • Package name.noelke.testplugin1.views: Package für Klassen, die zur View des Patterns Model-View-Controller gehören.
    • Klasse TestView: Die eigentlich View-Komponente des Plugins. Hier werden die Tests visualisiert und können gestartet werden.
    • Klasse ViewContentProvider: Hilfsklasse für den TableViewer, um Daten des Models darstellen zu können.
    • Klasse ViewLableProvider: Hilfsklasse für den TableViewer, um Daten des Models darstellen zu können.
  • Dateien build.properties und plugin.xml: Verwaltungsdaten des Plugins.

Das Package Runner

Unit Tests finden

Bei der Interaktion mit JUnit trifft man schnell auf das erste Problem, was ist überhaupt ein Test und wie findet man Testklassen?

Seit Eclipse 3.5 lässt sich diese Frage leicht beantworten, denn das JUnit Plugin von Eclipse stellt eine passende Methode bereit, um alle Tests in einem Container wie zum Beispiel einem Java-Projekt zu finden. Diese Methode arbeitet sehr effizient und berücksichtigt auch Sonderfälle wie abstrakte Methoden, die bereits als Test gekennzeichnet sind usw.

In der Klasse TestFinder gibt es hierzu die Methode findTests, die durch einen Aufruf von

IType[] result = JUnitCore.findTestTypes(projects[0], null);

in result alle Klassen eines Projekts enthält, die ausführbare Tests enthalten, gleich ob es JUnit 3 oder JUnit 4 Tests sind.

Vor Eclipse 3.5 war das Suchen von Tests deutlich aufwändiger. Hierzu gibt es den Artikel Suchen in Eclipse, der leider nicht ganz korrekt zeigt, wie man mit Eclipse-Bordmitteln nach Tests suchen kann.

Unit Tests starten

Naives ansprechen von JUnit

Im einfachsten Fall kann ein Plugin einen Unit Test durchführen, indem es JUnit direkt aufruft. Das Plugin könnte die Library für JUnit selber mitbringen oder hierzu die Library benutzen, die Eclipse beinhaltet.

Hierzu wird dann von der Klasse org.junit.runner.JUnitCore die Methode run gestartet. Als Parameter werden die Testklassen übergeben, sie liefert dann das Ergebnis als Rückgabewert.

Dieses Vorgehen hat einige entscheidende Nachteile:

  • Die Ausführung findet im gleichen Thread statt wie die aufrufende Komponente des Plugins. Hierdurch muss dieser Thread warten, bis der Testlauf abgeschlossen ist. Dies ist in der Regel unerwünscht, also muss sich der Plugin-Entwickler selber um eine Threadsteuerung kümmern.
  • Solche Tests sind nebenläufig zu der in Eclipse bereits vorhandenen Testengine. Andere Plugins wissen nicht, dass es nun eine weitere Testengine gibt, sie haben keine Chance, von solchen Tests zu erfahren und mit ihnen zu interagieren.
  • Das Plugin profitiert nicht von Updates des JUnit Plugins der Plattform. Jede neue JUnit Version muss wieder neu ins Plugin integriert werden.

Diese Methode, JUnit Tests zu starten, ist somit nicht empfehlenswert. Unit Tests sollten ausschließlich gestartet werden über die Mechanismen und Plugins, die Eclipse dazu bereithält.

Run Configurations

Ziel des Plugins TestViewer muss es also sein, Plugins so zu starten, als wenn ein Endanwender dies interaktiv über die Oberfläche tun würde. Nur so kann sichergestellt werden, dass alle in Eclipse integrierten Mechanismen und Plugins korrekt arbeiten.

Um einen Test also unter diesen Bedingungen zu starten, muss eine Run Configuration angelegt werden, die den Test ausführt. Im Idealfall sollte das Plugin vorher noch überprüfen, ob solch eine Configuration schon existiert. Dann muss diese Run Configuration dem Eclipse Launcher übergeben werden, so dass dieser sie ausführt. Das Vorgehen des Plugins ist hier im Folgenden beschrieben.

Das Anlegen und Ausführen der Run Configuration findet im Plugin in seiner Klasse JUnitInvoker durch die Methode run statt. Ihr wird als Parameter eine Liste der Klassen übergeben, die auszuführen sind.

  public void run(final Set<IType> testsToRun) throws CoreException

Zunächst wird in run die Klasse ConfigurationFactory des Plugins instanziiert und der Methode getConfiguration diese Liste testsToRun übergeben.

 final ILaunchConfiguration launchConfiguration = new ConfigurationFactory(project).getConfiguration(testsToRun);

Aufgabe dieser Methode ist es, eine Launch Configuration zu liefern, die die übergebenen Testklassen ausführen kann. Hierzu legt die Methode zunächst temporär eine neue Launch Configuration an. Danach versucht sie, eine bereits gespeicherte Launch Configuration zu finden, die die gleichen Tests ausführen würde. Falls eine solche gefunden wird, wird diese als Methodenergebnis zurückgeliefert. Ansonsten wird die neu erstellte Launch Configuration gespeichert und zurückgeliefert.

ILaunchConfiguration getConfiguration(Set<IType> testsToRun) throws CoreException {
   ILaunchConfigurationWorkingCopy wc = createDefaultLaunchConfig(testsToRun);
   ILaunchConfiguration existingConfig = findExistingLaunchConfiguration(wc);
   return existingConfig == null ? wc.doSave() : existingConfig;
}

Nachdem in der Methode run der Klasse JUnitInvoker nun eine gültige Launch Configuration existiert, muss diese Eclipse zum Starten übergeben werden. Hierzu schickt die Methode eine Nachricht an den Display-Task, seinerseits die Methode launch des Plugins zu starten.

 Activator.getDisplay().asyncExec(new Runnable() {
    public void run() {
       launch(launchConfiguration, delegate);
    }
 });

In dieser Methode launch wird eine neue anonyme Klasse von org.eclipse.core.runtime.jobs.Job abgeleitet und instantiiert, die ihrerseits die Launch Configuration startet.

private void launch(final ILaunchConfiguration config) {
    Job job = new Job("JUnit Launch") {
        @Override
        protected IStatus run(IProgressMonitor monitor) {
            monitor.beginTask("Launching JUnit", 100);
            try {
                invokeDelegate(config, monitor);
            } catch (CoreException e) {
                return e.getStatus();
            } finally {
                monitor.done();
            }
                return Status.OK_STATUS;
            }
        };
        job.setPriority(Job.INTERACTIVE);
        job.schedule();
    }
}

Nachdem der Job nun gestartet ist, bleibt zu klären, wie er mit Hilfe der Methode invokeDelegate die Launch Configuration, die bis hierhin durchgereicht wurde, startet. Hierzu bedient sich diese Methode der Klasse Launch und JUnitLaunchConfigurationDelegate.

final ILaunchConfigurationDelegate delegate = new JUnitLaunchConfigurationDelegate();
final ILaunch launch = new Launch(config, ILaunchManager.RUN_MODE, null);
launch.setAttribute(DebugPlugin.ATTR_CONSOLE_ENCODING, PluginHelper.getLaunchManager().getEncoding(config));
PluginHelper.getLaunchManager().addLaunch(launch);
delegate.launch(config, ILaunchManager.RUN_MODE, launch, monitor);

Alle Unit Tests ausführen

Wenn man nun einerseits eine Methode hat, alle Tests in einem Workspace zu finden, und andererseits eine Methode, um Tests ausführen zu können, kann man diese beiden in einer eigenen Methode bündeln. Dies ist in der Methode runAllTestsInWorkspace der Klasse Controller realisiert.

public void runAllTestsInWorkspace() {
   IJavaProject javaProjects[] = null;
   IJavaProject currentProject[] = new IJavaProject[1];
   try {
      javaProjects = pluginHelper.getAllJavaProjects();
      for (int i=0; i<javaProjects.length; i++) {
         currentProject[0] = javaProjects[i];
         Set<IType> testsToRun = new HashSet<IType>();
         JUnitInvoker invoker = new JUnitInvoker(javaProjects[i]);

         IMethod testMethods[] = TestFinder.findTestMethodsInWorkspace(currentProject);
         for (int j=0; j<testMethods.length; j++) {
            testsToRun.add(testMethods[j].getDeclaringType());
         }
         invoker.run(testsToRun);
      }
   } catch (JavaModelException e) {
      e.printStackTrace();
   } catch (CoreException e) {
      e.printStackTrace();
   }
}

Hier werden zunächst alle Java-Projekte im Workspace gesucht. In jedem dieser Java-Projekte werden nun alle Testmethoden gesucht. Zu jeder dieser Methoden wird die passende Klasse gesucht, die dann in ein Set eingestellt werden. All diese Klassen werden dann dem JUnitInvoker zur Ausführung übergeben.

Eine bestimmte Testklasse

Eine weitere Anforderung an das Plugin ist, neben der Ausführung aller Tests einen ganz bestimmten Test wiederholen zu lassen. Hierbei ist allerdings zu beachten, dass im JUnit Framework, die kleinste ausführbare Einheit eine Testklasse ist, und nicht eine einzelne Testmethode.

Eine Methode, die dies ermöglicht ist runSingleTest der Klasse Controller:

 public void runSingleTest(TestCase tc) {
    IJavaProject currentProject[] = new IJavaProject[1];
    currentProject[0] = tc.getTestRunSession().getLaunchedProject();
    Set<IType> testsToRun = new HashSet<IType>();
    JUnitInvoker invoker = new JUnitInvoker(currentProject[0]);
    IMethod testMethod = TestFinder.findJavaMethodByName(currentProject, tc.getClassname(), tc.getMethod());
    if (testMethod != null)
       testsToRun.add(testMethod.getDeclaringType());
    try {
       invoker.run(testsToRun);
    } catch (CoreException e) {
       e.printStackTrace();
    }
 }

Hier wird der Methode ein ganz spezieller Testfall übergeben, der bereits gelaufen ist. Die Methode sucht eben diese Testklasse, und übergibt sie der JUnitInvoker.

Das Package Controller

Laufende Tests erkennen

Wenn man nun über die angegebenen Methoden Eclipe angewiesen hat, Unit Tests auszuführen, möchte man natürlich auch über den Fortschritt und das Ergebnis informiert werden. Da man ja wie oben bereits erklärt JUnit nie selbst händisch starten sollte, muss hierzu ein entsprechender Teil im Plugin realisiert werden. Eine weitere Anforderung ist, dass man nicht nur Tests erkennen möchte, die aus dem Plugin selber heraus ausgelöst wurden, sondern insbesondere auch solche, die von anderer Seite beispielsweise von anderen Plugins ausgelöst wurden.

Bis zu Version 3.2 wurde man von JUnit in Eclipse über Testergebnisse informiert, indem man einen bestimmten Extension Point erweiterte. Dies wird seit Version 3.3 nicht mehr empfohlen (siehe auch [1]). Nun wird man über laufende Tests und Testergebnisse informiert, indem man eine Klasse, die die Klasse TestRunListener erweitert, als Listener bei JUnit registriert. Durch Überschreiben der Methoden sessionStarted, sessionFinished, testCaseStarted und testCaseFinished wird man darüber informiert, wann Testsessions starten und enden, beziehungsweise Testfälle starten und enden.

Das Plugin TestViewer enthält also die Klasse MyListener

 public class MyListener extends TestRunListener {

Diese Klasse wird später als Listener registriert und überschreibt zwei der vier möglichen Methoden.

    @Override
    public void testCaseStarted(ITestCaseElement testCaseElement) { ... }

Jeder gestartete Test wird im Model angelegt, so dass der Testfortschritt im View angezeigt werden kann.

    @Override
    public void testCaseFinished(ITestCaseElement testCaseElement) { ... }

Nach Beenden eines Tests wird der Status im Model aktualisiert. Hierzu wird jeweils eine TestCase Objekt erzeugt und dem Model übergeben. Dieses informiert den Controller, dass es verändert wurde, damit die View aktualisiert werden kann.

Das Package Model

Die Aufgabe des Models ist es, die Daten darzustellen, Methoden für einen sicheren Zugriff zu bieten und sorge zu tragen, dass Daten nur in erlaubter Weise verändert werden können. Außerdem soll es bei Veränderungen interessierte benachrichtigen, dass sich etwas geändert hat.

Klasse TestCase

Die Klasse TestCase repräsentiert einen einzelnen Testfall, der sich in Ausführung befindet oder bereits ausgeführt wurde. Konstruiert wird ein TestCase durch ein ITestCaseElement, wie es der TestListener übergeben wird. Die relevanten Daten werden dem ITestCaseElement entnommen, zusätzlich wird noch die aktuelle Uhrzeit hinzugefügt:

 public TestCase(ITestCaseElement testCaseElement) {
    method = testCaseElement.getTestMethodName();
    classname = testCaseElement.getTestClassName();
    testrunname = testCaseElement.getTestRunSession().getTestRunName();
    trs = (TestRunSession) ((TestCaseElement) testCaseElement).getTestRunSession();
    project = trs.getLaunchedProject().getElementName();
    zeitpunkt = new GregorianCalendar();
 }

Klasse Model

Die Klasse Model hält die TestCase Objekte, sobald sie erstellt wurden. Sie ist als Singleton ausgeführt:

public class Model {
	private static Model myModel = null;
	private HashMap<String, TestCase> testcases;
 
	private Model() {
		testcases = new HashMap<String, TestCase>();
	}
 
	public static Model getModel() {
		if (myModel==null)
			myModel = new Model();
		return myModel;
	}
 
        /*...*/	
 
}

Es existieren Methoden, um einen Testfall hinzuzufügen und um eine Liste aller Testfälle zu erhalten. Derzeit nicht implementiert sind Methoden, um einzelne oder alle Testfälle wieder zu löschen. Diese Anforderung hat sich im Rahmen des Plugins nicht gestellt.

Die Methode zum Hinzufügen von Testfällen ist die einzige, die Daten im Model verändern kann. Somit benachrichtigt Sie den Controller, der dann alle Listener benachrichtigt.

Das Package View

Die Klasse TestView

Diese Klasse stellt das eigentliche sichtbare Interface dar, mit dem der Anwender von Eclipse mit dem Plugin interagieren kann. Die Klasse generiert eine Eclipse-View als Tabelle, in der die laufenden Tests angezeigt werden können, sowie zwei Buttons, mit denen man entweder alle Tests oder einen bestimmten Test starten kann.

TestView ist abgeleitet von ViewPart, der Basisklasse für alle Views in Eclipse. In der Methode createPartControl erstellt die Klasse ein TableViewer Objekte, teilt es in Spalten ein und weist ihm den ViewContentProvider und den ViewLableProvider zu.

Die Klasse registriert sich beim Controller als Listener für Veränderungen am Model und stellt darüber hinaus eine Methode refresh zur Verfügung. Bei jeder Änderung von Daten im Model muss die View benachrichtigt werden, dass sie sich aktualisieren soll, um diese Änderung auch darzustellen. Dies ist realisiert in der Methode Refresh, die allerdings nichts weiter tut, als die Refresh-Methode des TableViewer aufzurufen.

Wesentlich komplexer ist hier, diese Methode korrekt aufzurufen. Hierbei ist zu beachten, dass es im Plugin verschiedene Threads gibt, und es gilt, die Methode im Kontext des richtigen aufzurufen. In der Klasse Controller ist hierzu die Methode notifyDataChange realisiert.

 public void notifyDataChange() {
    Display display = Display.getCurrent();
    if (display == null)
       display = Display.getDefault();
       display.syncExec(  
          new Runnable() {
             public void run() {
                viewer.refresh();
             }
       });
 }

Sie ermittelt zunächst den Display-Thread, und in ihm wird dann die Refresh-Methode aufgerufen.

An den TableViewer angehängt sind zwei anonyme Klassen, beide abgeleitet von Action. In der ersten wird der selektierte Testfall erneut ausgeführt, in der zweiten dann werden alle Testfälle gestartet.

Beispielcode


Nächster Artikel: JUnit Literatur