[spock] it's so silky in a single test

Posted by badt18 on Sun, 03 Nov 2019 21:07:10 +0100

Why does everyone hate to write single tests

Before about swagger As mentioned in the article, there are two things programmers hate most, one is that others don't write documents, the other is that they write documents themselves. The same holds here if you replace the documentation with unit tests.
Every developer understands the role of unit testing and that the higher the code coverage, the better. For code with high coverage, the lower the probability of BUG, the more stable it is to run online, and the fewer pots to connect, so it will not be afraid of the sudden concern of test colleagues.
Since there are so many advantages, why do you hate him? At least in my opinion, there are several reasons why I can't like it.
First, you need to write a lot of extra code. A single test code with high coverage is often more than the business code you want to test, or even several times of the business code. It's hard to accept. Think about the mood of developing 5 minutes and measuring 2 hours alone. And it's not that the single test will be OK after it's written. If the subsequent business changes, the single test code you write should also be maintained synchronously.
Second, even if you have the patience to write a single test, will you be given so much time to write a single test in the current environment of struggling for time? Write a single test time can achieve a requirement, how would you choose?
Third, writing a single test is usually a very boring thing, because he is relatively dead, the main purpose is to verify, in contrast, he is more like an individual, not really writing business code that kind of creative sense of achievement. Write it out, and it's lost to verify that you can't find a bug. Write it in vain, and then verify that you feel like you're slapping yourself in the face.

1. Why does everyone have to write a single test again

So the conclusion is not to write a single test? So the problem is coming again. Sooner or later, it will be paid back. If there is a problem on the line, who is the ultimate responsible person? It's not the products that we need, or the test students who don't find any problems. At most, they are jointly and severally liable. The one who should be responsible for this code is you. Especially for those developers who are engaged in financial, trading, e-commerce and other related businesses, the real money and silver are the ones who communicate with each line of code. Every time a star does things, micro-blog hangs up and has been passed on as a joke. After all, it is only entertainment related. If Alipay and WeChat are hung, then users will not have such a big tolerance. If there are serious problems in these businesses, they will go out of the door, and then the whole career will bear this stain, and the most important thing is to directly change from object-oriented development to prison oriented development. So unit tests protect not only the program, but also you who write the program.
At last, I come to a helpless conclusion that single test is a thing that people love and hate. It's something they don't want to do but have to do. Although we can't change how to write unit tests, we can change how to write unit tests.

2. SPOCK can help you improve the single test experience

Of course, this article does not teach you to improve code coverage in a side-by-side way. It's a magic framework spock to improve the efficiency of your unit tests. The source of spock's name, I guess, is due to the character of the same name in Star Trek (cover). So how does spock improve the efficiency of writing a single test? I think there are the following points:
First, it can use less code to implement unit tests, so that you can focus on the process of verifying results rather than writing single test code. So how can he write less code? He used a magic called groovy.
Groovy is a dynamic language based on jvm. It can be simply understood as python or js running on the jvm. Speaking of this, students who may not have been exposed to dynamic language will have a relatively rigid impression on them. They are too flexible, prone to problems, and have poor maintainability. Therefore, there is the "dynamic for a while, family xxx" barrier. First of all, these are his problems. Strictly speaking, they are caused by improper use. So it mainly depends on the users. For example, gradle, the official dependency management tool in Android, is based on groovy.
In addition, don't mistakenly think that I need to learn this framework and another language, which costs too much. In fact, don't worry. If you can groovy, it's better. If you can't, it doesn't matter. Because groovy is based on java, you can safely and boldly use java syntax. Some of the unique syntax of groovy to be used is very few, and will tell you later.
Second, it has better semantics and makes your single test code more readable.
The word semantic may not be well understood. Let's take two examples. The first one is a language with better semantics - HTML. His grammar is characterized by tags. Different types are put in different tags. For example, the head is the information of the head, the body is the information of the main content, and the table is the information of the table, which can be easily understood by people without programming experience. The second one is regular, a language with poor semantics. It can be said that there is basically no such thing as semantics, and the direct problem is that even if your own writing is regular, you don't know what was written a few days later. For example, the following regular, can you guess what he means? (you can reply with a message)

((?:(?:25[0-5]|2[0-4]\d|[01]?\d?\d)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d?\d))

3. Appreciate the magic of SPOCK

3.1 introduce dependency

        <!--If not spring boot,The following packages can be omitted-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!--Introduce spock Core package-->
        <dependency>
            <groupId>org.spockframework</groupId>
            <artifactId>spock-core</artifactId>
            <version>1.3-groovy-2.5</version>
            <scope>test</scope>
        </dependency>
        <!--Introduce spock And spring Integrated package-->
        <dependency>
            <groupId>org.spockframework</groupId>
            <artifactId>spock-spring</artifactId>
            <version>1.3-groovy-2.5</version>
            <scope>test</scope>
        </dependency>
        <!--Introduce groovy rely on-->
        <dependency>
            <groupId>org.codehaus.groovy</groupId>
            <artifactId>groovy-all</artifactId>
            <version>2.5.7</version>
            <scope>test</scope>
        </dependency>
Explain

The comments have indicated that the first package is required for the spring boot project. If you just want to use spock, you only need to use the bottom three packages. The first package, spock core, provides the core functions of spock, and the second package, spock spring, provides the integration with spring (it can also not be introduced without spring) Note the version number of these two packages - > 1.3-groovy-2.5. The first version number 1.3 actually represents the version of spock, and the second version number represents the version of the groovy environment that spock depends on. The last package is the groovy we depend on.

3.2 preparation for basic test

3.2.1 Calculator.java

/*
 * *
 *  * blog.coder4j.cn
 *  * Copyright (C) 2016-2019 All Rights Reserved.
 *
 */
package cn.coder4j.study.example.spock;

/**
 * @author buhao
 * @version Calculator.java, v 0.1 2019-10-30 10:34 buhao
 */
public class Calculator {

    /**
     * Add operation
     *
     * @param num1
     * @param num2
     * @return
     */
    public static int add(int num1, int num2) {
        return num1 + num2;
    }

    /**
     * Integer division operation
     *
     * @param num1
     * @param num2
     * @return
     */
    public static int divideInt(int num1, int num2) {
        return num1 / num2;
    }

    /**
     * Floating point operation
     * @param num1
     * @param num2
     * @return
     */
    public static double divideDouble(double num1,  double num2){
        return num1 / num2;
    }
}
Explain

This is a very simple calculator class. Only three methods are written, one is the operation of addition, one is the division operation of integer type, and one is the division operation of floating-point type.

3.3 start single test calculation.java

3.3.1 create a single test class CalculatorTest.groovy

class CalculatorTest extends  Specification {
    
}
Explain

It's important to note that we've said before that spock is based on groovy. So the suffix of the singleton class is not. java but * *. Groovy. Never create a normal java class. Otherwise, there is no problem in creation, but writing some groovy syntax will result in an error. If you use IDEA, you can create it in the following way. We used to choose the first option when creating java classes. Now we can choose the third Groovy Class * *.

In addition, Spock's test class needs to inherit the spock.lang.Specification class.

3.3.2 verification plus operation expect

    def "test add"(){
        expect:
        Calculator.add(1, 1) == 2
    }
Explain

def is the keyword of groovy, which can be used to define variables and method names. The following "test add" is the name of your unit test, which can also be in Chinese. Finally, the key word is expect.
Expect literally means what we expect to happen. In the case of other single test frameworks, a similar one is assert. For example, "assert. Assertequals (" "calculator. Add (" "1 + 1)", 2) "means that the result of adding 1 and 1 is 2. If the result is this, the use case passes, if not, the use case fails. This is the same as the code above.
The syntax meaning of expect is that in the block of expect, if all expressions are valid, the validation passes; otherwise, if any expression is not valid, the validation fails. A concept of block is introduced here. How to understand spock's block? We said above that spock has good semantics and better readability because of the function of this block. It can be likened to a tag in html. The range of html tags is between two tags, and spock is more concise. From the beginning of this tag to the beginning of the next tag or the end of the code, it is his range. As long as we see the "expect" tag, we can see that the scope of it is all the results we expect to get.

3.3.3 verification plus operation - given - and

The code here is relatively simple. I only use parameters once, so I write them directly. If I want to reuse them, I have to take these parameters as variables. At this time, you can use the given block of spock. Given's syntax is equivalent to an initialized block of code.

    def "test add with given"(){
        given:
        def num1 = 1
        def num2 = 1
        def result = 2

        expect:
        Calculator.add(num1, num2) == result
    }

Of course, you can write as follows, but it is not recommended seriously, because although it can achieve the same effect, it does not conform to the semantics of spock. Just like we usually introduce js and css in the head, but you can import them in the body or any tag. There is no problem with the syntax, but it destroys the semantics and is not easy to understand and maintain.

    // Instead
    def "test add with given"(){
        expect:
        def num1 = 1
        def num2 = 1
        def result = 2
        Calculator.add(num1, num2) == result
    }

If you want to make the semantics better, we can define parameters and results separately, and block can be used at this time. Its grammatical function can be understood as the latest tag above.

    def "test add with given and"(){
        given:
        def num1 = 1
        def num2 = 1

        and:
        def result = 2

        expect:
        Calculator.add(num1, num2) == result
    }

3.3.4 verification plus operation - expect - where

Looking at the above example, I may think that spock is just better in semantics, but I haven't written a few lines of code less. Don't worry. Let's take a look at one of spock's killers.

    def "test add with expect where"(){
        expect:
        Calculator.add(num1, num2) == result

        where:
        num1    |   num2    |   result
        1       |   1       |   2
        1       |   2       |   3
        1       |   3       |   4
    }

Where block can be understood as a place to prepare test data. It can be used in combination with expect. In the above code, three variables num1, num2 and result are defined in the expect block. The data can be defined in the where block. Where block uses a definition method similar to the table in markdown. The first row, or header, lists the variable names we want to transfer data to. Here, it should correspond to expect, not less but more. All other rows are data rows, which are separated by the 「 sign 」 just like the header. In this way, spock will run three use cases, namely 1 + 2 = 2, 1 + 2 = 3, 1 + 3 = 4. What about? Is it very convenient? If you want to expand the use case later, just add another row of data.  

3.3.5 verification plus operation - expect - where - @Unroll

The above use cases are all normal and can run through. If the IDEA runs through, it will be as follows:

Now let's see what happens if a use case fails. Change the last 4 of the above code to 5

    def "test add with expect where"(){
        expect:
        Calculator.add(num1, num2) == result

        where:
        num1    |   num2    |   result
        1       |   1       |   2
        1       |   2       |   3
        1       |   3       |   5
    }

Run again, and IDEA will show the following

What is marked on the left is the execution result of the use case. It can be seen that although there are three pieces of data, two of which are successful, they only show the overall success or not, so the display fails. But three pieces of data, how can I know which one failed?
The error log printed by spock is marked on the right. It can be clearly seen that when num1 is 1, num2 is 3, and result is 5, and the judgment relationship between them is = = the result is false This log of spock is printed with a fairly long history. If it is a comparison string, it will also calculate the matching degree between the abnormal string and the correct string. Interested students can test it by themselves.
Well, it's a bit troublesome to know which use case failed through the log. spock knows that, too. So he also provided a * * @ roll * * annotation. Let's add this note to the above code:

    @Unroll
    def "test add with expect where unroll"(){
        expect:
        Calculator.add(num1, num2) == result

        where:
        num1    |   num2    |   result
        1       |   1       |   2
        1       |   2       |   3
        1       |   3       |   5
    }

The operation results are as follows:
By adding * * @ Unroll * * annotation, spock automatically splits the above code into three independent single test tests, which are run separately and the results are clearer.
So can it be clearer? Of course, we find that after spock is split, the name of each use case is actually the name of the single test method you wrote, and then an array subscript is added, which is not very intuitive. We can use groovy's String Syntax to put variables into use case names. The code is as follows:

    @Unroll
    def "test add with expect where unroll by #num1 + #num2 = #result"(){
        expect:
        Calculator.add(num1, num2) == result

        where:
        num1    |   num2    |   result
        1       |   1       |   2
        1       |   2       |   3
        1       |   3       |   5
    }

As mentioned above, we added a sentence after the method name: "num1 +" num2 = "result". It's a bit similar to what we used in mybatis or some template engines. #The variables declared by No. splicing can be used. After execution, the results are as follows.

This is clearer.
Another point is that where uses the form of table by default:

        where:
        num1    |   num2    |   result
        1       |   1       |   2
        1       |   2       |   3
        1       |   3       |   5

It's intuitive, but there's a drawback to this form. The above 「| 」 sign is so neat. It's all from one space and one TAG. Although grammar does not require alignment, obsessive-compulsive disorder is the result. Fortunately, there is another form:

    @Unroll
    def "test add with expect where unroll arr by #num1 + #num2 = #result"(){
        expect:
        Calculator.add(num1, num2) == result

        where:
        num1 << [1, 1, 2]
        num2 << [1, 2, 3]
        result << [1, 3, 4]
    }

You can assign an array to a variable through the 「 < 」 (pay attention to the direction), which is the same as the above data table. There is no table intuitive, but it is simpler and does not need to consider the alignment problem. These two forms are based on personal preferences.

3.3.6 verify integer division - when - then

We all know that an integer divided by 0 will throw an 「 / by zero 」 exception, so what if we assert this exception. It's not easy to use the above expect. We can use another similar block * * when... Then * *.

    @Unroll
    def "test int divide zero exception"(){
        when:
        Calculator.divideInt(1, 0)

        then:
        def ex = thrown(ArithmeticException)
        ex.message == "/ by zero"
    }

When... Then usually occurs in pairs, which represents the expectation in the then block when the operation in the when block is performed. For example, the above code shows that when the operation of Calculator.divideInt(1, 0) is executed, an ArithmeticException exception will be thrown, and the exception information is / by zero.

3.4 preparing Spring test classes

Now that we have learned the basic usage of spock, let's learn about integrating with spring. First, create several demo classes for testing

3.4.1 User.java

/*
 * *
 *  * blog.coder4j.cn
 *  * Copyright (C) 2016-2019 All Rights Reserved.
 *
 */
package cn.coder4j.study.example.spock.model;

import java.util.Objects;

/**
 * @author buhao
 * @version User.java, v 0.1 2019-10-30 16:23 buhao
 */
public class User {
    private String name;
    private Integer age;
    private String passwd;

    public User(String name, Integer age, String passwd) {
        this.name = name;
        this.age = age;
        this.passwd = passwd;
    }

    /**
     * Getter method for property <tt>passwd</tt>.
     *
     * @return property value of passwd
     */
    public String getPasswd() {
        return passwd;
    }

    /**
     * Setter method for property <tt>passwd</tt>.
     *
     * @param passwd value to be assigned to property passwd
     */
    public void setPasswd(String passwd) {
        this.passwd = passwd;
    }

    /**
     * Getter method for property <tt>name</tt>.
     *
     * @return property value of name
     */
    public String getName() {
        return name;
    }

    /**
     * Setter method for property <tt>name</tt>.
     *
     * @param name value to be assigned to property name
     */
    public void setName(String name) {
        this.name = name;
    }

    /**
     * Getter method for property <tt>age</tt>.
     *
     * @return property value of age
     */
    public Integer getAge() {
        return age;
    }

    /**
     * Setter method for property <tt>age</tt>.
     *
     * @param age value to be assigned to property age
     */
    public void setAge(Integer age) {
        this.age = age;
    }

    public User() {
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        return Objects.equals(name, user.name) &&
                Objects.equals(age, user.age) &&
                Objects.equals(passwd, user.passwd);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age, passwd);
    }
}

3.4.2 UserDao.java

/*
 * *
 *  * blog.coder4j.cn
 *  * Copyright (C) 2016-2019 All Rights Reserved.
 *
 */
package cn.coder4j.study.example.spock.dao;

import cn.coder4j.study.example.spock.model.User;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;

/**
 * @author buhao
 * @version UserDao.java, v 0.1 2019-10-30 16:24 buhao
 */
@Component
public class UserDao {

    /**
     * Simulation database
     */
    private static Map<String, User> userMap = new HashMap<>();
    static {
        userMap.put("k",new User("k", 1, "123"));
        userMap.put("i",new User("i", 2, "456"));
        userMap.put("w",new User("w", 3, "789"));
    }

    /**
     * Query users by user name
     * @param name
     * @return
     */
    public User findByName(String name){
        return userMap.get(name);
    }
}

3.4.3 UserService.java

/*
 * *
 *  * blog.coder4j.cn
 *  * Copyright (C) 2016-2019 All Rights Reserved.
 *
 */
package cn.coder4j.study.example.spock.service;

import cn.coder4j.study.example.spock.dao.UserDao;
import cn.coder4j.study.example.spock.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * @author buhao
 * @version UserService.java, v 0.1 2019-10-30 16:29 buhao
 */
@Service
public class UserService {

    @Autowired
    private UserDao userDao;

    public User findByName(String name){
        return userDao.findByName(name);
    }

    public void loginAfter(){
        System.out.println("Login successfully");
    }

    public void login(String name, String passwd){
        User user = findByName(name);
        if (user == null){
            throw new RuntimeException(name + "Non-existent");
        }
        if (!user.getPasswd().equals(passwd)){
            throw new RuntimeException(name + "Password input error");
        }
        loginAfter();
    }
}

3.4.3 Application.java

/*
 * *
 *  * blog.coder4j.cn
 *  * Copyright (C) 2016-2019 All Rights Reserved.
 *
 */

package cn.coder4j.study.example.spock;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}

3.5 integration test with spring

/*
 * *
 *  * blog.coder4j.cn
 *  * Copyright (C) 2016-2019 All Rights Reserved.
 *
 */

package cn.coder4j.study.example.spock.service

import cn.coder4j.study.example.spock.model.User
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import spock.lang.Specification
import spock.lang.Unroll

@SpringBootTest
class UserServiceFunctionTest extends Specification {

    @Autowired
    UserService userService

    @Unroll
    def "test findByName with input #name return #result"() {
        expect:
        userService.findByName(name) == result

        where:
        name << ["k", "i", "kk"]
        result << [new User("k", 1, "123"), new User("i", 2, "456"), null]

    }

    @Unroll
    def "test login with input #name and #passwd throw #errMsg"() {
        when:
        userService.login(name, passwd)

        then:
        def e = thrown(Exception)
        e.message == errMsg

        where:
        name    |   passwd  |   errMsg
        "kd"     |   "1"     |   "${name}Non-existent"
        "k"     |   "1"     |   "${name}Password input error"

    }
}

The integration of spock and spring is particularly simple, as long as you add the spock spring and spring boot starter test mentioned at the beginning. Add @ SpringBootTest annotation to the class of test code. Just inject the classes you want to use directly, but note that only functional test or integration test can be used here, because spring container will be started when running use cases, and external dependencies must also exist. It's time-consuming, and sometimes it's impossible to run with external dependencies, so we usually use mock to complete unit tests.

3.6 and spring mock test

/*
 * *
 *  * blog.coder4j.cn
 *  * Copyright (C) 2016-2019 All Rights Reserved.
 *
 */

package cn.coder4j.study.example.spock.service

import cn.coder4j.study.example.spock.dao.UserDao
import cn.coder4j.study.example.spock.model.User
import spock.lang.Specification
import spock.lang.Unroll

class UserServiceUnitTest extends Specification  {

    UserService userService = new UserService()
    UserDao userDao = Mock(UserDao)

    def setup(){
        userService.userDao = userDao
    }

    def "test login with success"(){

        when:
        userService.login("k", "p")

        then:
        1 * userDao.findByName("k") >> new User("k", 12,"p")
    }

    def "test login with error"(){
        given:
        def name = "k"
        def passwd = "p"

        when:
        userService.login(name, passwd)

        then:
        1 * userDao.findByName(name) >> null

        then:
        def e = thrown(RuntimeException)
        e.message == "${name}Non-existent"

    }

    @Unroll
    def "test login with "(){
        when:
        userService.login(name, passwd)

        then:
        userDao.findByName("k") >> null
        userDao.findByName("k1") >> new User("k1", 12, "p")

        then:
        def e = thrown(RuntimeException)
        e.message == errMsg

        where:
        name        |   passwd  |   errMsg
        "k"         |   "k"     |   "${name}Non-existent"
        "k1"        |   "p1"     |   "${name}Password input error"

    }
}

It's also very easy for spock to use mock. You can use mock (class) directly. The above code ﹣ UserDao userDao = Mock(UserDao). _There are several points to explain in the above example. Take this method as an example:

    def "test login with error"(){
        given:
        def name = "k"
        def passwd = "p"

        when:
        userService.login(name, passwd)

        then:
        1 * userDao.findByName(name) >> null

        then:
        def e = thrown(RuntimeException)
        e.message == "${name}Non-existent"

    }

given, when, then needless to say, you are already familiar with them, but what's the ghost of 1 * userdao.findbyname (name) > > null in the first then?
First of all, we can know that there can be multiple then blocks in a use case, and multiple expectations can be placed in multiple then.
Secondly, 1 * xx indicates that the expected xx operation has been performed once. 1 * userDao.findByName(name) * * indicates that when executing userService.login(name, passwd), I expect to execute the userDao.findByName(name) method once. If you don't want to execute this method, it's ﹤ 0 * xx, which is very useful in the verification of conditional code. Then what does > > null mean? On behalf of me, I asked him to return null after executing the userDao.findByName(name) method. Because userDao is a mock object, it is a fake object. In order to make the follow-up process go according to our ideas, I can use the 「 > 」 to let spock simulation return the specified data.
Third, note that the second then code block uses ${name} to reference variables, which is different from the header's "{name * *".

3.7 other contents

3.7.1 public methods

Method name Effect
setup() Each method is called before execution.
cleanup() After each method is executed, it is called.
setupSpec() Each method class is called once before loading.
cleanupSpec() Every method class is called once after execution

These methods are usually used for some initialization operations before the start of the test and cleaning operations after the completion of the test, as follows:

    def setup() {
        println "Initialize before method start"
    }

    def cleanup() {
        println "Clean up after method execution"
    }

    def setupSpec() {
        println "Class initialization before loading"
    }

    def cleanupSpec() {
        println "So the method finishes cleaning"
    }

3.7.2 @Timeout

For some methods, you need to specify their time. If the running time exceeds the specified time, you can use the timeout annotation

    @Timeout(value = 900, unit = TimeUnit.MILLISECONDS)
    def "test timeout"(){
        expect:
        Thread.sleep(1000)
        1 == 1
    }

Annotation has two values, one is the value we set, and unit is the unit of the value.

3.7.3 with

    def "test findByName by verity"() {
        given:
        def userDao = Mock(UserDao)

        when:
        userDao.findByName("kk") >> new User("kk", 12, "33")

        then:
        def user = userDao.findByName("kk")
        with(user) {
            name == "kk"
            age == 12
            passwd == "33"
        }

    }

With is a syntax sugar. Without it, we can only judge the value of an object, user.getXxx() == xx. If there are too many attributes, it's troublesome. After wrapping with, just write the attribute name in curly brackets, as shown in the code above.

4. other

4.1 complete code

Due to the limited space, all codes cannot be pasted. The complete code has been uploaded github.

4.2 reference documents

This article, after looking at the following bloggers' wonderful blog posts, plus its own learning summary and processing, if there is something you don't understand when reading this article, you can see the link below.

  1. Spock in Java slowly fell in love with write unit testing
  2. Using Groovy+Spock to easily write a more concise single test
  3. Introduction and application of Spock test framework
  4. Spock test based on BDD
  5. Official documents of Spock
  6. Spock test framework
  7. spock-testing-exceptions-with-data-tables

Topics: Java Spring calculator less