[JAVA] Let's introduce spock framework and write unit tests like test driven

Introduction

If you try using a testing framework called spock framework for unit testing in Java I would like to introduce it because it was more convenient than I expected. Click here for the official website → spock framework

Since there are places where the flow until introduction is a little difficult to understand, I tried to write an article from the first introduction and actually writing a test using spock. I'm glad if you can think of spock yeah.

By the way, my environment is Ubuntu 18.04 LTS + eclipse, Of course you can use spock in Windows environment. Is that so?

Writer environment

OS: Ubuntu 18.04 LTS IDE: eclipse

Environment creation

Introducing groovy

Introduced groovy-eclipse

Since we will write the test code with groovy this time, we will introduce a plugin to handle groovy in eclipse.

Help > Install New Software > groovyのアドインを入れる1.PNG

For the URL, refer to this site (groovy-eclipse) and use the one that corresponds to the version of eclipse you are using. Check the Main Package and Finish.

Test creation

Make a project

This time, I will use gradle to proceed quickly. Go to any directory where you want to place the source code and run gradle.

$ gradle init --type java-application --test-framework spock

that's all. You now have a project that uses spock for unit testing. Easy!

Add spock to an existing project

If it is not new, add the description to the existing build.gradle (pom.xml for maven).

build.gradle


plugins {
    id 'groovy'
}

repositories {
    jcenter()
}

dependencies {
    testImplementation 'org.codehaus.groovy:groovy-all:2.4.17'
    testImplementation 'org.spockframework:spock-core:1.0-groovy-2.4'
    testImplementation 'junit:junit:4.12'
}

Google Code Archive - Long-term storage for Google Code Project Hosting. It's crumbled, so keep the shaped one.

Spock version Groovy version JUnit version Grails version Spring version
0.5-groovy-1.6 1.6.1-1.6.x 4.7-4.x 1.2.0-1.2.x 2.5.0-3.x
0.5-groovy-1.7 1.7.0-1.7.x 4.7-4.x 1.3.0-1.3.x 2.5.0-3.x
0.6-groovy-1.7 1.7.0-1.7.x 4.7-4.x 1.3.0-1.3.x 2.5.0-3.x
0.6-groovy-1.8 1.8.1-1.8.x 4.7-4.x 2.0-2.x 2.5.0-3.x
0.7-groovy-1.8 1.8.1-1.8.x 4.7-4.x 2.0-2.x 2.5.0-3.x
0.7-groovy-2.0 2.0.0 -2.x.x 4.7-4.x 2.2-2.x 2.5.0-3.x
1.0-groovy-2.0 2.0.0 -2.2.x 4.7-4.x 2.2-2.x 2.5.0-4.x
1.0-groovy-2.3 2.3.0 -2.3.x 4.7-4.x 2.2-2.x 2.5.0-4.x
1.0-groovy-2.4 2.4.0 -2.x.x 4.7-4.x 2.2-2.x 2.5.0-4.x

Let's write a test

Start writing like test driven

Let's write a test as a trial. The location of the test is src / test / groovy / [package Name] /. The extension is .groovy. Make sure it is in the same package as the class under test. The test class name is [Class name to be tested] + Spec. This is because it represents the specification of the class under test even though it is called a unit test. It's my commitment, so I think anything is fine.

Make it like this. image.png

Now, let's implement a method that calculates the tax-included amount (8%) of the consumption tax.

TaxCalculaterSpec.groovy


import spock.lang.Specification

class TaxCalculaterSpec extends Specification {
        def sut = new TaxCalculater()

        def "calculate:Calculate the tax-included amount"() {
                expect:
                sut.calculate(100) == 108
        }
}                                                      

Suddenly I wrote a test. As I said earlier, this is a specification, so the test method name is something that represents the specification. At this point, the build will not pass before the test passes, so write the main code.

TaxCalculater.java


public class TaxCalculater {
    public int calculate(int value) {
        return 0;
    }
}

Well, when I try to run it ...

Condition not satisfied:

sut.calculate(100) == 108
|   |              |
|   0              false

Yes, the test failed brilliantly. That's right, it fails because it's a method that returns 0.

The point of interest is how this message appears. As a result of executing the method, 0 was returned, so the comparison with 108 became false and the test failed. You can see at a glance. This ease of understanding is one of the attractions of spock.

Now that we've confirmed that the test fails, let's write some code that will pass the test.

TaxCalculater.java


public class TaxCalculater {
    public int calculate(int value) {
        return 108;
    }
}

The test passed. I did it (I didn't do it)

Triangulation

With just one test, you don't know if that test is really valid. Maybe it's just returning a fixed value. Therefore, there are variations in values and situations. With jUnit, you can increase method calls in such cases It's quite annoying because duplicate code is generated, but spock makes it a little easier to read.

TaxCalculaterSpec.groovy


class TaxCalculaterSpec extends Specification {
        def sut = new TaxCalculater()

        def "Calculate the tax-included amount"() {
                expect:
                sut.calculate(value) == $result

                where:  
                value   |$result
                100     |108
                200     |216
        }
}

where! What a guy! Suddenly, an unknown writing style came out. This where is a notation that allows you to write a combination of values that is written like markdown. The above example shows that the "calculate tax-included amount" method is repeated for the pattern. Since there are two patterns, this test method will be executed twice.

If you run the test in this state, it will fail like this.

Condition not satisfied:

sut.calculate(value) == $result
|   |         |      |  |
|   108       200    |  216
|                    false

216 is the expected value, but 108 is returned, so that's right. That's the implementation. It can not be helped.

So, I will modify the implementation.

TaxCalculater.java


public class TaxCalculater {

    public int calculate(int value) {
        return (int)(value * 1.08);
    }
}

The test is now passed.

When specifications are added

Let's add a specification to the method for calculating the tax-included amount. Add a specification that "values after the decimal point included in the tax-included amount are rounded to the first decimal place".

TaxCalculaterSpec.groovy


class TaxCalculaterSpec extends Specification {
        def sut = new TaxCalculater()

        def "Calculate the tax-included amount"() {
                expect:
                sut.calculate(value) == $result

                where:
                value   |$result
                100     |108
                200     |216

                and: "Values after the decimal point included in the tax-included amount are rounded to the first decimal place"
                111     |120
        }
}

Added to the where pattern. 111 is 119.88 when multiplied by 1.08, so the expected value should be 120.

Condition not satisfied:

sut.calculate(value) == $result
|   |         |      |  |
|   119       111    |  120
|                    false

It's been truncated! !! (I knew) So let's put in a rounding process.

TaxCalculater.java


public class TaxCalculater {
    public int calculate(int value) {
        return (int)(Math.round(value * 1.08));
    }
}

In this way, once the specifications are finalized, we will add patterns to the test. It is very easy to see if you write the specifications such as boundary values in the test as described above. And writing a test will give you the courage to refactor.

TaxCalculater.java


public class TaxCalculater {
    private final static double taxRate = 1.08;

    public int calculate(int value) {
        return (int)(Math.round(value * taxRate));
    }
}

I put out the tax rate, but the test passes successfully, so there is no problem. Don't you like the fixed variable name being camel case? If so, let's fix it and run the test. It is safe if the test passes.

exception

One of the specifications is that an exception occurs under certain conditions. Let's write a test that raises an exception.

TaxCalculaterSpec.groovy


	def "Exception occurs when calculating tax-included amount from negative value"() {
		when:
		sut.calculate(-1000)
		
		then:
		thrown(ApplicationException)
	}

It's easy to understand! Let's run a test.

Expected exception of type 'org.omg.CORBA.portable.ApplicationException', but no exception was thrown
	at org.spockframework.lang.SpecInternals.checkExceptionThrown(SpecInternals.java:79)
	at org.spockframework.lang.SpecInternals.thrownImpl(SpecInternals.java:66)
	at spockSampleProject.TaxCalculaterSpec.Exception occurs when calculating tax-included amount from negative value(TaxCalculaterSpec.groovy:27)

The test failed that the ApplicationException was supposed to be thrown but wasn't.

TaxCalculater.java


public int calculate(int value) throws ApplicationException {
        if (value < 0) {
                throw new ApplicationException(null, null);
        }

        return (int) (Math.round(value * taxRate));
}

If you implement it to throw an exception if it is negative and rerun the test, everything will succeed. I did it (I did it)

Reporting (digression)

If you decide to write the specification for the method name of the test class in Japanese, the report will be much easier to read. If you look at this, you can see the specifications. Personally, it's much easier to read than javadoc, so I don't have to write javadoc. I'm thinking of writing a unit test properly. image.png

Finally

I tried test-driven programming using the spock framework, What do you think.

I would be happy if you could convey the goodness of being able to write the specifications clearly and easily. In addition to the contents introduced this time, it is quite powerful that you can write mocking tests without using a mock library such as mockit. Of course, you can also test your data in combination with DbUnit. I would like to write these articles soon.

Recommended Posts

Let's introduce spock framework and write unit tests like test driven
Introduce RSpec and write unit test code
Let's cure the unit test like breathing (Apex trigger edition)
Read and write like java.nio
Let's unit test with [rails] Rspec!