Moki, stubs and spies in the Spock Framework
- Transfer
Spock provides 3 powerful (but different in essence) tools that simplify writing tests: Mock, Stub and Spy.
Quite often, the code that needs to be tested needs to interact with external modules called dependencies (in the original article, the term collaborators is used, which is not very common in the Russian-speaking environment).
Unit tests are most often developed for testing one isolated class using various mock options: Mock, Stub and Spy. So the tests will be more reliable and will be less likely to break as the code of dependencies evolves.
Such isolated tests are less prone to problems when changing the internal details of the implementation of dependencies.
From the translator: every time I use the Spock Framework to write tests, I feel that I can be mistaken when choosing a method for replacing dependencies. This article has the shortest possible crib for choosing a mechanism for creating mocks.
TL; DR
Mocks
Use Mock to:
- contract verification between code under test and dependencies
- checks that dependency methods are called the correct number of times
- checking the correctness of the parameters with which the dependency code is called
Stubs
Use Stub to:
- providing predefined call results
- performing predefined actions expected from dependencies, such as throwing exceptions
Spies
Fear Spyware (Spy). As stated in the Spock documentation:
Think twice before using this mechanism. Perhaps you should redesign your solution and reorganize your code.
But it just so happens that there are situations when we have to work with legacy code. Legacy code can be difficult or even impossible to test with mocks and stubs. In this case, there is only one solution - use Spy.
It is better to have legacy code covered with tests using Spy than not having any legacy tests.
Use Spy to:
- testing legacy code that cannot be tested by other methods
- checks that dependency methods are called the correct number of times
- validation of transmitted parameters
- provide a predetermined response to dependencies
- performing predefined actions in response to calls to dependency methods
Mocks
The whole power of mocks is manifested when the task of the unit test is to verify the contract between the code being tested and the dependencies. Let's look at the following example, where we have a controller FooController
that uses FooService
as a dependency, and test this functionality using mocks.
FooController.groovy
package com.mycompany.myapp
import groovy.transform.CompileStatic
@CompileStaticclassFooController{
FooService fooService
def doSomething(){
render fooService.doSomething("Sally")
}
}
FooService.groovy
package com.mycompany.myapp
import groovy.transform.CompileStatic
@CompileStaticclassFooService{
String doSomething(String name){
"Hi ${name}, FooService did something"
}
}
In this scenario, we want to write a test that checks:
- contract between
FooController
andFooService
FooService.doSomething(name)
is called the correct number of timesFooService.doSomething(name)
called with the correct parameter
Take a look at the test:
MockSpec.groovy
package com.mycompany.myapp
import grails.testing.web.controllers.ControllerUnitTest
import spock.lang.Specification
classMockSpecextendsSpecificationimplementsControllerUnitTest<FooController> {
void"Mock FooService"() {
given: "создаём мок зависимости"
def fooService = Mock(FooService)
and: "устанавливаем экземпляр мока в контроллер"
controller.fooService = fooService
when: "вызываем действие контроллера"
controller.doSomething()
then: "мок можно использовать для проверки числа вызовов и значений параметров"1 * fooService.doSomething("Sally")
and: "мок возвращает 'пустое' значение по умолчанию - 'null'"
response.text == null.toString()
}
}
The above test creates a mock service:
def fooService = Mock(FooService)
The test also checks that it FooService.doSomething(name)
is called once, and the parameter passed to it matches the string "Sally"
.
1 * fooService.doSomething("Sally")
The above code solves 4 important tasks:
- creates mock for
FooService
- makes sure that it
FooService.doSomething(String name)
is called exactly once with a parameterString
and a value"Sally"
- isolates the code under test, replacing the dependency implementation
Stubs
Does the code being tested use dependencies? Is the purpose of testing to make sure that the code under test works correctly when interacting with dependencies? Are the results of the dependency method calls the input values for the code under test?
If the behavior of the code being tested changes depending on the behavior of the dependencies, then you need to use stubs.
Let's look at the following example with FooController
and FooService
and test the functionality of the controller by using stubs.
FooController.groovy
package com.mycompany.myapp
import groovy.transform.CompileStatic
@CompileStaticclassFooController{
FooService fooService
def doSomething(){
render fooService.doSomething("Sally")
}
}
FooService.groovy
package com.mycompany.myapp
import groovy.transform.CompileStatic
@CompileStaticclassFooService{
String doSomething(String name){
"Hi ${name}, FooService did something"
}
}
Test code:
StubSpec.groovy
package com.mycompany.myapp
import grails.testing.web.controllers.ControllerUnitTest
import spock.lang.Specification
classStubSpecextendsSpecificationimplementsControllerUnitTest<FooController> {
void"Stub FooService"() {
given: "создаём стаб сервиса"
def fooService = Stub(FooService) {
doSomething(_) >> "Stub did something"
}
and: "устанавливаем экземпляр стаба в контроллер"
controller.fooService = fooService
when: "вызываем действие контроллера"
controller.doSomething()
then: "стаб возвращает константное значение"// 1 * fooService.doSomething()// проверки числа вызовов не поддерживаются для стабов
response.text == "Stub did something"
}
}
You can create a stub like this:
def fooService = Stub(FooService) {
doSomething(_) >> "Stub did something"
}
The above code solves 4 important tasks:
- creates a stub
FooService
- makes sure that it
FooService.doSomething(String name)
returns a string"Stub did something"
regardless of the parameter passed (so we used the symbol_
) - isolates the code under test, replacing the dependency implementation with a stub
Spies
Please do not read this section.
Do not look.
Skip to the next one.
Still reading? Well, well, let's deal with Spy.
Do not use spy. As stated in the Spock documentation:
Think twice before using this mechanism. Perhaps you should redesign your solution and reorganize your code.
At the same time, there are situations when we have to work with legacy code. Legacy code can not be tested using mocks or stubs. In this case, the spy remains the only viable option.
Spies are different from mocks or stubs, because they do not work as stubs.
When a dependency is replaced by a mock or a stub, a test object is created, and the real dependency source code is not executed.
The spy, on the other hand, will execute the main source code of the dependency for which the spy was created, but the spy will allow you to change what the spy returns, and check for method calls, as well as moki and stubs. (Hence the name Spy).
Let's take a look at the following example FooController
that uses FooService
, and then test the functionality with a spy.
FooController.groovy
package com.mycompany.myapp
import groovy.transform.CompileStatic
@CompileStaticclassFooController{
FooService fooService
def doSomething(){
render fooService.doSomething("Sally")
}
}
FooService.groovy
package com.mycompany.myapp
import groovy.transform.CompileStatic
@CompileStaticclassFooService{
String doSomething(String name){
"Hi ${name}, FooService did something"
}
}
Test code:
SpySpec.groovy
package com.mycompany.myapp
import grails.testing.web.controllers.ControllerUnitTest
import spock.lang.Specification
classSpySpecextendsSpecificationimplementsControllerUnitTest<FooController> {
void"Spy FooService"() {
given: "создаём экземпляр-шпион"
def fooService = Spy(FooService)
and: "устанавливаем зависимость в контроллер"
controller.fooService = fooService
when: "вызываем действие контроллера"
controller.doSomething()
then: "проверяем число вызовов и значения параметров"1 * fooService.doSomething("Sally") >> "A Spy can modify implementation"
and: 'шпион может изменять реализацию методов зависимостей'
response.text == "A Spy can modify implementation"
}
}
Creating a spy instance is quite simple:
def fooService = Spy(FooService)
In the above code, the spy allows us to check the call FooService.doSomething(name)
, the number of calls and the parameter values. Moreover, the spy modifies the implementation of the method to return a different value.
1 * fooService.doSomething("Sally") >> "A Spy can modify implementation"
The above code solves 4 important tasks:
- creates a spy instance for
FooService
- checks interaction with dependencies
- Checks how the application works according to the specific results of the calls to the dependency method.
- isolates the code under test, replacing the dependency implementation with a stub
FAQ
Which option to use: Mock, Stub or Spy?
This is a question that many developers face. This FAQ can help if you are unsure which approach to use.
Q: Is the goal of testing the verification of the contract between the code being tested and the dependencies?
A: If you answered yes, use mock
Q: Is the purpose of testing to make sure that the code under test works correctly when interacting with dependencies?
A: If you answered yes, use Stub
Q: Are the results of the dependency method calls the input values for the code under test?
A: If you answered yes, use Stub
Q: Do you work with Legacy code that is very difficult to test, and you have no options left?
A: Try using spy
Code of examples
You can find the code for all examples of this article at the link:
https://github.com/ddelponte/mock-stub-spy