Testen in Java. Spock Framework

  • Tutorial

In früheren Artikeln zu den Beispielen von JUnit und TestNG habe ich testgetriebene Entwicklung (TDD) und datengetriebenes Testen (DDT) erwähnt . Es gibt jedoch einen anderen Ansatz, der immer beliebter wird: die verhaltensorientierte Entwicklung (BDD) . Dies ist eine solche Entwicklung der TDD-Technologie, bei der der Test nicht als Testen einiger Systemkomponenten, sondern als funktionale Anforderungen angesehen wird. Wenn ein TDD Konzepte wie einen Test oder eine Methode verwendet, ist dies für BDD eine Spezifikation und eine Anforderung. Über diese Technik wurde bereits früher auf einem Habr gesprochen:

Dieser Ansatz ist sowohl mit JUnit als auch mit TestNG anwendbar. Es gibt aber auch andere Tools, die speziell auf BDD zugeschnitten sind. In diesem Artikel werde ich über einen solchen Rahmen sprechen. Es heißt Spock Framework und kombiniert nicht nur die Prinzipien von BDD, sondern auch die Vorzüge von Groovy . Ja, es ist Groovy. Obwohl Groovy verwendet wird, wird es auch zum Testen von Java-Code verwendet. Anwendungsbeispiele sind Spring, Grails, Tapestry5. Interessant? Dann lesen Sie weiter.


Verhaltensorientierte Entwicklung


Lassen Sie mich Sie daran erinnern, was es ist. Betrachten Sie ein Beispiel. Gibt es ein Dienstprogramm, das mit Ameisenmustern funktioniert (dies sind die zum Abrufen von Dateien)? - ein beliebiges 1 und nur 1 Zeichen, * - eine beliebige Anzahl beliebiger Zeichen, ** - ein beliebiger Pfad. Es sieht ungefähr so ​​aus:

public abstract class PathUtils {
  public static boolean matchAntPath(final String path, final String pattern) {
    // ...
  }
  public static boolean matchAntPattern(final String path, final String pattern) {
    // ...
  }
}

Beide Methoden prüfen, ob die übergebene Zeichenfolge mit dem Muster übereinstimmt oder nicht. Die matchAntPattern- Methode berücksichtigt jedoch nur das lokale Muster, ohne den Pfad zu berücksichtigen. MatchAntPath übernimmt den vollständigen Pfad. Nach den Prinzipien von TDD wird für jede Methode ein Test mit einigen Eingabedaten und einigen erwarteten Ergebnissen erstellt.

public class TestPathUtils extends Assert {
  @Test(dataProvider = "matchAntPatternData")
  public void testMatchAntPattern(final String pattern, final String text, final boolean expected) {
    final boolean actual = PathUtils.matchAntPattern(text, pattern);
    assertEquals(actual, expected);
  }
  @Test(dataProvider = "matchAntPathData")
  public void testMatchAntPath(final String pattern, final String path, final boolean expected) {
    final boolean actual = PathUtils.matchAntPath(path, pattern);
    assertEquals(actual, expected);
  }
}

Möglicherweise werden hier auch Tests auf falsche Parameter hinzugefügt, wenn Ausnahmen ausgelöst werden sollen. Betrachten wir es nun aus Sicht von BDD.
Ein Test ist nicht nur ein Test, sondern eine Spezifikation und besteht nicht aus Methoden, sondern aus Anforderungen. Markieren Sie die Anforderungen für PathUtils :
  • Symbol? in der Vorlage sollte einem beliebigen Zeichen in der zu prüfenden Zeichenfolge entsprechen
  • Symbol? in der Vorlage sollte 1 entsprechen und nur 1 Zeichen in der zu prüfenden Zeichenfolge
  • Das * -Zeichen im Muster muss einem beliebigen Zeichen in der zu prüfenden Zeichenfolge entsprechen
  • Das * -Zeichen im Muster muss einer beliebigen Anzahl von Zeichen in der zu prüfenden Zeichenfolge entsprechen
  • Die Werte der Vorlage und der zu prüfenden Zeichenfolge dürfen nicht null sein

Darüber hinaus verfügt jede Anforderung über ein eigenes Überprüfungsskript, für das normalerweise die Begriffe verwendet werden, die wann angegeben werden. Gegeben - Einstellungen für den Start des Skripts, wenn - der Grund, dann - die Bedingung für das Skript. Zum Beispiel:

Given:
PathUtils
---
When:
matchAntPattern(null, "some string")
---
Then:
NullPointerException should be thrown

Der Test sieht also ungefähr so ​​aus:

public class PathUtilsSpec extends Assert {
  @Test
  public void question_character_should_mean_any_character() {
    assertTrue(PathUtils.matchAntPattern("abb", "a?b"));
    assertTrue(PathUtils.matchAntPattern("a1b", "a?b"));
    assertTrue(PathUtils.matchAntPattern("a@b", "a?b"));
    assertTrue(PathUtils.matchAntPath("abb", "a?b"));
    assertTrue(PathUtils.matchAntPath("a1b", "a?b"));
    assertTrue(PathUtils.matchAntPath("a@b", "a?b"));
    // ...
  }
  @Test
  public void question_character_should_mean_only_one_character() {
    assertFalse(PathUtils.matchAntPattern("ab", "a?b"));
    assertFalse(PathUtils.matchAntPattern("aabb", "a?b"));
    assertFalse(PathUtils.matchAntPath("ab", "a?b"));
    assertFalse(PathUtils.matchAntPath("aabb", "a?b"));
    // ...
  }
  @Test
  public void asterisk_character_should_mean_any_character() {
    assertTrue(PathUtils.matchAntPattern("abb", "a*b"));
    assertTrue(PathUtils.matchAntPattern("a1b", "a*b"));
    assertTrue(PathUtils.matchAntPattern("a@b", "a*b"));
    assertTrue(PathUtils.matchAntPath("abb", "a*b"));
    assertTrue(PathUtils.matchAntPath("a1b", "a*b"));
    assertTrue(PathUtils.matchAntPath("a@b", "a*b"));
    // ...
  }
  @Test
  public void asterisk_character_should_mean_any_number_of_characters() {
    assertTrue(PathUtils.matchAntPattern("ab", "a*b"));
    assertTrue(PathUtils.matchAntPattern("aabb", "a*b"));
    assertTrue(PathUtils.matchAntPath("ab", "a*b"));
    assertTrue(PathUtils.matchAntPath("aabb", "a*b"));
    // ...
  }
  @Test
  public void double_asterisk_character_should_mean_any_path() {
    assertTrue(PathUtils.matchAntPath("aaa/bbb", "aaa/**/bbb"));
    assertTrue(PathUtils.matchAntPath("aaa/ccc/bbb", "aaa/**/bbb"));
    assertTrue(PathUtils.matchAntPath("aaa/c/c/c/bbb", "aaa/**/bbb"));
    // ...
  }
}

Nun mehr zum Spock Framework.

Hauptmerkmale


Wie gesagt, Skripte sind in Groovy geschrieben. Ist es gut oder schlecht? Entscheiden Sie selbst, Anfänger können Groovy in 15 Minuten lesen - eine schnelle Übersicht .

Die Spezifikation sollte von spock.lang.Specification geerbt werden . Es kann Felder, Fixture-Methoden, Feature-Skripte und Hilfsmethoden enthalten.

Felder werden standardmäßig nicht von Skripten gemeinsam genutzt, d. H. Feldänderungen aus einem Szenario sind in einem anderen Szenario nicht sichtbar. Zum Teilen können Sie mit @Shared Anmerkungen machen .

Installationsmethoden sind:
  • setup () - Analogon von @Before in JUnit, ausgeführt vor jedem Skript
  • cleanup () - Analogon von @After in JUnit, ausgeführt nach jedem Skript
  • setupSpec () - ein Analogon von @BeforeClass in JUnit, das vor dem ersten Skript in der Spezifikation ausgeführt wird
  • cleanupSpec () - ein Analogon von @AfterClass in JUnit, das nach dem letzten Skript in der Spezifikation ausgeführt wird

Wie in anderen Test-Frameworks werden diese Methoden verwendet, um nicht für jedes Skript den gleichen Installationscode zu schreiben.

Anforderungsszenarien sind der Hauptteil der Spezifikation. Hier wird das Verhalten von Komponenten beschrieben. Es ist üblich, sie mit String-Literalen aufzurufen, und Sie können beliebige Zeichen verwenden. Hauptsache, dieser Name beschreibt so klar wie möglich, was dieses Skript tut. Zum Beispiel in unserem Fall:

class PathUtilsSpec extends Specification {
  def "? character should mean any character"() {
    // ...
  }
  def "? character should mean only one character"() {
    // ...
  }
  def "* character should mean any character"() {
    // ...
  }
  def "* character should mean any number of characters"() {
    // ...
  }
  def "** character should mean any path"() {
    // ...
  }
}

Jedes Szenario besteht aus Blöcken, die durch Beschriftungen gekennzeichnet sind:
  • Das Setup entspricht der Installationsmethode setup () und gilt nur für ein bestimmtes Szenario. Muss sich vor den verbleibenden Blöcken befinden und sollte nicht wiederholt werden. Das Setup- Label fehlt möglicherweise. Sie können auch angegeben anstelle von Setup schreiben. Dies dient der besseren Lesbarkeit (gegeben-wann-dann).
  • Die Bereinigung entspricht der Methode cleanup () und gilt nur für ein bestimmtes Skript. Muss sich am Ende des Skripts vor dem where- Block befinden , falls vorhanden, und sollte nicht wiederholt werden
  • Wann-Dann ist der Grund und die Bedingung für die Ausführung. Im Wann- Teil werden normalerweise Variablen deklariert, die erforderlichen Aktionen ausgeführt, im Teil werden dann einige Bedingungen überprüft. Dies kann das Überprüfen von Bedingungen, das Überprüfen des Auslösens einer Ausnahme oder das Warten auf die Ausführung einiger Methoden für Scheinobjekte sein. Dieser Block kann wiederholt werden, aber die Autoren des Frameworks empfehlen, sich nicht einzumischen. Ein gutes Skript sollte 1 bis 5 solcher Blöcke enthalten
  • Expect ist ein vereinfachter When-Then- Block, in dem Aktion und Validierung im selben Ausdruck enthalten sind
  • Dabei handelt es sich um ein Analogon von @DataProvider von TestNG, mit dem ein Datensatz für einen Test erstellt werden soll

Nun zu allem im Detail. Betrachten Sie ein anderes Beispiel. PathSearcher wurde zum Durchsuchen von Dateien entwickelt und verwendet Ameisenmuster als Filter für Dateien.

public class PathSearcher {
  public PathSearcher(final String path) {...}
  public PathSearcher include(final String... patterns) {...}
  public PathSearcher exclude(final String... patterns) {...}
  public Set search() {...}
}

Wir schreiben die Anforderung "muss nach Dateien im Dateisystem suchen":

class PathSearcherSpec extends Specification {
  def "it should search files under the file system"() {
    given:
    def searcher = PathSearcher.create(inClasspath("test1"))
    when:
    def results = searcher.search();
    then:
    results.containsAll(["1.txt", "2.txt"]);
    results.size() == 2
  }
  private String inClasspath(path) {
    return ClassLoader.getSystemResource(path).toExternalForm()
  }
}

Also ist es gegeben - eine Suchmaschine, die im Ordner test1 aus dem Klassenpfad sucht , die Suche überprüft, Ausführungsbedingung - die Suchmaschine sollte unsere Dateien finden. inClasspath ist eine Hilfsmethode , die den absoluten Pfad einer Datei aus dem Klassenpfad zurückgibt .

Ein weiteres Beispiel für PathUtils "Die Werte der Vorlage und der zu prüfenden Zeichenfolge dürfen nicht null sein."

class PathUtilsSpec extends Specification {
  def "null parameter values are not allowed"() {
    when:
    PathUtils.matchAntPattern(null, "some string")
    then:
    thrown(NullPointerException)
    when:
    PathUtils.matchAntPattern("some string", null)
    then:
    thrown(NullPointerException)
    when:
    PathUtils.matchAntPath(null, "some string")
    then:
    thrown(NullPointerException)
    when:
    PathUtils.matchAntPath("some string", null)
    then:
    thrown(NullPointerException)
  }
}

Hier sehen wir ein Verfahren Thrown (...) , ist es die Erwartung der genannten Ausnahmen gibt es auch ein Verfahren notThrown (...) und noExceptionThrown () . Sie sollen überprüfen, ob eine bestimmte / keine Ausnahme ausgelöst wird. Auch im damaligen Teil gibt es möglicherweise Erwartungen an einige Methoden für Scheinobjekte, aber etwas später. Ein weiteres Beispiel:

class PathUtilsSpec extends Specification {
  def "? character should mean any character"() {
    expect:
    PathUtils.matchAntPattern("abb", "a?b")
    PathUtils.matchAntPattern("a1b", "a?b")
    PathUtils.matchAntPattern("a@b", "a?b")
    PathUtils.matchAntPath("abb", "a?b")
    PathUtils.matchAntPath("a1b", "a?b")
    PathUtils.matchAntPath("a@b", "a?b")
  }
}

Wie Sie dem Beispiel entnehmen können, ist es bequemer, den Expect- Block zu verwenden , wenn sowohl wann als auch dann Teile zu einer Bedingung kombiniert werden können . Dieses Szenario kann verbessert werden, indem es mit dem where- Block parametrierbar gemacht wird :

class PathUtilsSpec extends Specification {
  def "? character should mean any character"() {
    expect:
    PathUtils.matchAntPattern(text, pattern)
    PathUtils.matchAntPath(text, pattern)
    where:
    pattern | text
    "ab?"   | "abc"
    "ab?"   | "ab1"
    "ab?"   | "ab@"
    "a?b"   | "abb"
    "a?b"   | "a1b"
    "a?b"   | "a@b"
    "?ab"   | "aab"
    "?ab"   | "1ab"
    "?ab"   | "@ab"
  }
}

Oder so:

class PathUtilsSpec extends Specification {
  def "? character should mean any character"() {
    expect:
    PathUtils.matchAntPattern(text, pattern)
    PathUtils.matchAntPath(text, pattern)
    where:
    pattern << ["ab?", "ab?", "ab?", "a?b", "a?b", "a?b", "?ab", "?ab", "?ab"]
    text    << ["abc", "ab1", "ab@", "abb", "a1b", "a@b", "aab", "1ab", "@ab"]
  }
}

Oder so:

class PathUtilsSpec extends Specification {
  def "? character should mean any character"() {
    expect:
    PathUtils.matchAntPattern(text, pattern)
    PathUtils.matchAntPath(text, pattern)
    where:
    [pattern, text] << [
        ["ab?", "abc"],
        ["ab?", "ab1"],
        ["ab?", "ab@"],
        ["a?b", "abb"],
        ["a?b", "a1b"],
        ["a?b", "a@b"],
        ["?ab", "aab"],
        ["?ab", "1ab"],
        ["?ab", "@ab"]
    ]
  }
}

Oder sogar so:

class PathUtilsSpec extends Specification {
  def "? character should mean any character"() {
    expect:
    PathUtils.matchAntPattern(text, pattern)
    PathUtils.matchAntPath(text, pattern)
    where:
    [pattern, text] = sql.execute("select pattern, text from path_utils_test")
  }
}

Ich denke, aus den Beispielen geht alles hervor, deshalb werde ich mich nicht darauf konzentrieren. Ich stelle nur fest, dass Sie im where- Block keine Felder verwenden können, die nicht als @Shared markiert sind .

Interaktionen


Mit dem Framework können Sie unter anderem mit Scheinobjekten ohne zusätzliche Abhängigkeiten arbeiten. Sie können Mokas für Schnittstellen und nicht endgültige Klassen erstellen. Die Kreation sieht folgendermaßen aus:

    def dao1 = Mock(UserDAO)
    UserDAO dao2 = Mock()

Sie können Rückgabewerte oder die Methoden solcher Objekte selbst überschreiben. Autoren nennen diese Interaktionen.

    dao1.findAll() >> [
        new User(name: "test1", description: "Test User"),
        new User(name: "test2", description: "Test User"),
        new User(name: "test3", description: "Test User")
    ]
    dao2.findAll() >> { throw new UnsupportedOperationException() }

Interaktionen sind lokal (im then- Block definiert) und global (an anderer Stelle definiert). Lokale sind nur im then- Block verfügbar, globale sind ab dem Punkt ihrer Definition überall verfügbar. Auch für lokale Interaktionen können Sie deren Leistung angeben. Dies ist die erwartete Anzahl von Methodenaufrufen.

class UserCacheSpec extends Specification {
  def users = [
      new User(name: "test1", description: "Test User"),
      new User(name: "test2", description: "Test User"),
      new User(name: "test3", description: "Test User")
  ]
  def "dao should be used only once for all user searches until invalidated"() {
    setup:
    def dao = Mock(UserDAO)
    def cache = new UserCacheImpl(dao)
    when:
    cache.getUser("test1")
    cache.getUser("test2")
    cache.getUser("test3")
    cache.getUser("test4")
    then:
    1 * dao.findAll() >> users
  }
}

In diesem Beispiel erstellen wir mit diesem Modell (Setup-Block) ein Modell für UserDAO und ein echtes UserCache- Objekt . Dann suchen wir nach mehreren Benutzern nach Namen ( when- Block) und überprüfen schließlich, ob die findAll- Methode, die das vorbereitete Ergebnis zurückgibt, nur einmal aufgerufen wird.
Bei der Beschreibung von Interaktionen können Sie Vorlagen verwenden:

    1 * dao.findAll() >> users
    (1..4) * dao.findAll() >> users
    (2.._) * dao.findAll() >> users
    (_..4) * dao.findAll() >> users
    _.findAll() >> users
    dao./find.*/(_) >> users

Lesen Sie hier mehr .

Zusätzliche Funktionen


Wie Sie sehen können, verfügt das Framework bereits über viele Funktionen. Aber wie bei anderen Frameworks besteht die Möglichkeit, die Funktionalität zu erweitern. Beispiele sind integrierte Erweiterungen:
  • @Timeout - Legt das maximale Timeout für das Skript fest, analog zum Timeout- Attribut von @Test von JUnit
  • @Ignore - Deaktiviert das Skript, analog zu @Ignore von JUnit
  • @IgnoreRest - Deaktiviert alle Skripte mit Ausnahme von Anmerkungen. Dies ist nützlich, wenn Sie nur einen Test überprüfen müssen
  • @FailsWith - Legt die erwartete Ausnahme analog zum erwarteten Attribut bei @Test von JUnit fest
  • @Unroll - Gibt an, dass parametrisierte Skripte als separate Skripte für jede Iteration angegeben werden sollen. Hier können Sie auch eine Vorlage für den Anforderungsnamen angeben. Standardmäßig lautet sie "#featureName [#iterationCount]".

class InternalExtensionsSpec extends Specification {
  @FailsWith(NumberFormatException)
  @Unroll("#featureName (#data)")
  def "integer parse method should throw exception for wrong parameters"() {
    Integer.parseInt(data)
    where:
    data << ["Hello, World!!!", "0x245", "1798237199878129387197238"]
  }
  @Ignore
  @Timeout(3)
  def "temporary disabled feature"() {
    setup:
    sleep(20000)
  }
}

Integrationen mit anderen Frameworks erfolgen in separaten Modulen:
  • Spring - Die Spezifikation wird mit @ContextConfiguration (location = "application_context_xml") mit Anmerkungen versehen, und Abhängigkeiten können mit @Autowired in Felder eingefügt werden

    @ContextConfiguration(locations = "context.xml")
    class SpringIntegrationSpec extends Specification {
      @Autowired
      String testSymbol
      def "test-symbol should be spring"() {
        expect:
        testSymbol == "spring"
      }
    }
    

  • Guice - Die Spezifikation wird mit @UseModules (guice_module_class) mit Anmerkungen versehen, und Abhängigkeiten können mit @Inject in Felder eingefügt werden

    public class GuiceModule extends AbstractModule {
      @Override
      protected void configure() {
        bind(String.class).annotatedWith(Names.named("test-symbol")).toInstance("guice");
      }
    }
    @UseModules(GuiceModule)
    class GuiceIntegrationSpec extends Specification {
      @Inject
      @Named("test-symbol")
      String testSymbol
      def "test-symbol should be guice"() {
        expect:
        testSymbol == "guice"
      }
    }
    

  • Tapisserie - Die Spezifikation wird mit @SubModule (tapestry_module_class) kommentiert, und Abhängigkeiten können mithilfe der Annotation @Inject in Felder eingebettet werden

    public class TapestryModule {
      public void contributeApplicationDefaults(final MappedConfiguration configuration) {
        configuration.add("test-symbol", "tapestry");
      }
    }
    @SubModule(TapestryModule)
    class TapestryIntegrationSpec extends Specification {
      @Inject
      @Symbol("test-symbol")
      String testSymbol
      def "test-symbol should be tapestry"() {
        expect:
        testSymbol == "tapestry"
      }
    }
    


Wenn Sie Ihre eigenen Funktionen benötigen, können Sie Ihre eigenen Erweiterungen hinzufügen. Schlüsselklassen zur Erweiterung der Funktionalität:
  • IMethodInterceptor, IMethodInvocation - Mit der ersten Proxy-Spezifikationsmethode können Sie Ihren Code vor und nach dem Methodenaufruf hinzufügen. Um die Arbeit zu vereinfachen, können Sie die AbstractMethodInterceptor- Klasse verwenden . Die zweite ist ab der ersten verfügbar und dient zur Arbeit mit der ursprünglichen (Proxy-) Methode
  • IGlobalExtension - Ermöglicht das Arbeiten mit Spezifikationsmetadaten ( SpecInfo ). Hier können Sie Metadaten für alle Spezifikationskomponenten (Felder, Installationsmethoden, Anforderungsskripts) anzeigen und Ihre eigenen Interceptors hinzufügen
  • IAnnotationDrivenExtension - wie die vorherige vereinfacht nur die Aufgabe. Wenn unsere Erweiterung an eine bestimmte Anmerkung gebunden ist, können Sie die AbstractAnnotationDrivenExtension- Klasse verwenden, um die Arbeit zu vereinfachen

Um Ihre eigene Erweiterung zu erstellen, müssen Sie eine IGlobalExtension- oder IAnnotationDrivenExtension-Nachkommenklasse erstellen , in der Ihr IMethodInterceptor höchstwahrscheinlich zu den Spezifikationskomponenten hinzugefügt wird , und schließlich die spi-Erweiterung zu META-INF / services / org.spockframework.runtime.extension.IGlobalExtension für IGlobalExtension hinzufügen Für IAnnotationDrivenExtension muss unsere Annotation mit @ExtensionAnnotation (extension_class) kommentiert werden .
Ein Beispiel für eine Erweiterung, die ein Skript eine bestimmte Anzahl von Malen ausführt:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
@ExtensionAnnotation(RepeatExtension.class)
public @interface Repeat {
  int value() default 1;
}
public class RepeatExtension extends AbstractAnnotationDrivenExtension {
  @Override
  public void visitFeatureAnnotation(Repeat annotation, FeatureInfo feature) {
    feature.addInterceptor(new RepeatInterceptor(annotation.value()));
  }
}
public class RepeatInterceptor extends AbstractMethodInterceptor{
  private final int count;
  public RepeatInterceptor(int count) {
    this.count = count;
  }
  @Override
  public void interceptFeatureExecution(IMethodInvocation invocation) throws Throwable {
    for (int i = 0; i < count; i++) {
      invocation.proceed();
    }
  }
}


class CustomExtensionsSpec extends Specification {
  @Repeat(10)
  def "custom extension"() {
    expect:
    Integer.parseInt("123") == 123
  }
}


Ausführen von Tests


Aufgrund der Tatsache, dass Spock-Tests mit dem JUnit-Launcher ( Sputnik ) ausgeführt werden, funktionieren sie unter verschiedenen IDEs einwandfrei (wie die Autoren sagen, habe ich nur unter der Idee überprüft). Sie können den Start von Tests auch mit ant, maven, gradle konfigurieren. Alle notwendigen Informationen zu den Einstellungen finden Sie hier .
Ich werde das für mich selbst hinzufügen, ich habe die Konfiguration unter maven ein wenig schamanisiert, weil vorgeschlagen von den Autoren funktionierte nicht unter maven3. Hier ist meine Konfigurationsoption:

4.0.0com.exampletesting-example1.0-SNAPSHOTcom.exampletesting-spock1.0-SNAPSHOTjarTesting Spock Framework Example
    This is an example application that demonstrates Spock Framework usage.
  org.codehaus.groovygroovy-all${groovy-version}testorg.spockframeworkspock-core${spock.version}testsrc/test/groovysrc/test/resourcesorg.apache.maven.pluginsmaven-surefire-plugin**/*Spec.groovyorg.codehaus.gmavengmaven-plugin${gmaven-version}${gmaven-provider}testCompileorg.codehaus.groovygroovy-all${groovy-version}1.7.101.31.70.5-groovy-1.7


Fazit


Trotz der Tatsache, dass ich dieses wundervolle Framework kürzlich kennengelernt habe und praktisch keine Erfahrung damit habe, kann ich mit Zuversicht sagen, dass es in seinen Fähigkeiten nicht minderwertig ist und in einigen Aspekten sogar andere Frameworks übertrifft. Ich habe es wirklich gemocht, Tests auf Groovy zu schreiben, ich habe es gemocht, Tests zu schreiben, die von BDD geleitet werden. Deshalb rate ich Ihnen, es zu versuchen.

Beispiele finden Sie hier .

Literatur



Jetzt auch beliebt: