Wednesday, July 8, 2015

BDD with groovy and Spock

Groovy language is not as "strict" as Java, I don't think it does not makes a sense to create using Groovy complex production applications, because we can have situation when everything is compiling, but not working. It can we very hard to understand where exactly variable type User was used like it has type Task(for example)  :)   But for writing test, groovy - is a very good option, because it can make writing of test much easier, with writing less code.  In this post I'll show how can we test Java code by BDD methodology, using groovy language and spock library. 

Wiki: 

Example how it looks :
def "it should say Hi for regular name"() {
    given:
    def person=new TestJavaClass()

    when :
    def result=person.sayHi("Huan Sebastyan")

    then:
    result!=null    result=="Hi, Huan Sebastyan"}


1. Project structure 

We have 2 Java classes for testing in our project: JavaTestClass.java, AnotherJavaClass.java, and one groovy test file : GroovyTest.groovy. 



2. Main project file(pom.xml)

We have to include some spock-related dependencies into pom file. I commented dependencies which was not used in this project, but can be used in future.


<?xml version="1.0"?>
<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>com.demien</groupId>
  <artifactId>spock</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>jar</packaging>
  <name>Test Spock Project</name>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> 
  </properties>

  <build>
    <plugins>
      <!-- Mandatory plugins for using Spock -->
      <plugin>
        <!-- The gmavenplus plugin is used to compile Groovy code. To learn more about this plugin, 
        visit https://github.com/groovy/GMavenPlus/wiki -->
        <groupId>org.codehaus.gmavenplus</groupId>
        <artifactId>gmavenplus-plugin</artifactId>
        <version>1.4</version>
        <executions>
          <execution>
            <goals>
              <goal>compile</goal>
              <goal>testCompile</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
      <!-- Optional plugins for using Spock -->
      <!-- Only required if names of spec classes don't match default Surefire patterns (`*Test` etc.) -->
      <plugin>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>2.6</version>
        <configuration>
          <useFile>false</useFile>
          <includes>
            <include>**/*Spec.java</include>
          </includes>
        </configuration>
      </plugin>
      <!-- Only required for spock-example build -->
      <plugin>
        <artifactId>maven-deploy-plugin</artifactId>
        <version>2.5</version>
        <configuration>
          <skip>true</skip>
        </configuration>
      </plugin>
    </plugins>
  </build>

  <dependencies>
    <!-- Mandatory dependencies for using Spock -->
    <dependency>
      <groupId>org.spockframework</groupId>
      <artifactId>spock-core</artifactId>
      <version>1.0-groovy-2.4</version>
      <scope>test</scope>
    </dependency>
    <!-- Optional dependencies for using Spock -->
    <dependency> <!-- use a specific Groovy version rather than the one specified by spock-core -->
      <groupId>org.codehaus.groovy</groupId>
      <artifactId>groovy-all</artifactId>
      <version>2.4.1</version>
    </dependency>
  <!-- enables mocking of classes (in addition to interfaces) -->
    <dependency>
      <groupId>cglib</groupId>
      <artifactId>cglib-nodep</artifactId>
      <version>3.1</version>
      <scope>test</scope>
    </dependency>

  <!-- enables mocking of classes without default constructor (together with CGLIB)
    <dependency>
      <groupId>org.objenesis</groupId>
      <artifactId>objenesis</artifactId>
      <version>2.1</version>
      <scope>test</scope>
    </dependency>
    -->
  <!-- only required if Hamcrest matchers are used
    <dependency>
      <groupId>org.hamcrest</groupId>
      <artifactId>hamcrest-core</artifactId>
      <version>1.3</version>
      <scope>test</scope>
    </dependency>
    -->
    <!-- Dependencies used by examples in this project (not required for using Spock)
    <dependency>
      <groupId>com.h2database</groupId>
      <artifactId>h2</artifactId>
      <version>1.4.185</version>
    </dependency>
    -->
  </dependencies>


  <!-- Only required if a snapshot version of Spock is used
  <repositories>

    <repository>
      <id>spock-snapshots</id>
      <url>https://oss.sonatype.org/content/repositories/snapshots/</url>
      <snapshots>
        <enabled>true</enabled>
      </snapshots>
    </repository>
  </repositories>
  -->

  <!-- Only required in spock-example build
  <distributionManagement>
    <repository>
      <id>foo</id>
      <url>file:///fake.repository.to.make.maven.happy</url>
    </repository>
    <snapshotRepository>
      <id>bar</id>
      <url>file:///fake.repository.to.make.maven.happy</url>
    </snapshotRepository>
  </distributionManagement>
  -->
</project>

3. JavaTestClass

It's a very simple class, it just has 2 methods : sayHi and getHistory(returns List with history of invocatons). 


package com.demien.spock;

import java.util.ArrayList;
import java.util.List;

public class TestJavaClass {

    private List<String> history=new ArrayList<String>();

    public void checkName(String name) {
        if (name==null) {
            throw new IllegalArgumentException("name is null!");
        }
    }

    public String sayHi(String name) {
        checkName(name);
        history.add(name);
        return "Hi, "+name;
    }

    public List<String> getHistoryItems() {
        return history;
    }
}


4.AnotherJavaClass

Just another simple class which use JavaTestClass for method intruduceYourself. Also it use getHistory method for transforming result from JavaTestClass to String(from List). 


package com.demien.spock;

public class AnotherJavaClass {
    TestJavaClass testJavaClass;

    public void setTestJavaClass(TestJavaClass testJavaClass) {
        this.testJavaClass = testJavaClass;
    }

    public String introduceYourself() {
        return testJavaClass.sayHi("I'm AnotherJavaClass");
    }

    public String getHistory() {
        StringBuilder result=new StringBuilder();
        for (String s:testJavaClass.getHistoryItems()) {
            if (result.length()>0) {
                result.append(",");
            }
            result.append(s);
        }
        return result.toString();
    }
}

5. Groovy test

By comments in code I tried to explain everything what is going on. I believe, it's easy to understand.


package com.demien.spock

import spock.lang.Specification;

class GroovyTest extends Specification {

    def "it should say Hi for regular name"() {
        given:
        def person=new TestJavaClass()

        when :
        def result=person.sayHi("Huan Sebastyan")

        then:
        result!=null
        result=="Hi, Huan Sebastyan"
    }

    def "it should thrown an error if name is null"() {
        given:
        def person=new TestJavaClass()

        when :
        def result=person.sayHi(null)

        then:
        thrown(IllegalArgumentException)
    }

    // mock - is a "empty" class: all methods are returning null.
    def "check method invocation count"() {
        given:
        TestJavaClass mock=Mock()
        AnotherJavaClass anotherJavaClass=new AnotherJavaClass(testJavaClass: mock)

        when:
        mock.sayHi("Test Name") // first invocation
        anotherJavaClass.introduceYourself() // second invocation

        then:
        2*mock.sayHi(_)
    }

    //  you have to re-define each method you want to use for mock
    def "Mocked class used in another class should return mocked value"() {
        setup :
        TestJavaClass mock=Mock()
        mock.sayHi(_)>>"EMPTY"
        AnotherJavaClass anotherJavaClass=new AnotherJavaClass(testJavaClass: mock)

        when:
        String result=anotherJavaClass.introduceYourself()

        then:
        result=="EMPTY"
    }

    def "Mocked class should return proper mocked history"() {
        setup :
        TestJavaClass mock=Mock()
        mock.getHistoryItems()>>["one", "two", "three"]
        AnotherJavaClass anotherJavaClass=new AnotherJavaClass(testJavaClass: mock)

        when:
        String result=anotherJavaClass.getHistory()

        then:
        result=="one,two,three"
    }

    // stubbed class - "empty" class like mock, but methods are not returning values at all
    // so, like in mocks, you have to re-define each method you want to use
    def "stubbed class used in another class"() {
        setup:
        TestJavaClass stub=Stub()

        stub.sayHi(_)>>{String name->
            "Hello, "+name
        }
        AnotherJavaClass anotherJavaClass=new AnotherJavaClass(testJavaClass: stub)

        when:
        String result=anotherJavaClass.introduceYourself()

        then:
        result=="Hello, I'm AnotherJavaClass"
    }

    // spy - is a "wrapper" - 100% working copy of class
    //  but you can re-define methods if you want
    def "spy test"() {
        setup:
        TestJavaClass spy=Spy()
        spy.checkName(null)>>{} // now calling this method with (null) will not cause exeption

        AnotherJavaClass anotherJavaClass=new AnotherJavaClass(testJavaClass: spy)

        when:
        String resultFromClass=anotherJavaClass.introduceYourself()
        String resultFromSpy=spy.sayHi(null)


        then:
        resultFromClass=="Hi, I'm AnotherJavaClass"
        resultFromSpy=="Hi, null" // exception was not thrown
    }

}

6. Conclusion.

I think groovy is becoming more and more popular for writing test, and spock framework will help it with that! Source code can be downloaded from here.




No comments:

Post a Comment