Course – LS – All
announcement - icon

Get started with Spring Boot and with core Spring, through the Learn Spring course:

>> CHECK OUT THE COURSE

1. Introduction

When we test Spring applications using Spock, we sometimes want to change the behavior of a Spring-managed component. In this tutorial, we’ll learn how to inject our own Stub, Mock, or Spy in place of a Spring auto-wired dependency. We’ll use a Spock Stub for most examples, but the same techniques apply when we use a Mock or Spy.

2. Setup

Let’s begin by adding our dependencies and creating a class with a dependency we can replace.

2.1. Dependencies

First, let’s add our Maven compile dependency for Spring Boot 3:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
    <version>3.3.0</version>
</dependency>

Now, let’s add our Maven test dependencies for spring-boot-starter-test and spock-spring. Since we’re using Spring Boot 3 / Spring 6, we need Spock v2.4-M1 or later to get Spock’s compatible Spring annotations, so let’s use 2.4-M4-groovy-4.0:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <version>3.3.0</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.spockframework</groupId>
    <artifactId>spock-spring</artifactId>
    <version>2.4-M4-groovy-4.0</version>
    <scope>test</scope>
</dependency>

2.2. Our Subject

With our dependencies in place, let’s create an AccountService class with Spring-managed DataProvider dependency that we can use for our tests.

First, let’s create our AccountService:

@Service
public class AccountService {
    private final DataProvider provider;

    public AccountService(DataProvider provider) {
        this.provider = provider;
    }

    public String getData(String param) {
        return "Fetched: " + provider.fetchData(param);
    }
}

Now, let’s create a DataProvider that we’ll substitute later:

@Component
public class DataProvider {
    public String fetchData(final String input) {
        return "data for " + input;
    }
}

3. Basic Test Class

Now, let’s create a basic test class that validates our AccountService using its usual components.

We’ll use Spring’s @ContextConfiguration to bring our AccountService and DataProvider into scope and autowire in our two classes:

@ContextConfiguration(classes = [AccountService, DataProvider])
class AccountServiceTest extends Specification {
    @Autowired
    DataProvider dataProvider

    @Autowired
    @Subject
    AccountService accountService

    def "given a real data provider when we use the real bean then we get our usual response"() {
        when: "we fetch our data"
        def result = accountService.getData("Something")

        then: "our real dataProvider responds"
        result == "Fetched: data for Something"
    }
}

4. Using Spock’s Spring Annotations

Now that we have our basic test let’s explore the options for stubbing our subject’s dependencies.

4.1. @StubBeans Annotation

We often write tests where we want a dependency stubbed out and don’t care about customizing its response. We can use Spock’s @StubBeans annotation to create Stubs for each dependency in our class.

So, let’s create a test Specification annotated with Spock’s @StubBeans annotation to stub our DataProvider class:

@StubBeans(DataProvider)
@ContextConfiguration(classes = [AccountService, DataProvider])
class AccountServiceStubBeansTest extends Specification {
    @Autowired
    @Subject
    AccountService accountService

// … }

Notice that we don’t need to declare a separate Stub for our DataProvider since the StubBeans annotation creates one for us.

Our generated Stub will return an empty string when our AccountService‘s getData method calls its fetchData method. Let’s create a test to assert that:

def "given a Service with a dependency when we use a @StubBeans annotation then a stub is created and injected to the service"() {
    when: "we fetch our data"
    def result = accountService.getData("Something")

    then: "our StubBeans gave us an empty string response from our DataProvider dependency"
    result == "Fetched: "
}

Our generated DataProvider stub returned an empty String from fetchData, causing our AccountService‘s getData to return “Fetched: ” with nothing appended.

When we want to stub more than one dependency, we use StubBeans with Groovy’s list [ ] syntax:

@StubBeans([DataProvider, MySecondDependency, MyThirdDependency])

4.2. @SpringBean Annotation

When we need to customize responses, we create a Stub, Mock, or Spy. So let’s use Spock’s SpringBean annotation inside our test, instead of the StubBeans annotation, to replace our DataProvider with a Spock Stub:

@SpringBean
DataProvider mockProvider = Stub()

Note that we can’t declare our SpringBean as a def or Object, we need to declare a specific type like DataProvider.

Now, let’s create an AccountServiceSpringBeanTest Specification with a test that sets up the stub to return “42” when its fetchData method is called:

@ContextConfiguration(classes = [AccountService, DataProvider])
class AccountServiceSpringBeanTest extends Specification {
    // ...
    def "given a Service with a dependency when we use a @SpringBean annotation then our stub is injected to the service"() {
        given: "a stubbed response"
        mockProvider.fetchData(_ as String) >> "42"

        when: "we fetch our data"
        def result = accountService.getData("Something")

        then: "our SpringBean overrode the original dependency"
        result == "Fetched: 42"
    }
}

The @SpringBean annotation ensures our Stub is injected into the AccountService so that we get our stubbed “42” response. Our @SpringBean-annotated Stub wins even when there’s a real DataProvider in the context.

4.3. @SpringSpy Annotation

Sometimes, we need a Spy to access the real object and modify some of its responses. So let’s use Spock’s SpringSpy annotation to wrap our DataProvider with a Spock Spy:

@SpringSpy
DataProvider mockProvider

First, let’s create a test that verifies our spied object’s fetchData method was invoked and returned the real “data for Something” response:

@ContextConfiguration(classes = [AccountService, DataProvider])
class AccountServiceSpringSpyTest extends Specification {
    @SpringSpy
    DataProvider dataProvider

    @Autowired
    @Subject
    AccountService accountService

    def "given a Service with a dependency when we use @SpringSpy and override a method then the original result is returned"() {
        when: "we fetch our data"
        def result = accountService.getData("Something")

        then: "our SpringSpy was invoked once and allowed the real method to return the result"
        1 * dataProvider.fetchData(_)
        result == "Fetched: data for Something"
    }
}

The @SpringSpy annotation wrapped a Spy around the auto-wired DataProvider and ensured our Spy was injected into the AccountService. Our Spy verified our DataProvider’s fetchData method was invoked without changing its result.

Now let’s add a test where our Spy overrides the result with “spied”:

def "given a Service with a dependency when we use @SpringSpy and override a method then our spy's result is returned"() {
    when: "we fetch our data"
    def result = accountService.getData("Something")

    then: "our SpringSpy was invoked once and overrode the original method"
    1 * dataProvider.fetchData(_) >> "spied"
    result == "Fetched: spied"
}

This time, our injected Spy bean verified our DataProvider‘s fetchData method was invoked and replaced its response with “spied”.

4.4. @SpringBean in a @SpringBoot Test

Now that we’ve seen the SpringBean annotation in our @ContextConfiguration test, let’s create another test class but use @SpringBootTest:

@SpringBootTest
class AccountServiceSpringBootTest extends Specification {
    // ...
}

The test in our new class is identical to the one we created in AccountServiceSpringBeanTest, so we won’t repeat it here!

However, our @SpringBootTest test class won’t run unless it has a SpringBootApplication, so let’s create a TestApplication class:

@SpringBootApplication
class TestApplication {
    static void main(String[] args) {
        SpringApplication.run(TestApplication, args)
    }
}

When we run our test, Spring Boot tries to initialize a DataSource. In our case, since we’re only using spring-boot-starter we don’t have a DataSource, so our test fails to initialize:

Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured.

So, let’s exclude DataSource auto-configuration by adding a spring.autoconfigure.exclude property to our SpringBootTest annotation:

@SpringBootTest(properties = ["spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration"])

Now, when we run our test, it runs successfully!

4.4. Context Caching

When using the @SpringBean annotation, we should note its impact on the Spring test framework’s context caching. Usually, when we run multiple tests annotated with @SpringBootTest, our Spring context gets cached rather than created each time. This makes our tests quicker to run.

However, Spock’s @SpringBean attaches a mock to a specific test instance similar to the @MockBean annotation in the spring-boot-test module. This prevents context-caching, which can slow down our overall test execution when we have a lot of tests that use them.

5. Conclusion

In this tutorial, we learned how to stub our Spring-managed dependencies using Spock’s @StubBeans annotation. Next, we learned how to replace a dependency with a Stub, Mock, or Spy using Spock’s @SpringBean or @SpringSpy annotation.

Finally, we noted that excessive use of the @SpringBean annotation can slow down the overall execution of our tests by interfering with the Spring test framework’s context caching. So, we should be judicious in our use of this feature!

As usual, the code for this article is available over on GitHub.

Course – LS – All
announcement - icon

Get started with Spring Boot and with core Spring, through the Learn Spring course:

>> CHECK OUT THE COURSE

res – REST with Spring (eBook) (everywhere)
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments