0. Why does everyone hate writing single tests

As mentioned in the previous swagger article, there are two things programmers hate most, one is when others don’t document, and the other is when they document themselves. The same is true here if you replace documentation with unit tests. Every developer understands the role of unit tests, and the more code coverage the better. Code with high coverage is relatively less buggy, more stable online, less likely to catch POTS, and less likely to fear sudden interest from test colleagues. With all this upside, why hate him? In my opinion, at least, there are a few things I don’t like about single testing. First, you need to write lots and lots of extra code. A single test code with high coverage is often more than the actual business code that you are testing, even several times as much as the business code. That’s hard to accept, considering how it feels to develop for 5 minutes and test for 2 hours. And it’s not just a matter of writing a single test. If the business changes later, the single test code you wrote also needs to 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 speed squeeze environment? Write a single test time can fulfill a requirement, how would you choose? Third, writing tests is usually boring, because it is more of a manual job, with less of a creative sense of accomplishment than actually writing business code. Write out, verification of the bug is very lost, white write, verification of the bug and feel that he is playing in his face.

1. Why does everyone have to write a single test

So the conclusion is no single test, right? Then the problem came again, out of mix sooner or later is to return, the line gave a problem, who is ultimately responsible? It is not the product that raised the demand, it is not the test student that did not find the problem, they are joint liability at most. The person most responsible is you who wrote this code. Especially for those in finance, trading, e-commerce and other businesses closely related to the developers, with every line of code traffic is real money. Every time a star does something, the micro-blog will be hung up, which has been rumored as a joke. After all, it is only entertainment related. If it is Alipay or wechat, the users will not be so tolerant. If something goes badly wrong with these businesses, they can either be out of the door and have that stigma for their entire careers, or they can go straight from object-oriented development to prison-oriented development. So unit testing protects not only the program, but also you who wrote the program. In the end, I came to the unyielding conclusion that single testing is something people both love and hate, something they don’t want to do but have to do. We can’t change how we write unit tests, but we can change how we write unit tests.

2. SPOCK improves your playtest experience

Of course, this article is not a roundabout way to improve code coverage. Instead, use spock, an amazing framework, to increase your efficiency in writing unit tests. The name spock comes from the star Trek character of the same name (cover image). So how does Spock make writing single tests more efficient? First, it allows you to implement unit testing with less code, allowing you to focus more on verifying the results rather than writing a single test. So how does he write less code? It turns out he uses a magic trick called Groovy. Groovy is actually a dynamic language based on the JVM. It can simply be thought of as Python or JS running on the JVM. At this point, students who have not been in contact with dynamic languages may have a stereotypical impression of them. They are too flexible, prone to problems, and have poor maintainability, so there is the phrase “dynamic for a while, the whole family XXX”. First of all, these are his problems, strictly speaking, caused by inappropriate use. So it depends on the user. For example, Gradle, the official dependency management tool for Android, is based on Groovy. In addition, do not mistake me to learn this framework, but also to learn a language, the cost is too big. Don’t worry. If you know Groovy, it’s great. If you don’t, it’s ok. Since Groovy is Java-based, it’s safe to be bold with Java syntax. Some groovy-specific syntax is rare and will be covered later. Second, it has better semantics, making your single-test code more readable. Semantic is a tricky word to understand. To take two examples, the first is HTML, a semantically good language. Its syntactic feature is tags, and different types are placed in different tags. For example, head is the information about the head, body is the information about the body content, and table is the information about the table. It is also easy to understand for people who have no programming experience. The second is a less semantically oriented language, re. He can say that there is basically no such thing as semantics, and the immediate problem with this is that even if you write your own regulars, days later you won’t know what you were writing at the time. For example, can you guess what this regular means? (You can leave a message to reply)

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

3. Taste SPOCK’s magic

3.1 Importing Dependencies

<! If spring Boot is not enabled, <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <! <dependency> <groupId>org. spockFramework </groupId> <artifactId> Spock -core</artifactId> < version > 1.3 - groovy 2.5 < / version > < scope > test < / scope > < / dependency > <! <dependency> <groupId>org. spockFramework </groupId> <artifactId> Spock -spring</artifactId> < version > 1.3 - groovy 2.5 < / version > < scope > test < / scope > < / dependency > <! Groovy </groupId> <artifactId>groovy-all</artifactId> < version > 2.5.7 < / version > < scope > test < / scope > < / dependency >Copy the code

instructions

The comments indicate that the first package is what spring Boot projects need to use, but if you just want to use Spock, just the bottom three. The first package, Ock – Core, provides spock’s core functionality, and the second package, Ock – Spring, provides integration with Spring (or without spring). Note the version numbers for both packages -> 1.3-groovy-2.5. The first version number, 1.3, actually represents the version of Spock, and the second version represents the version of the Groovy environment on which Spock depends. The last package is groovy, which we’ll rely on.

3.2 Preparing basic test classes

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 Calculator public class Calculator {/** * @param num1 * @param num2 * @return */ public static int add(int num1, int num2) { return num1 + num2; } @param num1 * @param num2 * @return */ public static int divideInt(int divide1, int divide2) int num2) { return num1 / num2; } @param num1 * @param num2 * @return */ public static double divideDouble(double num1, double num1) double num2){ return num1 / num2; }}Copy the code

instructions

This is a very simple calculator class. Only three methods are written: an addition operation, an integer division operation, and a floating-point division operation.

3.3 Start single testCalculator.java

3.3.1 Creating single-measure class calculatortest.groovy

class CalculatorTest extends  Specification {
    
}Copy the code

instructions

It’s important to note here that we’ve already said that Spock is based on Groovy. So the single test class suffix is not.java .groovy. Never create a normal Java class. Otherwise the creation is fine, but writing some Groovy syntax will give you an error. If you are using IDEA, you can create a Java class by selecting the first option. Now we choose the third optionGroovy ClassThat’s it.Another is that Spock’s test classes need to be inheritedspock.lang.Specification Class.

3.3.2 Verification plus operation – expect

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

instructions

Def is a Groovy keyword that can be used to define variable and method names. “Test add” is the name of your unit test, or you can use Chinese. The final emphasis is on the keyword expect. Expect literally means expect, what we expect to happen. A similar thing to assert is when using other monolithic frameworks. For example assert.assertequals (calculator.add (1 + 1), 2) means we Assert that the addition of 1 to 1 adds up to 2. If so, the use case passes; if not, the use case fails. This is functionally consistent with our code above. The syntactic meaning of Expect is that in an Expect block, verification succeeds if all expressions are true, and fails if any of them are not. This introduces the concept of a block. What about spock blocks? We said that Spock has good semantics and better readability because of this block. This is analogous to tags in HTML. Whereas an HTML tag ranges between two tags, Spock is more concise, starting with one tag and ending with the next. We only have to look at the expect label to see that its range is what we expect to get.

3.3.3 Verification plus Operation-give-and

The code here is relatively simple, and I only use the parameter once, so I just write it dead. If I want to reuse it, I have to extract these parameters as variables. You can use spock’s Given block at this point. The syntactic meaning of given is equivalent to an initialized code block.

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

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

Of course, you could write it like this, but it’s not recommended because it does the same thing, but it doesn’t match the semantics of Spock. Just like we normally introduce JS and CSS in head, you can do it in body or any tag. The syntax is fine but it breaks the semantics and is hard to understand and maintain.

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

If you want to make the semantics even better, we can define parameters and results separately, using the and block. Its syntactic function can be understood as the closest tag above it.

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

        and:
        def result = 2

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

3.3.4 Verification plus Operation – expect – where

Looking at the examples above, you might think that Spock is just better at semantics, but it doesn’t save you a few lines of code. Don’t worry, let’s take a look at Spock’s killer where.

    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
    }Copy the code

The WHERE block can be understood as a place to prepare test data, and it can be used in combination with Expect. Num1, num2 and result are three variables defined in the expect block. This data can be defined in the WHERE block. The WHERE block uses a definition method much like that used for tables in Markdown. The first line, or table header, lists the names of the variables we want to pass, which correspond to Expect, not less but more. Other lines are rows of data, like the header are separated by “|” character. By doing this, Spock runs three use cases: 1 + 2 = 2, 1 + 2 = 3, and 1 + 3 = 4. How’s that? Isn’t it convenient to extend the use case later and just add one more line of data?

3.3.5 Verification plus Operations – expect – where – @unroll

The above use cases are normal and can be run. If IDEA is run, it will be as follows:So now let’s see what happens if the useful example doesn’t pass, change the last 4 in the code above to a 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
    }Copy the code

Run again, IDEA will appear as followsThe use case execution results are marked on the left. It can be seen that although there are three pieces of data, two of which are successful, only the overall success or failure is displayed, so it is not passed. But three numbers, how do I know which one failed? The error log printed by Spock is marked on the right. If num1 = 1, num2 = 3, result = 5, and result == false If you are comparing strings, it will calculate the match between the exception string and the correct string. If you are interested, you can test it yourself. Well, it’s possible to know which use case failed from the log, but it’s still a bit of a hassle. And Spock knows it. So he offered one at the same time @Unroll Annotation. Let’s add this comment to the code above:

    @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
    }Copy the code

The running results are as follows:By adding @UnrollNote that Spock automatically split the above code into three separate single-test tests and ran them separately, making the results clearer. So could it be any clearer? Sure, we found that after Spock split, the name of each use case is actually the name of the single test method you wrote, followed by an array subscript, which is not very intuitive. We can use Groovy’s string syntax to put variables into use case names 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
    }Copy the code

As above, we add a sentence after the method name#num1 + #num2 = #result. This is somewhat similar to the approach we use in Mybatis or some template engines. # concatenates the declared variables, and the result is as follows.This is a little bit clearer. Another point is that where defaults to the form of a table:

        where:
        num1    |   num2    |   result
        1       |   1       |   2
        1       |   2       |   3
        1       |   3       |   5Copy the code

It’s intuitive, but there’s a downside to this form. The above “|” right so neatly. It’s all a blank and a TAG. Although grammar does not require alignment, it does kill ocD. Fortunately, however, there can be 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]
    }Copy the code

You can assign an array to a variable using the “<<” symbol (note the direction), which is equivalent to the table above. It’s not as intuitive as the table, but it’s simpler and doesn’t need to worry about alignment.

3.3.6 Verifying the integer division operation-when – then

We all know that dividing an integer by zero throws a “/ by zero” exception, but how about asserting that exception? Less easy to use with 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"
    }Copy the code

when … Then usually comes in pairs and represents the expectation in the THEN block when an operation in the WHEN block is performed. For example, the code above shows that ArithmeticException must be thrown when calculator. divideInt(1, 0) is executed with/by zero.

3.4 Preparing Spring test classes

Now that you’ve learned the basics of Spock, let’s learn about integrating with Spring, starting with creating a few demo classes for testing

3.4.1 track 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);
    }
}Copy the code

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 @component Public class UserDao {/** * 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")); } @param name * @return */ public User findByName(String name){return usermap.get (name); }}Copy the code

Rule 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 @service public class UserService {@autowired private UserDao UserDao; public User findByName(String name){ return userDao.findByName(name); } public void loginAfter(){system.out.println (" login succeeded "); } public void login(String name, String passwd){ User user = findByName(name); If (user == null){throw new RuntimeException(name + "not found "); } if (! User.getpasswd ().equals(passwd)){throw new RuntimeException(name + "password entered incorrectly "); } loginAfter(); }}Copy the code

Rule 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);
    }

}
Copy the code

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: The name | passwd | errMsg "kd" | "1" | "${name} does not exist" "k" | "1" | "${name} password input error"}}Copy the code

Spock and Spring integration is particularly easy, as long as you add the spock- Spring and spring-boot-starter-test mentioned at the beginning. Add the @Springboottest annotation to the class of the test code. The desired classes can be injected directly, but note that this is only a functional or integration test, since the Spring container is started when the use case is run, and external dependencies must also be present. It’s time-consuming, and sometimes it doesn’t work with external dependencies, so we mock unit tests.

3.6 With Spring Mock testing

/* * * * * 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: } @unroll def "login with "(){when: login when: login when: login when: login 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: The name | passwd | errMsg "k" | "k" | "${name} does not exist" "k1" | "p1" | "${name} password input error"}}Copy the code

Spock mocks are also easy to use. You can use mocks (classes) directly. UserDao UserDao = Mock(UserDao) There are a few points to illustrate 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} does not exist "}Copy the code

1 * userdao.findByName (name) >> null = 1 * userdao.findByName (name) >> null First, we can know that a use case can have multiple THEN blocks, and that multiple expectations can be placed in multiple THEN blocks. Second, 1 * xx indicates that the xx operation is expected to be performed once. 1 * userdao.findByName (name) indicates that I expect the userdao.findByName (name) method to be executed once when userService.login(name, passwd) is executed. This method is 0 * xx if it is not expected to execute, which is useful in conditional code validation, and >> null? He represents that when the userdao.findByName (name) method is executed, I let it return null. Since userDao is a mock object we mock out, it is a fake object. In order for the process to follow our wishes, I can “>>” spock simulation to return the specified data. Third, notice that the second THEN block uses ${name} to refer to the variable, which is different from the #name of the title.

3.7 Other Contents

3.7.1 Public methods

The method name role
setup() Called before each method executes
cleanup() Called after each method is executed
setupSpec() Called once before each method class is loaded
cleanupSpec() Each method class completes the call once

These methods are usually used for some initialization operations before the test starts, and cleaning operations after the test is completed, as follows:

Def setup() {println () {setspec () {setspec () {setspec () {setspec () {setspec () {setspec () {setspec () {setspec () {setspec () {setspec () {setspec () CleanupSpec () {println "So the method is done cleaning up"}Copy the code

3.7.2 @ the Timeout

For some methods, you need to specify their time and use the timeout annotation if they fail to run for longer than the specified time

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

The 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"
        }

    }Copy the code

With is a syntactic sugar, without which we can only determine the value of the object, user.getxxx () == xx. If there are too many attributes, this can be troublesome. After wrapping with with, simply write the attribute name in curly braces, as shown in the code above.

4. Other

4.1 Complete Code

Due to limited space, I cannot post all the codes. The complete code has been uploaded to Github.

4.2 Reference Documents

This article after looking forward to the following wonderful blog of the blogger, plus their own learning summary processing, if this article in the reading of the time do not understand the place can see the link below.

  1. Spock in Java grew to love writing unit tests
  2. Easier to write simpler tests using Groovy+Spock
  3. Spock testing framework introduction and detailed usage
  4. Spock is based on BDD testing
  5. Spock official document
  6. Spock tests the framework
  7. spock-testing-exceptions-with-data-tables