Статьи

Спок — Вернуть вложенных шпионов / издевается

Здравствуй! Некоторое время назад я написал статью о Mockito и использовании  RETURNS_DEEP_STUBS при работе с JAXB . Совсем недавно мы столкнулись с похожей проблемой с глубоко утерянным JAXB и потрясающей средой тестирования, написанной на Groovy, под названием  Spock . Родной Спок не поддерживает создание глубоких заглушек или шпионов, поэтому нам нужно было обойти это, и эта статья покажет вам, как это сделать.

Структура проекта

Мы будем работать с той же структурой данных, что и в  RETURNS_DEEP_STUBS, при работе со  статьей JAXB, поэтому структура проекта будет весьма схожей:

Как видите, основное отличие заключается в том, что тесты находятся в папке / test / groovy / вместо папки / test / java /.

Расширенная спецификация спока

Чтобы использовать Spock в качестве среды тестирования, вы должны создать тестовые сценарии Groovy, расширяющие класс спецификации Spock. Подробная информация о том, как использовать Спока, доступна  здесь . В этом проекте я создал абстрактный класс, который расширяет спецификацию и добавляет два дополнительных метода для создания вложенных тестовых двойников (я не помню, видел ли я где-нибудь в Интернете прототип этого подхода).

ExtendedSpockSpecification.groovy

package com.blogspot.toomuchcoding.spock;
 
import spock.lang.Specification
 
/**
 * Created with IntelliJ IDEA.
 * User: MGrzejszczak
 * Date: 14.06.13
 * Time: 15:26
 */
abstract class ExtendedSpockSpecification extends Specification {
    /**
     * The method creates nested structure of spies for all the elements present in the property parameter. Those spies are set on the input object.
     *
     * @param object - object on which you want to create nested spies
     * @param property - field accessors delimited by a dot - JavaBean convention
     * @return Spy of the last object from the property path
     */
    protected def createNestedSpies(object, String property) {
        def lastObject = object
        property.tokenize('.').inject object, { obj, prop ->
            if (obj[prop] == null) {
                def foundProp = obj.metaClass.properties.find { it.name == prop }
                obj[prop] = Spy(foundProp.type)
            }
            lastObject = obj[prop]
        }
        lastObject
    }
 
    /**
     * The method creates nested structure of mocks for all the elements present in the property parameter. Those mocks are set on the input object.
     *
     * @param object - object on which you want to create nested mocks
     * @param property - field accessors delimited by a dot - JavaBean convention
     * @return Mock of the last object from the property path
     */
    protected def createNestedMocks(object, String property) {
        def lastObject = object
        property.tokenize('.').inject object, { obj, prop ->
            def foundProp = obj.metaClass.properties.find { it.name == prop }
            def mockedProp = Mock(foundProp.type)
            lastObject."${prop}" >> mockedProp
            lastObject = mockedProp
        }
        lastObject
    }
}

Эти два метода работают очень похожим образом.

  • Если предположить, что свойство аргумента метода выглядит следующим образом: «abcd», то методы маркируют строку на «.» и перебрать массив — [«a», «b», «c», «d»]. 
  • Затем мы перебираем свойства  метакласса,  чтобы найти тот, чье имя равно prop (например, «a»). 
  • Если это так, то мы используем метод Спока Mock / Spy для создания Test Double данного класса (типа). 
  • Наконец, мы должны связать макет вложенного элемента с его родителем. 
    • Для Шпиона это легко, поскольку мы устанавливаем значение для родителя (lastObject = obj [prop]). 
    • Однако для макетов нам нужно использовать перегруженный оператор >> для записи поведения нашего макета — вот почему динамически вызывать свойство, присутствующее в переменной prop (lastObject. «$ {Prop}» >> mockedProp). 
  • Затем мы возвращаемся из закрытия инсценированного / шпионского экземпляра и повторяем процесс для него

Класс для тестирования

Давайте посмотрим на класс для тестирования:

PlayerServiceImpl.java

package com.blogspot.toomuchcoding.service;
 
import com.blogspot.toomuchcoding.model.PlayerDetails;
 
/**
 * User: mgrzejszczak
 * Date: 08.06.13
 * Time: 19:02
 */
public class PlayerServiceImpl implements PlayerService {
    @Override
    public boolean isPlayerOfGivenCountry(PlayerDetails playerDetails, String country) {
        String countryValue = playerDetails.getClubDetails().getCountry().getCountryCode().getCountryCode().value();
        return countryValue.equalsIgnoreCase(country);
    }
}

Тестовый класс

А теперь тестовый класс:

PlayerServiceImplWrittenUsingSpockTest.groovy

package com.blogspot.toomuchcoding.service
 
import com.blogspot.toomuchcoding.model.*
import com.blogspot.toomuchcoding.spock.ExtendedSpockSpecification
 
/**
 * User: mgrzejszczak
 * Date: 14.06.13
 * Time: 16:06
 */
class PlayerServiceImplWrittenUsingSpockTest extends ExtendedSpockSpecification {
 
    public static final String COUNTRY_CODE_ENG = "ENG";
 
    PlayerServiceImpl objectUnderTest
 
    def setup(){
        objectUnderTest = new PlayerServiceImpl()
    }
 
    def "should return true if country code is the same when creating nested structures using groovy"() {
        given:
            PlayerDetails playerDetails = new PlayerDetails(
                    clubDetails: new ClubDetails(
                            country: new CountryDetails(
                                    countryCode: new CountryCodeDetails(
                                            countryCode: CountryCodeType.ENG
                                    )
                            )
                    )
            )
 
        when:
            boolean playerIsOfGivenCountry = objectUnderTest.isPlayerOfGivenCountry(playerDetails, COUNTRY_CODE_ENG);
 
        then:
            playerIsOfGivenCountry
    }
 
    def "should return true if country code is the same when creating nested structures using spock mocks - requires CGLIB for non interface types"() {
        given:
            PlayerDetails playerDetails = Mock()
            ClubDetails clubDetails = Mock()
            CountryDetails countryDetails = Mock()
            CountryCodeDetails countryCodeDetails = Mock()
            countryCodeDetails.countryCode >> CountryCodeType.ENG
            countryDetails.countryCode >> countryCodeDetails
            clubDetails.country >> countryDetails
            playerDetails.clubDetails >> clubDetails
 
        when:
            boolean playerIsOfGivenCountry = objectUnderTest.isPlayerOfGivenCountry(playerDetails, COUNTRY_CODE_ENG);
 
        then:
            playerIsOfGivenCountry
    }
 
 
    def "should return true if country code is the same using ExtendedSpockSpecification's createNestedMocks"() {
        given:
            PlayerDetails playerDetails = Mock()
            CountryCodeDetails countryCodeDetails = createNestedMocks(playerDetails, "clubDetails.country.countryCode")
            countryCodeDetails.countryCode >> CountryCodeType.ENG
 
        when:
            boolean playerIsOfGivenCountry = objectUnderTest.isPlayerOfGivenCountry(playerDetails, COUNTRY_CODE_ENG);
 
        then:
            playerIsOfGivenCountry
    }
 
    def "should return false if country code is not the same using ExtendedSpockSpecification createNestedMocks"() {
        given:
            PlayerDetails playerDetails = Mock()
            CountryCodeDetails countryCodeDetails = createNestedMocks(playerDetails, "clubDetails.country.countryCode")
            countryCodeDetails.countryCode >> CountryCodeType.PL
 
        when:
            boolean playerIsOfGivenCountry = objectUnderTest.isPlayerOfGivenCountry(playerDetails, COUNTRY_CODE_ENG);
 
        then:
            !playerIsOfGivenCountry
    }
 
    def "should return true if country code is the same using ExtendedSpockSpecification's createNestedSpies"() {
        given:
            PlayerDetails playerDetails = Spy()
            CountryCodeDetails countryCodeDetails = createNestedSpies(playerDetails, "clubDetails.country.countryCode")
            countryCodeDetails.countryCode = CountryCodeType.ENG
 
        when:
            boolean playerIsOfGivenCountry = objectUnderTest.isPlayerOfGivenCountry(playerDetails, COUNTRY_CODE_ENG);
 
        then:
            playerIsOfGivenCountry
    }
 
    def "should return false if country code is not the same using ExtendedSpockSpecification's createNestedSpies"() {
        given:
            PlayerDetails playerDetails = Spy()
            CountryCodeDetails countryCodeDetails = createNestedSpies(playerDetails, "clubDetails.country.countryCode")
            countryCodeDetails.countryCode = CountryCodeType.PL
 
        when:
            boolean playerIsOfGivenCountry = objectUnderTest.isPlayerOfGivenCountry(playerDetails, COUNTRY_CODE_ENG);
 
        then:
            !playerIsOfGivenCountry
    }
 
 
}
Давайте рассмотрим методы тестирования один за другим . Сначала я представляю код, а затем краткое описание представленного фрагмента.

def "should return true if country code is the same when creating nested structures using groovy"() {
    given:
        PlayerDetails playerDetails = new PlayerDetails(
                clubDetails: new ClubDetails(
                        country: new CountryDetails(
                                countryCode: new CountryCodeDetails(
                                        countryCode: CountryCodeType.ENG
                                )
                        )
                )
        )
 
    when:
        boolean playerIsOfGivenCountry = objectUnderTest.isPlayerOfGivenCountry(playerDetails, COUNTRY_CODE_ENG);
 
    then:
        playerIsOfGivenCountry
}

Здесь вы можете найти подход к созданию вложенных структур с помощью функции Groovy для передачи свойств, которые должны быть установлены в конструкторе.

def "should return true if country code is the same when creating nested structures using spock mocks - requires CGLIB for non interface types"() {
given:
PlayerDetails playerDetails = Mock()
ClubDetails clubDetails = Mock()
CountryDetails countryDetails = Mock()
CountryCodeDetails countryCodeDetails = Mock()
countryCodeDetails.countryCode >> CountryCodeType.ENG
countryDetails.countryCode >> countryCodeDetails
clubDetails.country >> countryDetails
playerDetails.clubDetails >> clubDetails
when:
boolean playerIsOfGivenCountry = objectUnderTest.isPlayerOfGivenCountry(playerDetails, COUNTRY_CODE_ENG);
then:
playerIsOfGivenCountry
}

Здесь вы можете найти тест, который создает макеты с использованием Spock — учтите, что вам нужен CGLIB в качестве зависимости, когда вы моделируете неинтерфейсные типы.

def "should return true if country code is the same using ExtendedSpockSpecification's createNestedMocks"() {
given:
PlayerDetails playerDetails = Mock()
CountryCodeDetails countryCodeDetails = createNestedMocks(playerDetails, "clubDetails.country.countryCode")
countryCodeDetails.countryCode >> CountryCodeType.ENG
when:
booleanplayerIsOfGivenCountry = objectUnderTest.isPlayerOfGivenCountry(playerDetails, COUNTRY_CODE_ENG);
then:
playerIsOfGivenCountry
}

Здесь у вас есть пример создания вложенных макетов с использованием метода createNestedMocks.

def "should return false if country code is not the same using ExtendedSpockSpecification createNestedMocks"() {
given:
PlayerDetails playerDetails = Mock()
CountryCodeDetails countryCodeDetails = createNestedMocks(playerDetails, "clubDetails.country.countryCode")
countryCodeDetails.countryCode >> CountryCodeType.PL
when:
booleanplayerIsOfGivenCountry = objectUnderTest.isPlayerOfGivenCountry(playerDetails, COUNTRY_CODE_ENG);
then:
!playerIsOfGivenCountry
}

Пример, показывающий, что создание вложенных макетов с использованием метода createNestedMocks действительно работает — должно возвращать false для неправильного кода страны.

def "should return true if country code is the same using ExtendedSpockSpecification's createNestedSpies"() {
given:
PlayerDetails playerDetails = Spy()
CountryCodeDetails countryCodeDetails = createNestedSpies(playerDetails, "clubDetails.country.countryCode")
countryCodeDetails.countryCode = CountryCodeType.ENG
when:
booleanplayerIsOfGivenCountry = objectUnderTest.isPlayerOfGivenCountry(playerDetails, COUNTRY_CODE_ENG);
then:
playerIsOfGivenCountry
}

Здесь у вас есть пример создания вложенных шпионов с использованием метода createNestedSpies.

def "should return false if country code is not the same using ExtendedSpockSpecification's createNestedSpies"() {
given:
PlayerDetails playerDetails = Spy()
CountryCodeDetails countryCodeDetails = createNestedSpies(playerDetails, "clubDetails.country.countryCode")
countryCodeDetails.countryCode = CountryCodeType.PL
when:
booleanplayerIsOfGivenCountry = objectUnderTest.isPlayerOfGivenCountry(playerDetails, COUNTRY_CODE_ENG);
then:
!playerIsOfGivenCountry
}

Пример, показывающий, что создание вложенных шпионов с использованием метода createNestedSpies действительно работает — должно возвращать false для неправильного кода страны.

Резюме

В этом посте я показал вам, как вы можете создавать вложенные макеты и шпионы, используя Спока. Это может быть полезно, особенно когда вы работаете с вложенными структурами, такими как JAXB. Тем не менее, вы должны иметь в виду, что эти структуры в некоторой степени нарушают закон Деметры. Если вы посмотрите мою предыдущую статью о Mockito, вы увидите, что:


Мы получаем вложенные элементы из сгенерированных JAXB классов.
Хотя он нарушает 
закон Деметры,  довольно часто называют методы 
структур,  потому что сгенерированные JAXB классы на самом деле являются структурами, поэтому на самом деле я полностью согласен с 
Мартином Фаулером, что его следует называть предложением Деметры .

И в случае этого примера идея та же самая — мы говорим о структурах, поэтому мы не нарушаем Закон Деметры.

преимущества

  • С помощью одного метода вы можете шутить / шпионить за вложенными элементами
  • Очиститель кода, чем создание тонны объектов, которые затем необходимо установить вручную

Недостатки

  • Ваша IDE не поможет вам предоставить имена свойств, так как свойства представлены в виде строк
  • Вы должны установить Test Doubles только в контексте Спецификации (и иногда вы хотите выйти за пределы этой области)

источники

Как обычно, исходники доступны на 
BitBucket  и 
GitHub .