В начале у меня будет один вопрос к тебе дорогой читатель. Кодил ли ты когда-нибудь unit тесты на Groovy ? Если твоя профессия андроид-разработчик, то вероятность этого крайне мала. И я с таким не сталкивался до проекта Альфы.

Давай представим, что ты приходишь на проект и видишь такой тест:

class InvestmentsInstrumentsRepositorySpec extends Specification {

    @ClassRule
    @Shared
    RxJavaRule rxJavaRule

    def service = Mock(InvestmentsInstrumentsService)
    def repository = new InvestmentsInstrumentsRepository(service)

    def 'should call service and return proper response'() {
        given:
            def expectedResponse = InvestmentsInstrumentsTestDataKt.getInvestmentsInstrumentsResponse()
        when:
            def observer = repository.getInstruments().test()
        then:
            1 * service.getInvestmentsInstruments() >> Single.just(expectedResponse)
            observer.assertValue(expectedResponse)
    }
}

Не буду таить, моя реакция была примерно такой:

Но давай пока оставим помидоры в стороне и дадим шанс такому тесту.

Если присмотреться, в целом, ничего страшного в нём нет. Это тест на репозиторий, который мокает зависимости и проверяет, что при вызове определённого метода, один раз дергается нужный метод мока. Причем тест выглядит довольно красиво, очень помогают метки given/when/then (чуть позже мы посмотрим за счёт чего это работает).

Вот как выглядит такой же тест на JUnit 5, для сравнения:

@ExtendWith(RxJavaExtension::class)
class InvestmentsInstrumentsRepositoryTest {

    private val service = mockk<InvestmentsInstrumentsService>()
    private val repository = InvestmentsInstrumentsRepository(service)

    @Test
    fun `should call service and return proper response`() {
        // given
        val expectedResponse = getInvestmentsInstrumentsResponse()
        every { service.getInvestmentsInstruments() } returns Single.just(expectedResponse)
        // when
        val observer = repository.getInstruments().test()
        // then
        verify(exactly = 1) { service.getInvestmentsInstruments() }
        observer.assertResult(expectedResponse)
    }
}

Пока можем заметить, что работа с метками given/when/then выглядит не так приятно, но давай пойдём дальше.

Для написания тестов на Groovy у нас используется библиотека Spock. Spock как тестовый движок использует JUnit, причём начиная с версии 2.x это уже JUnit 5. Однако в нашем случае мы пользовались той версией, которая использует JUnit 4.

А теперь вопрос — слышал ли ты про Data Driven Testing ? Если нет, то сейчас я покажу пример:

@Unroll
    def 'should call paymentsMediator startActivity'() {
        given:
            featureToggle.isDisabled(Feature.REDESIGN) >> REDESIGN_TOGGLE
            featureToggle.isDisabled(Feature.WIDGET_PAYMENT_HUB) >> PAYMENT_TOGGLE
        when:
            router.openPayScreen(expectedAccountNumber)
        then:
            1 * paymentsMediator.startActivity(*_) >> { activity, actualAccountNumber ->
                assert actualAccountNumber == expectedAccountNumber
            }
        where:
            REDESIGN_TOGGLE | PAYMENT_TOGGLE
            false           | true
            true            | false
            false           | false
    }

И ещё один пример:

@Unroll
    def 'should map unread count and reports to common items list'() {
        when:
            def actualMappedData = mapper.map(
                    AccountStatementsTestDataKt.getReportsListResponse(),
                    unreadCountResponse
            )
        then:
            actualMappedData == expectedMappedData
        where:
            unreadCountResponse                                          | expectedMappedData
            AccountStatementsTestDataKt.gerUnreadCountResponseWithZero() | AccountStatementsTestDataKt.getBaseItemsListWithZeroCount()
            AccountStatementsTestDataKt.gerUnreadCountResponse()         | AccountStatementsTestDataKt.getBaseItemsListWithOneCount()
    }

Такие тесты называются параметризованными. В блоке where: описываем параметры и тест будет запускаться с каждым из этих параметров.

Такой подход позволяет сильно экономить на количестве кода, так как нам не надо описывать несколько однотипных тестов, чтобы проверить все условия. Такие тесты можно объединить в один параметризованный тест. И в Споке, как ты видишь, это выглядит довольно красиво. Если интересно можешь глянуть как такое делать в голом JUnit 4 или JUnit 5. Спойлер — это будет выглядеть более громоздко :

object ProvideAccountStatusNotClosed : ArgumentsProvider {

        override fun provideArguments(context: ExtensionContext?): Stream<out Arguments> {
            return TestParamsUtils.getParams<BrokerageAccountStatusesResponse>(
                InvestmentsDocumentsTestData.createBrokerageAccountStatusOpenResponse(),
                InvestmentsDocumentsTestData.createBrokerageAccountStatusPendingResponse(),
                InvestmentsDocumentsTestData.createBrokerageAccountStatusErrorResponse(),
                InvestmentsDocumentsTestData.createBrokerageAccountStatusUnknownResponse(),
            )
        }
    }

    @ParameterizedTest
    @ArgumentsSource(ProvideAccountStatusNotClosed::class)
    fun `should load documents if brokerage account not closed`(expectedStatusResponse: BrokerageAccountStatusesResponse) {
        // when
        presenter.onViewCreated()
        // then
        verify(exactly = 1) { mapper.prepareDocModelsList(expectedDocsResponse) }
    }

Выглядит не так уж поэтично, согласись.

Давай еще учитывать факт, что показан один из самых простых кейсов. Это я еще не говорю об остальных проблемах, которые появляются когда мы захотим иметь несколько параметризованных тестов в одном тест классе. А также я не говорю о том, что в JUnit 4 с параметризованными тестами дела обстоят ещё хуже.

И ещё один небольшой факт, у нас сейчас на проекте 3 тысячи параметризованных тестов.

Но у тебя может возникнуть вопрос. «Вот автор только и делает, что нахваливает тесты на Groovy, в чём же он проиграл?»

Не торопись, сейчас я все расскажу, до этого я пытался объяснить, почему мы решили писать тесты на Groovy :)

Минус №1

Всё ли нормально в этом тесте ?

Выглядит так, что всё нормально, студия ничего красным не подсвечивает. Но давай попробуем его запустить:

Не работает, в чем же дело? А дело в том, что в Groovy используется динамическая компиляция. Это одновременно и огромная сила, которая позволяет делать красивые тесты и одновременно огромное проклятье.

Но есть один workaround, мы можем поставить аннотацию @CompileStatic.

Мы забыли проставить импорт и теперь это подсвечивается. Но эту аннотацию не получится проставить на всех тестах, так как она убивает большинство синтаксических фич, которые мы видели в тестах выше. Ее можно использовать как своего рода маркер, когда ты не можешь понять в чём проблема.

Проблема выше стреляет часто, даже у тех, кто уже привык писать тесты на Groovy. А теперь представь, первый день на проекте и тебя попросили написать тесты. Готов поспорить, что все возможные шишки будут собраны прежде, чем тест запустится. Да и я ещё ни разу на собесе не сталкивался с ситуацией когда кандидат говорил «Да вы что, у вас тесты на Groovy!! Как здорово, у меня как раз много опыта в таких тестах!» То есть такие тесты добавляют дополнительный порог вхождения в проект.

Минус №2

Когда мы хотим написать тест, где используется зависимость из Android (так называемый интеграционный тест) мы используем robolectric. Для Spock есть обвязка вокруг robolectric, которая называется electricspock.

Видишь проблемы в скриншоте ниже?

Последние актуальные коммиты 4 года назад и electricspock, в целом, выглядит заброшенным. Можно сказать, что это не супер большая проблема.

Но такое утверждение работает ровно до того момента, когда тебе понадобится обновить версию AGP (Android Gradle plugin). В нашем кейсе мы получили большое количество неработающих тестов, которые использовали electricspock. Проблема была зарыта глубоко и нам пришлось форкать electricspock и добавлять свои фиксы. Во-первых, это создает громадный bus factor, а во-вторых, это решение работает ровно до следующего раза, когда понадобится обновить AGP.

К тому же, чтобы доработать electricspock, надо сначала разобраться как работает сам Spock. Сейчас я покажу небольшой фрагмент кода оттуда ты поймешь насколько это нетривиальная задача. Не будем погружаться супер глубоко:

Начинается все с класса SpockTransform. То есть мы уже имеем дело с компиляторным плагином.

Внутри SpockTransform создается SpecParser, SpecRewriter, SpecAnnotator, которые работает с AST нашего теста. Также нам придется разобраться как работает Sputnik — JUnit runner для Spock тестов:

В целом ничего сложного, нужно всего лишь изучить как работает рантайм Spock и тогда можно спокойно фиксить проблемы electricspock:

Но от проблемы bus factor это не спасает слова совсем. Особенно если учитывать то, как быстро развивается Android, Gradle и AGP вместе с ними.

Итоги

В какой-то момент проблемы стали бить слишком больно. И мы приняли решение начать постепенный переход на другой фреймворк для тестирования, а именно Kotest, который зиждется на JUnit 5. Почему мы выбрали именно его и как начали миграцию, я опишу в отдельной статье. Однако не стоит недооценивать Spock и тесты на Groovy, они действительно получаются менее объемными по количеству кода и более читаемыми.


Подписывайтесь на Телеграм-канал Alfa Digital — там мы постим новости, опросы, видео с митапов, краткие выжимки из статей, иногда шутим.

Читайте другие статьи из блога Альфа-Банка [подборка от редактора]:

Приходи к нам помогать с тестами, сейчас ищем Android-разработчика и QA Lead (Логистика).

Комментарии (21)


  1. aleksandy
    00.00.0000 00:00
    +1

    появляются когда мы захотим иметь несколько параметризованных тестов в одном тест классе

    Для junit5 вообще не проблема.

    нам пришлось форкать electricspock и добавлять свои фиксы.

    И где PR-ы в основной репозиторий?


    1. Ab0cha Автор
      00.00.0000 00:00
      +1

      Для ванильного junit5 в сложных кейсах это становится неподдерживаемым, в той статье, которую вы скинули как пример рассматриваются только простые кейсы)

      По electricspock доработки только в нашем форке, основной репозиторий уже никто не меинтейнит)


      1. zloddey
        00.00.0000 00:00

        Видимо, авторы тоже перешли на альтернативный фреймворк, лет 5 назад...


        1. Ab0cha Автор
          00.00.0000 00:00

          Вполне может быть :)


  1. KrutoyAn
    00.00.0000 00:00
    +1

    Интересный опыт ! Groovy.. он такой


  1. igor_suhorukov
    00.00.0000 00:00
    +1

    С Груви тестами и Spock так же работал без особого вдохновения. Но больше чем язык впечатления может испортить команда из написавшая. Если что я на хабре автор в топе Груви хаба…


    1. Ab0cha Автор
      00.00.0000 00:00

      Согласен, этот фактор тоже сильно повлиять может)


  1. sshikov
    00.00.0000 00:00
    +1

    Ну вообще, выглядят эти проблемы не совсем как проблемы груви. Если бы вы не разрабатывали под андроид, второго минуса у вас не было бы вообще. Я не к тому, что это не минус — но в это стоило бы отразить по четче. В чем-то это обратная сторона красивостей спока, слишком глубоко лезет в потроха.

    Ну и потом, вы же активно пользовались плюсами, если бы скажем у вас не было параметризованных тестов — вам бы пришлось написать на порядки больше тестов, и потратить время на их разработку. То есть, в чем-то вы может и проиграли, а в чем-то выиграли (например время), точно это сказать сложно. И то, что вы сейчас нашли фреймворк получше — вполне логично, в конце концов, я лично спок применял еще в лохматом 2010 году. То есть он далеко не новый, ему лет 14. С тех пор вполне могло и должно было появиться что-то получше.


    1. Ab0cha Автор
      00.00.0000 00:00

      Да, думаю стоило это чётче отразить, согласен с вашими поинтами)


  1. slonopotamus
    00.00.0000 00:00
    +1

    Проблема была зарыта глубоко и нам пришлось форкать electricspock и добавлять свои фиксы. Во-первых, это создает громадный bus factor

    Вы уверены что вы понимаете что такое bus factor?


    1. Ab0cha Автор
      00.00.0000 00:00

      Вполне. Возможно мы вкладываем разные понятия в этот термин)

      Бас фактор, приводит к тому, что когда увольняется человек, разобравшийся в electricspock достаточно глубоко, мы получаем проблему когда никто не знает, как исправить ошибку в библиотеке, если она возникнет.

      В любом случае это можно назвать даже train factor или как угодно, суть проблемы я описал в полной мере)


  1. lim14
    00.00.0000 00:00

    Если честно, не понял в чём было преимущество написания тестов на groovy? Пытались решить какую-то проблему таким подходом?


    1. YuryB
      00.00.0000 00:00

      скорее преимущество Spoc, автор не указал другие фичи, например когда у вас в ассерте выражение и оно не матчится, то в консоле будет расписано по шагам как оно вычисляется (результаты вызова цепочки foo.bar().substring(5) - вам напишут что было в foo что в bar и что после substring(5) ) а не просто тру!=фалсе - ошибка братан!

      ну и в целом тесты сильно короче и наглядней получаются в томчисле благодаря groovy - хочешь лист - просто пиши "[1,2]" и т.д. и т.п.

      другое дело, что это не java и надо порой шевелить мозгами и разбираться как с сами фреймворком так и груви, а всем лень.


    1. Ab0cha Автор
      00.00.0000 00:00

      В нашем случае одним из поинтов было то, что на споке получаются, читаемые и поддерживаемые параметризованные тесты, даже если они большие и их много на один тестовый класс)

      Плюс с точки зрения читаемости спок довольно хорош, когда надо понять верхнеуровнево, что проверяется в тесте)

      Плюс бекендеры в то время писали тесты на груви и мы решили перенять у них такую экспертизу (можно много обсуждать насколько это было удачное решение)


      1. Ab0cha Автор
        00.00.0000 00:00

        Юрий тоже привел хорошие плюсы)


  1. stopper79
    00.00.0000 00:00

    given:

    when:

    then:


    1. stopper79
      00.00.0000 00:00

      Удалите пожалуйста комментарий


      1. Ab0cha Автор
        00.00.0000 00:00

        У меня нет такой возможности(


        1. stopper79
          00.00.0000 00:00

          НЛО, помоги


  1. kaazak007
    00.00.0000 00:00
    +1

    Крутая статья!

    Если у вас только параметризованных тестов 3к, значит обычных еще больше, не думали ли написать автоматический конвертер спок2котест, базирующийся на PSI?)


    1. Ab0cha Автор
      00.00.0000 00:00

      Хороший вопрос !
      Задумывались, но пока пошли другим путем, однако в голове держим и этот вариант)