Spock framework Mock object, method and experience summary

Posted by thallium6 on Fri, 11 Feb 2022 14:30:15 +0100

Recently, it has fallen into the vast ocean of unit testing. Tens of thousands of lines of code suddenly require unit testing coverage, which is really terrible. After the most arduous struggle and learning, I finally crossed the technical barrier. I'd like to share my recent experience of stepping on pits and some typical use cases.

The following is a common project I have used, and some information is hidden. You can refer to it when you practice in your own project. Try not to copy the code directly. There are many compatibility holes in my own use, especially the automatic import function of IDE.

Technical scheme

This technical proposal is based on Spock unit testing framework promoted by the company. Spock is a unit testing framework based on Groovy language, and its foundation is Junit of Java. At present, the latest version has reached 2.0, but there are high requirements for Groovy and corresponding Java versions, so Groovy version uses 1. +, The Mock and Spy provided by Spock are good enough. The simulation of object behavior meets most scenarios, but there are limitations when it comes to static method simulation. Therefore, Mockito and PowerMock are introduced to realize the test simulation scenario of designing static methods.

The following are the dependent versions:

        <dependency>
            <groupId>org.spockframework</groupId>
            <artifactId>spock-core</artifactId>
            <version>1.2-groovy-2.5</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.spockframework</groupId>
            <artifactId>spock-spring</artifactId>
            <version>1.2-groovy-2.5</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.powermock</groupId>
            <artifactId>powermock-api-mockito2</artifactId>
            <version>1.7.4</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.powermock</groupId>
            <artifactId>powermock-module-junit4</artifactId>
            <version>1.7.4</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-core</artifactId>
            <version>2.8.9</version>
            <scope>test</scope>
        </dependency>

Groovy all also needs a self annotation because it supports groovy all

        <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.7</version>
        </dependency>

In addition, several dependencies used in special scenarios are added in the provided configuration file for reference:

        <dependency> <!-- enables mocking of classes (in addition to interfaces) -->
            <groupId>net.bytebuddy</groupId>
            <artifactId>byte-buddy</artifactId>
            <version>1.9.9</version>
            <scope>test</scope>
        </dependency>
        <dependency> <!-- enables mocking of classes without default constructor (together with CGLIB) -->
            <groupId>org.objenesis</groupId>
            <artifactId>objenesis</artifactId>
            <version>3.0.1</version>
            <scope>test</scope>
        </dependency>
        <dependency> <!-- only required if Hamcrest matchers are used -->
            <groupId>org.hamcrest</groupId>
            <artifactId>hamcrest-core</artifactId>
            <version>2.1</version>
            <scope>test</scope>
        </dependency>

Non static resources

Due to the repeated method names of multiple single test frameworks, I also posted the import content. If the same code cannot run, you can check whether to import the correct methods and classes. import static is not recommended here, because there may be mixed use and problems that are difficult to check.

Since there is no logic using Spy release in the current test, Mock mode is used, and the method of Mock object needs to be simulated. This is divided into two categories: Spock and PowerMock (combined with Mockito). The reason is that in the scenario of mixed static and non static resources, the @ RunWith running rules of PowerMock are specified, which is incompatible with Spock writing method and requires the function of Mock object of PowerMock framework.

Mock tested object

@Autowired construction method

Take a controller as an example. The source code is as follows:

@Api(tags = "SLA Rule management module")
@Slf4j
@RestController
@RequestMapping("/hickwall/v1/static/sla")
public class FunController {

    HttpServletRequest request;

    ISlaService service;

    @Autowired
    public FunController(HttpServletRequest request, ISlaService service) {
        this.request = request;
        this.service = service;
    }

}

Spock single test code is as follows:

import com.funtester.service.ISlaService
import com.funtester.vo.sla.SlaBean
import spock.lang.Shared
import spock.lang.Specification

import javax.servlet.http.HttpServletRequest

class FunControllerTest extends Specification {

    def service = Mock(ISlaService)

    @Shared
    def request = Mock(HttpServletRequest)
    
    def FunController = new FunController(request, service)

}

@Autowired property object, no construction method

The source code is as follows:

public class ApiImpl implements IApi {

    @Autowired
    private ApiRMapper mapper;
}

The codes of Spock single test part are as follows:

import com.funtester.mapper.ApiRMapper
import com.funtester.vo.ApiR
import spock.lang.Shared
import spock.lang.Specification


    ApiRMapper mapper = Mock(ApiRMapper)

    def drive = new ApiImpl(mapper:mapper)

PowerMock usage

Scenarios are also divided into two types: whether there is a construction method or not. Except that the Mock method is different, the others are the same. They are not listed here.

PS: if there is an attribute in the object attribute that is not annotated by @ Autowired, the lombok annotation of @ AllArgsConstructor cannot be used, and an error will be reported when the service is started.

The source code is as follows:

@Component
@Slf4j
public class TaskScheduled {

    @Autowired
    IService service;
    
    @Value("${hickwall.statistic.cid}")
    public  String cid;

}

Shared objects and initialization

The functions provided by Spock are used uniformly. The annotation @ Shared cannot be assigned in the Spock method without adding it, but it can be used as an ordinary object.

Spock framework Demo:

    @Shared
    def slaBean = new SlaBean()

    def setupSpec() {
        request.getHeader("operator") >> "FunTester"
        slaBean.name = "test"
        slaBean.quota = 1
        slaBean.upstream = "PRO"
        slaBean.threshold = 0.1
        slaBean.creator = "FunTester"
        slaBean.id = 100
    }

Define object behavior

Spock defines the behavior of Mock objects

The basic Spock syntax structure when then expct is as follows:

    def "AddSla"() {
        when:
        def sla = FunController.addSla(slaBean)
        then:
        service.addSla(_) >> {f ->
            assert "FunTester" in f.creator
            1
        }

        expect:

        sla.code == 0
        sla.data == 1

    }

You can also add given at the beginning. when and then are usually used together.

The above Demo asserts and processes the parameters during the Mock method, which is also a feature of Spock framework. Others are Groovy syntax features.

Other syntax for defining Mock behavior is as follows:

        service.getAllGroup(_,_) >> null//Return null
        service.getAllGroup(_,_) >> {throw new Exception()} //Throw exception
        service.getAllGroup(_,_) >> []//Returns an empty list. Groovy implements ArrayList by default
        service.getAllGroup(_,_) >> [slaBean,slaBean]//Return to normal list
        service.getAllGroup(_,_) >> [slaBean,slaBean]//Return to normal list
        service.getAllGroup(_,10) >> [slaBean,slaBean]//Timing a parameter
        service.getAllGroup(any(),10) >> [slaBean,slaBean]//any() is equivalent to_
        service.getAllGroup(any(),10) >> service.getAllGroup(1,10)//Call other methods to return

Mockito simulates object behavior

The syntax for using Mockito with PowerMock is slightly more complex. First, we need to define the object behavior (usually in the com.funterbase.task.taskscheduledtest#setupspec method), and then use it in use cases.

Timed object behavior:

        Mockito.when(newutil.filter(Mockito.any())).thenReturn(true)

After defining the behavior, it can be used normally in the Spock use case, including in the object method created through the Mock object. If you call the method that has defined the behavior, you will also follow the custom logic.

Other commonly defined behaviors:

        Mockito.when(newutil.filter(Mockito.any())).thenReturn(null)
        Mockito.when(newutil.filter(Mockito.any())).thenThrow(Exception.class)//Throw exception
        PowerMockito.doNothing().when(newutil).filter(Mockito.any(ArrayList.class))//dothing, do nothing

In the third example, we assume that the filter method is a void method without return.

Generally, we need to build a return object. If the object needs too many attributes to be assigned, we can use the method of initializing assignment. The following is a Demo of the return value of Mock's method of returning list:

Mockito.when(newser.selectAllService()).thenReturn([new NewInterface() {

            {
                setUrl("/abc")
                setNname("test")
                setMethod("GET")
            }
        }, new NewInterface() {

            {
                setUrl("/abcd")
                setNname("test")
                setMethod("POST")
            }
        }, new NewInterface() {

            {
                setUrl("/abce")
                setNname("test")
                setMethod("GET")
            }
        }])