ScalaTest mit Maven


Dies ist ein Gastbeitrag von 0x7F800000. Eine kürzere Version dieses Artikels gibt es auch auf englisch: You can find a shorter english version of this post here.

Neuerdings wollte ich bei einem Scala-Projekt ScalaTest in Maven zum Laufen bringen, konnte aber kaum brauchbare Informationen darüber finden.

So stößt man beim googlen nach „maven scalatest plugin“ hauptsächlich auf Diskussionen, die darauf hinauslaufen, dass man eine @RunWith – Annotation an die ScalaTest-Suites anhängen und weiterhin mit JUnitRunner leben soll, etwa so:

import org.scalatest.junit.JUnitRunner
import org.junit.runner.RunWith
import org.scalatest.Spec
import org.scalatest.matchers.ShouldMatchers
 
@RunWith(classOf[JUnitRunner])
class MySpec extends Spec {
    // ScalaTest spec code here
}

(siehe beispielsweise JPz’log, stack-overflow oder scalatest-users google groups)

Das ganze wird dann einfach vom maven-surefire-plugin aufgegriffen, und als ein klassischer JUnit-Test ausgeführt. Das funktioniert zwar einigermaßen, aber es liefert nicht wirklich die erhoffte übersichliche Liste mit Testergebnissen wie hier angepriesen. Daraus ergibt sich die Frage: Wie bringt man Maven dazu, den org.scalatest.tools.Runner für die Scala-Suites und Specs zu verwenden?

Die Antwort lautet: Das maven-scalatest-plugin statt maven-surefire-plugin nutzen. Dies ist ein erst vor kurzem von Jon-Anders Teigen entwickeltes Maven-Plugin, und kann (noch) nicht aus den maven-repositories heruntergeladen werden: Man muss es stattdessen direkt von Teigens Seite herunterladen und installieren. Dies bewerkstelligt man, indem man den Installationsanweisungen auf der Projektseite folgt, also:

  1. Man stelle sicher, dass man über eine funktionsfähige Version von git verfügt
  2. Man klone das angegebene Repository. Teigen selbst empfiehlt Sean Griffins Repository. Ich habe es mit Teigens Version riskiert, und es scheint zu funktionieren.
  3. Man installiere das Plugin, wie in der Anleitung beschrieben (ebenfalls nur ein kurzer Maven Befehl)

Wie verwendet man es nun in dem eigenen Projekt? Ich will es direkt an einem kleinen Beispielprojekt demonstrieren. Dazu lassen wir uns zuerst ein Archetyp-Projekt von maven erzeugen:

> mvn archetype:generate
[...fürchterlich lange liste mit allen möglichen Archetypen, sorgt dafür dass eure Konsole die letzten paar Tausend Zeilen anzeigen kann...]
Choose a number: 139: 446
Choose version:
1: 1.0
2: 1.1
3: 1.2
4: 1.3
Choose a number: 4:
Define value for property 'groupId': : blog.escalation
Define value for property 'artifactId': : example
Define value for property 'version':  1.0-SNAPSHOT: :
Define value for property 'package':  blog.escalation: :
Confirm properties configuration:
groupId: blog.escalation
artifactId: example
version: 1.0-SNAPSHOT
package: blog.escalation
 Y: : Y

Nun erstellen wir ein paar Source-Dateien, eine primitive Applikation im main-Ordner, und eine ScalaTest-Suite im test-Ordner:

So sollte die Struktur des Projektes aussehen:

--example
  |--.gitignore
  |--pom.xml
  `--src
     |--main
     |  `--scala
     |     `--blog
     |        `--escalation
     |           `--App.scala
     `--test
        `--scala
           `--mytestpackage
              `--MyAppSuite.scala

Die ersten beiden Dateien wurden von archetype erzeugt, der src-Ordner wurde ebenfalls automatisch angelegt. Den mytestpackage-Ordner muss man extra erzeugen: Das soll nur zeigen, dass man seine Tests so ordnen kann, wie man will.

So könnten die beiden Scala-Dateien aussehen:

package blog.escalation

object App {
  
  def foo(x : Array[String]) = x.foldLeft("")((a,b) => a + b)
  
  def main(args : Array[String]) {
    println("Hello World!")
    println("concat arguments = " + foo(args))
  }
}
package mytestpackage

import org.scalatest.FunSuite
import org.scalatest.matchers.ShouldMatchers
import blog.escalation.App._

class MyAppSuite extends FunSuite with ShouldMatchers{
  
  test("concatenate words"){
    foo(Array("hello", "world")) should equal ("helloworld")
  }
  
  test("reverse hello world"){
    "dlrow olleh".reverse should equal ("hello world")
  }
  
  test("relativity theory test"){
    val E = 90
	val m = 10
    val c = 3
	E should equal (m * c * c)
  }
  
  test("string theory test")(pending)
}

Nun der wichtigste Schritt: pom.xml anpassen. Bevor wir ins Detail gehen, hier die gesamte POM:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>blog.escalation</groupId>
  <artifactId>example</artifactId>
  <version>1.0-SNAPSHOT</version>
  <name>${project.artifactId}</name>
  <description>My wonderfull scala app</description>
  <inceptionYear>2010</inceptionYear>
  <licenses>
    <license>
      <name>My License</name>
      <url>http://....</url>
      <distribution>repo</distribution>
    </license>
  </licenses>

  <properties>
    <maven.compiler.source>1.5</maven.compiler.source>
    <maven.compiler.target>1.5</maven.compiler.target>
    <encoding>UTF-8</encoding>
    <scala.version>2.9.0</scala.version>
  </properties>

  <repositories>
    <repository>
      <id>scala-tools.org</id>
      <name>Scala-Tools Maven2 Repository</name>
      <url>http://scala-tools.org/repo-releases</url>
    </repository>
  </repositories>

  <pluginRepositories>
    <pluginRepository>
      <id>scala-tools.org</id>
      <name>Scala-Tools Maven2 Repository</name>
      <url>http://scala-tools.org/repo-releases</url>
    </pluginRepository>
  </pluginRepositories>

  <dependencies>
  
    <!-- necessary for compilation of scala code -->
    <dependency>
      <groupId>org.scala-lang</groupId>
      <artifactId>scala-library</artifactId>
      <version>${scala.version}</version>
    </dependency>

    <!-- IMPORTANT: add dependencies that are necessary for testing -->  
    <dependency>
      <groupId>org.scalatest</groupId>
      <artifactId>scalatest_2.9.0</artifactId>
      <version>1.6.1</version>
      <scope>test</scope>
    </dependency>
	
	<!-- if you don't need junit, you can leave this dependency out, but maven will download it anyway
	<dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.8.1</version>
      <scope>test</scope>
    </dependency>
    -->
	
  </dependencies>

  <build>
    <sourceDirectory>src/main/scala</sourceDirectory>
    <testSourceDirectory>src/test/scala</testSourceDirectory>
    
	<!-- IMPORTANT: add this pluginManagement-tag to your POM -->
	<pluginManagement>
	  <plugins>
	    <plugin>
	      <groupId>com.jteigen</groupId>
	      <artifactId>maven-scalatest-plugin</artifactId>
	      <version>1.1-SNAPSHOT</version>
	    </plugin>
	  </plugins>
	</pluginManagement>
	      
	<plugins>
      <plugin>
        <groupId>org.scala-tools</groupId>
        <artifactId>maven-scala-plugin</artifactId>
        <version>2.15.0</version>
        <executions>
          <execution>
            <goals>
              <goal>compile</goal>
              <goal>testCompile</goal>
            </goals>
            <configuration>
              <args>
                <arg>-make:transitive</arg>
                <arg>-dependencyfile</arg>
                <arg>${project.build.directory}/.scala_dependencies</arg>
				<arg>-deprecation</arg>
              </args>
            </configuration>
          </execution>
        </executions>
      </plugin>
	  
	  <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>2.6</version>
        <configuration>
          <useFile>true</useFile>
          <disableXmlReport>true</disableXmlReport>
          <includes>
            <include>**/*Test.*</include>
            <include>**/*Suite.*</include>
			<include>**/*Spec.*</include>
          </includes>
		  <!-- IMPORTANT: if you want to deactivate tests run by surefire-plugin, add this tag: -->
		  <skipTests>true</skipTests>
        </configuration>
      </plugin>
	  
	  <!-- IMPORTANT: this is the plugin we want to use -->
      <plugin>
	    <groupId>com.jteigen</groupId>
	    <artifactId>maven-scalatest-plugin</artifactId>
	    <version>1.1-SNAPSHOT</version>
		<executions>
          <execution>
            <goals>
              <goal>test</goal>
            </goals>
          </execution>
        </executions>
		<!-- this is the standard way to configure maven-plugins: by adding "configuration"-tag -->
        <configuration>
		  
		  <!-- tell the plugin where your classes are -->
		  <runpath>target/test-classes,target/classes</runpath>
		  
		  <!-- this just finds all Suites in mytestpackage-package of your test-classes -->
		  <wildcards>mytestpackage</wildcards>
		  
		  <!-- "W" means "Without color", it removes the ANSI-color-chars from the output -->
		  <stdout>WD</stdout>
		  
        </configuration>		
	  </plugin>
    </plugins>
  </build>
  
  
  <reporting>
    <!-- This is necessary for creating scaladoc -->
    <plugins>
      <plugin>
        <groupId>org.scala-tools</groupId>
        <artifactId>maven-scala-plugin</artifactId>
		<version>2.15.0</version>
      </plugin>
    </plugins>
  </reporting>
</project>

Diese pom.xml unterscheidet sich von der ursprünglichen an vielen Stellen, hier die wichtigsten Punkte:

  1. scala.version ist auf 2.9.0 hochgesetzt
  2. scalatest-dependencies sind hinzugefügt
  3. pluginmanagement-Tag im build-Tag hinzugefügt
  4. maven-surefire-plugin mit dem skipTests-Tag deaktiviert.
  5. maven-scalatest-plugin hinzugefügt und konfiguriert.

An dieser Stelle einige Worte zur Konfiguration des Plugins. Leider scheint noch keine Beschreibung zu existieren, aber man kann alles nötige aus dem Source-Code herleiten. Beachte dazu folgendes:

  1. Die Konfiguration des maven-scalatest-plugins sieht komplett anders aus, als die Konfiguration des Surefire-Plugins, copy-paste ist zwecklos.
  2. Letztendlich leitet das Plugin alle Parameter an org.scalatest.tools.Runner weiter, deshalb ist es empfehlenswert, sich die Dokumentation anzusehen, damit man weiß, was man alles überhaupt konfigurieren kann.
  3. Alle child-Tags des configuration-Tags werden wie hier beschrieben 1:1 auf Member-Variablen des Plugins gemappt.
  4. Informationen über die relevanten Member-Variablen kann man meiner Meinung nach am besten direkt aus dem Source-Code des Plugins ablesen.

Einige Beispiele:
1) runpath

<runpath>target/test-classes,target/classes</runpath>

setzt die runpath-Variable so, dass das Plugin weiss, wo er nach class-Dateien zu suchen hat. Den Kommentaren im Source-Code entnimmt man unter anderem, dass die einzelnen Einträge durch Kommata getrennt werden müssen (und nicht etwa durch Semikolons, oder gar separate Tags).

2) stdout

<stdout>WD</stdout>

Ausgabe-Optionen (mehr dazu unter Configuring reporters). Das „W“ („without colors“) schaltet beispielsweise die Farben aus, die nicht überall funktionieren, und mit „D“ kann man die „durations“ ausgeben lassen.

3) wildcards und members

<suites>mytestpackage.MyAppSuite</suites>
<members>mytestpackage</members>
<wildcards>mytestpackage,otherpackage.with.subpackage</wildcards>

Damit kann man auswählen, wo nach Suites gesucht werden soll. Mit suites kann man eine kommagetrennte Liste von ganz konkreten Suites angeben, was besonders nützlich ist, wenn man die Suites so gruppiert. Mit dem members-Tag kann man eine kommagetrennte Liste von Packages angeben, in der nach Suites gesucht wird. Das wildcards-Tag ist ähnlich, durchsucht aber auch Subpackages, mehr dazu unter Specifying „members-only“ and „wildcard“ Suite paths.

Jetzt, wo die POM fertig ist, können wir beispielsweise alles compilieren und testen:

>mvn test

…was uns die gewünschte Ausgabe liefert:

Run starting. Expected test count is: 4
DiscoverySuite:
MyAppSuite:
- concatenate words (14 milliseconds)
- reverse hello world (2 milliseconds)
- relativity theory test (1 millisecond)
- string theory test (pending)
Run completed in 364 milliseconds.
Total number of tests run: 3
Suites: completed 2, aborted 0
Tests: succeeded 3, failed 0, ignored 0, pending 1
All tests passed.

Wer möchte, kann auch gleich alles verpacken:

>mvn package

oder beispielsweise eine schicke (bei dem Projekt natürlich erbärmlich kleine) Dokumentation erstellen:

>mvn scala:doc

…die dann im site-Ordner abgelegt wird.

Ich bin jetzt mit dem Zusammenspiel von Scala und Maven vorerst zufrieden, und hoffe, dass dieser Beitrag sich für jemanden als hilfreich erweist.

Grüße, 0x7F800000

Hier nach das Beispielprojekt zum Download, das jar kann mit

jar -xf example.jar

entpackt werden.

Der Autor weist noch darauf hin, dass es wahrscheinlich schon bald (vielleicht in ein paar Wochen) bessere Lösungen geben wird. Ich möchte mich für die Erlaubnis bedanken, diesen Artikel hier veröffentlichen zu dürfen. Wie immer sind Kritik und Verbesserungsvorschläge jederzeit willkommen.

Hinterlasse einen Kommentar

Diese Seite verwendet Akismet, um Spam zu reduzieren. Erfahre, wie deine Kommentardaten verarbeitet werden..