company_banner

REACT + JEST = TDD ❤️

    Привет, Хабр! Меня зовут Андрей Хижняк, я фронтенд-разработчик в команде, разрабатывающей App Store внутри ManyChat.

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

    От том, что из этого вышло, и будет моя статья, добро пожаловать под кат!



    Итак, начнем с определений


    • Test-Driven Development (TDD) — одна из техник экстремального программирования, основанная на 3-х шаговом цикле разработки:
      1. Пишем тест на функциональность, которую собираемся добавить.
      2. Пишем код, чтобы тест прошел.
      3. Делаем рефакторинг теста и кода, если нужно.
    • Jest — восхитительный (как они сами себя называют) JavaScript framework для тестирования с акцентом на простоту.
    • React — думаю, в представлении не нуждается.

    Зачем нужен TDD


    Многие разработчики до сих пор сомневаются в практический пользе TDD. Мне, однако, кажутся убедительными исследования с тремя группами разработчиков из Microsoft и одной из IBM, которые внедрили TDD. Результаты этих исследований показали, что количество pre-release bugs всех четырех продуктов снизилось на 40–90% по сравнению с аналогичными продуктами, в которых практика TDD не использовалась.

    Как и любая другая методология, TDD обладает не только плюсами, но и минусами. Если просто их перечислить, то из основных минусов можно выделить:

    1. Высокий порог вхождения — начинающим разработчикам будет труднее понять такой подход к разработке.
    2. Перманентная дисциплина — нельзя писать код раньше тестов.
    3. Снижение скорости разработки, большие ресурсные инвестиции на старте.
    4. Непонимание самой техники, неправильное применение.

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

    А теперь перейдем к плюсам:

    1. Качество — тестопригодный код, заранее покрытый тестами.
    2. Архитектура — TDD поощряет модульность (не поощряет связанность).
    3. Масштабируемость — модульный код покрытый тестами легко развивать и рефакторить.
    4. Устойчивость — коллеги ничего вам незаметно не сломают.
    5. Комфорт разработки — сначала пишется весь код, и только потом запускается проект для финального ручного тестирования и верстки.
    6. Ваш код делает ровно то, чего вы ожидаете от него в тестах.

    Почему мы выбрали TDD, а не просто Unit-тесты


    В какой-то момент договоренности о покрытии unit-тестами важной и сложной логики привели нас к тому, что покрытие кодовой базы было минимальным, да и тесты писались очень редко.

    Мы решили искать новые практики, которые помогли бы не только снизить количество багов, но и позволяли развивать Tecnhical Excellence. Все продуктовые команды в ManyChat сами выбирают, какие практики использовать в работе; наша команда решила поэкспериментировать и попробовать TDD. Этому выбору также поспособствовала внутренняя инженерная школа, направленная на улучшение процессов и инженерных практик, в рамках которой мы, в том числе, рассматривали TDD.

    Чтобы увидеть преимущества TDD (test first) методологии перед обычными unit-тестами (test last), достаточно пошагово сравнить два этих подхода.



    Первым шагом продумываем то, что собираемся написать


    При test last подходе не нужно много думать — можно сразу переходить к написанию кода. Однако разработка через тесты устроена совсем иным образом. Процесс мышления отличается и требует серьезной дисциплины и навыков. В зависимости от своего уровня можно выбрать один из следующих способов начать:

    1. Написать код и закомментировать его. Будьте готовы выбросить или переписать его после написания теста (уровень Junior).
    2. Представить код, который собираетесь написать, но не писать его (уровень Middle).
    3. Представить примерную архитектуру того, к чему собираетесь прикоснуться: как сущности и интерфейсы будут взаимодействовать (уровень Senior).

    В начале будут возникать объективные трудности, снизится скорость разработки — это нормально и, более того, неизбежно. Но опыт показывает, что со временем будет и повышаться уровень, и увеличиваться скорость в разработке через тесты. Нам понадобилось два месяца, чтобы вернуться на исходную скорость, но уже с TDD.

    Кроме того, хотя можно и сразу перейти к написанию кода при обычном тестировании, нужно понимать, что это то же самое, как действовать, заранее не задумываясь.

    Вторым шагом пишем Unit-тест


    Когда не нужно изучать код, который мы собираемся покрыть, писать тест очевидно проще. Нет кода — нет головной боли. Просто пишем unit-тест так, как хотим.

    При test last подходе придется не только потратить значительное время на изучение существующего кода, даже если мы написали его недавно, но и довольно часто его исправлять, чтобы он был пригодным для тестирования. Когда мы пишем код, не всегда удается держать в голове предстоящее написание unit-тестов. Вопрос мотивации также не обойти стороной, ведь когда код написан, еще нужно побороться с прокрастинацией и не заскучать до смерти, чтобы писать unit-тесты для уже работающего кода.

    Третьим шагом пишем код


    С TDD подходом нужно просто «позеленить» тест и не придумывать ничего лишнего. В то же время при test last подходе, вполне вероятно, мы напишем больше кода, чем нужно, тем самым добавив потенциально мертвый код (который, возможно, никогда нам не понадобится). Кроме того, придется покрыть его unit-тестами, тем самым увеличив расходы на тестирование.

    Четвертым шагом убеждаемся, что Unit-тест проходит


    Оба подхода требуют примерно одинаковых усилий для запуска unit-тестов в целях проверки. Однако при test last нужно будет постоянно подстраивать unit-тесты так, чтобы они наконец прошли. Ведь изменение рабочего кода — это последнее, чего мы хотим на этом этапе. Если мы все-таки изменим рабочий код, то нужно будет убедиться, что он все еще работает, а это может быть очень затратно. В то время как с TDD тест уже написан, и мы просто ожидаем, что он пройдет.

    Пятым шагом запускаем приложение для финальной проверки


    Здесь мы получаем еще одно значительное преимущество TDD. С ним необходимо запускать приложение только 1-2 раза и в большинстве случаев оно будет работать так, как мы ожидаем, потому что unit-тесты уже проверили код. Остается только поправить верстку и провести необходимое ручное тестирование.

    С другой стороны, при test last нужно запускать приложение множество раз при написании кода. Либо можно написать слишком много кода, а затем отлаживать и исправлять каждый шаг, либо писать небольшими частями и каждый раз их запускать. В любом случае выполнение, отладка и исправление выполняются намного дольше, чем просто запуск готовых unit-тестов, написанных через TDD.

    И победителем становится...


    Преимущество TDD над обычными unit-тестами, на мой взгляд, очевидно. Эта методология разработки мотивирует в первую очередь подумать, а не приступить. Это позволяет глубже погрузиться в задачу и не упустить всевозможные корнер-кейсы. Кроме того, мы экономим время и силы делая только то, что необходимо. В подарок к этому получается чистая модульная архитектура, покрытая тестами. И самое главное — наш код делает ровно то, чего мы от него ожидаем.

    TDD на практике


    От «it('renders component', () ⇒ {...})» до финального решения на примере модального окна с логикой


    В этом примере я сознательно откажусь от использования Redux и кастомных решений для react-test-renderer и Jest, чтобы снизить порог вхождения. Однако если эти темы вам интересны, сообщите об этом в комментариях, и мы напишем отдельную статью.

    Все тесты мы пишем с использованием react-test-renderer, а не testing-library, чтобы избежать затраты на зависимость от реального DOM. Это особенно важно, когда число unit-тестов в проекте уже превышает 4000.

    Задача


    Реализовать модальное окно публикации приложения.

    Критерии приемки


    1. Как пользователь, я хочу видеть модальное окно с названием приложения в заголовке.
    2. Как пользователь, я хочу отправить приложение на публикацию.
    3. Как пользователь, я хочу чтобы модальное окно закрылось после отправки на публикацию.
    4. Как пользователь, я хочу отправить public приложение на публикацию повторно.

    Реализация


    Вместе с переходом на TDD мы также перешли на TypeScript, поэтому все примеры будут показаны именно на нем.

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

    1.2
    Первый тест будет проверять, что модальное окно (из нашего UI Kit'a) отрендерилось с заданной шириной:

    // ApplicationPublishModal.test.tsx
    
    import React from 'react'
    import { create } from 'react-test-renderer'
    import * as ManyUI from '@manychat/manyui'
    // наш компонент - пока пустой
    import ApplicationPublishModal from '.'
    
    // нас не интересует реализация самого модального окна, поэтому делаем mock
    ManyUI.Modal = jest.fn().mockImplementation(() => null)
    
    describe('ApplicationPublishModal', () => {
      it('renders Modal with props open and width', () => {
        // arrange
        // достаточен mock модалки
    
        // act
        const renderer = create(<ApplicationPublishModal />)
    
        // assert
        const element = renderer.root.findByType(ManyUI.Modal)
        expect(element.props.open).toBe(true)
        expect(element.props.width).toBe(480)
      })
    })

    1.3

    Код минимально необходимый для того, чтобы «позеленить» наш тест:

    // ApplicationPublishModal.tsx
    
    import React from 'react'
    import { Modal } from '@manychat/manyui'
    
    const ApplicationPublishModal: React.FC = () => {
      return <Modal open width={480} />
    }
    
    export default ApplicationPublishModal
    

    1.4

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

    2.1
    Второй итерацией добавим в модальное окно заголовок, сформированный из названия приложения, полученного компонентом из props.

    2.2

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

    it('renders Modal with the correct title', () => {
      const renderer = create(<ApplicationPublishModal appName={'App Name'} />)
    
      const element = renderer.root.findByType(ManyUI.Modal)
      expect(element.props.title).toBe('Publish App Name app')
    })

    2.3

    import React from 'react'
    import { Modal } from '@manychat/manyui'
    
    const ApplicationPublishModal: React.FC<{ appName: string }> = ({ appName }) => {
    	const title = `Publish ${appName} app`
    
      return <Modal open width={480} title={title} />
    }
    
    export default ApplicationPublishModal

    2.4

    Убеждаемся, что тест прошел, и переходим к третьей итерации.

    3.1
    Теперь мы добавим кнопку, которая при клике будет вызывать метод onPublish, полученный компонентом из props, и закроет модальное окно.

    3.2

    Наш тест в таком случае должен проверять, что при клике по кнопке Publish вызывается метод onPublish и закрывается модальное окно:

    // события, действия, меняющие состояние компонента, необходимо оборачивать в act
    import { create, act } from 'react-test-renderer'
    
    it('calls method onPublish and closes the modal when button Publish is clicked', () => {
      const onPublishSpy = jest.fn()
      const renderer = create(<ApplicationPublishModal appName={'App Name'} onPublish={onPublishSpy} />)
      const element = renderer.root.findByType(ManyUI.Modal)
    	
    	// оборачиваем в act вызов события onClick, меняющий состояние компонента 
      act(() => {
        element.props.buttons
          .find((button: { label: string }) => button.label === 'Publish')
          .onClick()
      })
    
      expect(onPublishSpy).toHaveBeenCalledTimes(1)
      expect(element.props.open).toBe(false)
    })

    3.3

    import React, { useCallback, useState } from 'react'
    import { Modal } from '@manychat/manyui'
    
    const ApplicationPublishModal: React.FC<{ appName: string; onPublish: () => void }> = ({
      appName,
      onPublish,
    }) => {
      const [open, setOpen] = useState(true)
    
      const title = `Publish ${appName} app`
    
      const handlePublish = useCallback(() => {
        onPublish()
        setOpen(false)
      }, [onPublish])
    
      return (
        <Modal
          open={open}
          width={480}
          title={title}
          buttons={[{ label: 'Publish', onClick: handlePublish }]}
        />
      )
    }
    
    export default ApplicationPublishModal

    3.4

    Снова убеждаемся, что тест прошел, и переходим к заключительной итерации.

    4.1
    Итак, нам осталось добавить версионирование. Если в компонент придет prop — isPublic, тогда заголовок и название кнопки должны измениться на Republish.

    4.2

    it('renders button and title Republish when isPublic is true', () => {
      const renderer = create(<ApplicationPublishModal appName={'App Name'} isPublic />)
    
      const element = renderer.root.findByType(ManyUI.Modal)
      expect(element.props.title).toBe('Republish App Name app')
      expect(element.props.buttons[0].label).toBe('Republish')
    })

    4.3

    import React, { useCallback, useState } from 'react'
    import { Modal } from '@manychat/manyui'
    
    const ApplicationPublishModal: React.FC<{
      appName: string
      onPublish: () => void
      isPublic?: boolean
    }> = ({ appName, onPublish, isPublic }) => {
      const [open, setOpen] = useState(true)
    
      const actionLabel = `${isPublic ? 'Rep' : 'P'}ublish`
      const title = `${actionLabel} ${appName} app`
    
      const handlePublish = useCallback(() => {
        onPublish()
        setOpen(false)
      }, [onPublish])
    
      return (
        <Modal
          open={open}
          width={480}
          title={title}
          buttons={[{ label: actionLabel, onClick: handlePublish }]}
        />
      )
    }
    
    export default ApplicationPublishModal

    4.4

    И, наконец, проведем рефакторинг: добавим недостающие props в каждый тест, вынесем повторяющиеся значения в переменные.

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

    В итоге, за 4 итерации по TDD, мы получили следующий код:

    // ApplicationPublishModal.test.tsx
    
    import React from 'react'
    import { create, act } from 'react-test-renderer'
    import * as ManyUI from '@manychat/manyui'
    import ApplicationPublishModal from '.'
    
    ManyUI.Modal = jest.fn().mockImplementation(() => null)
    
    describe('ApplicationPublishModal', () => {
      const appName = 'App Name'
    
      it('renders Modal with props open and width', () => {
        const renderer = create(<ApplicationPublishModal appName={appName} onPublish={jest.fn()} />)
    
        const element = renderer.root.findByType(ManyUI.Modal)
        expect(element.props.open).toBe(true)
        expect(element.props.width).toBe(480)
      })
    
      it('renders Modal with the correct title', () => {
        const renderer = create(<ApplicationPublishModal appName={appName} onPublish={jest.fn()} />)
    
        const element = renderer.root.findByType(ManyUI.Modal)
        expect(element.props.title).toBe('Publish App Name app')
      })
    
      it('calls method onPublish and closes the modal when button Publish is clicked', () => {
        const onPublishSpy = jest.fn()
        const renderer = create(<ApplicationPublishModal appName={appName} onPublish={onPublishSpy} />)
        const element = renderer.root.findByType(ManyUI.Modal)
    
        act(() => {
          element.props.buttons
            .find((button: { label: string }) => button.label === 'Publish')
            .onClick()
        })
    
        expect(onPublishSpy).toHaveBeenCalledTimes(1)
        expect(element.props.open).toBe(false)
      })
    
      it('renders button and title Republish when isPublic is true', () => {
        const renderer = create(
          <ApplicationPublishModal appName={appName} onPublish={jest.fn()} isPublic />,
        )
    
        const element = renderer.root.findByType(ManyUI.Modal)
        expect(element.props.title).toBe('Republish App Name app')
        expect(element.props.buttons[0].label).toBe('Republish')
      })
    })

    // ApplicationPublishModal.tsx
    
    import React, { useCallback, useState } from 'react'
    import { Modal } from '@manychat/manyui'
    
    const ApplicationPublishModal: React.FC<{
      appName: string
      onPublish: () => void
      isPublic?: boolean
    }> = ({ appName, onPublish, isPublic }) => {
      const [open, setOpen] = useState(true)
    
      const actionLabel = `${isPublic ? 'Rep' : 'P'}ublish`
      const title = `${actionLabel} ${appName} app`
    
      const handlePublish = useCallback(() => {
        onPublish()
        setOpen(false)
      }, [onPublish])
    
      return (
        <Modal
          open={open}
          width={480}
          title={title}
          buttons={[{ label: actionLabel, onClick: handlePublish }]}
        />
      )
    }
    
    export default ApplicationPublishModal

    Заключение


    Что изменилось с момента внедрения TDD


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

    Однако спустя всего месяц активного применения TDD, вкупе с парным программированием, скорость разработки выровнялась и пошла вверх. Уже был сформирован достаточный фундамент, чтобы для большинства кейсов можно было найти аналогичный пример. Мы ощутили на практике все плюсы раздела «Зачем нужен TDD» уже в первый месяц.

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

    Переходите на TDD, пишите качественный и безопасный код. Буду рад ответить на ваши вопросы или подискутировать, если вы все еще сомневаетесь в пользе этого подхода.

    А также, выбирайте тему, о которой вам будет интересно прочитать в следующий раз в нашем блоге.

    Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

    В следующих сериях…

    • 21,9%Парное, моб-программирование через TDD, или как пинг-понг помогает одновременно повысить и скорость, и качество.7
    • 62,5%Как с помощью Redux + TDD сделать все компоненты независимыми и избавиться от props.20
    • 34,4%Кастомизация Jest, AAA и DSL, или как упростить тестирование.11
    ManyChat
    Будущее мессенджер-маркетинга

    Похожие публикации

    Комментарии 20

      –2
      Итак, взяли смешной детский пример, бесконечно далёкий от реальности, написали 29 строчек тривиальнейшего кода, и 50 строчек тестов. Наверное, это хорошо. Непонятно только, почему.

      И, разумеется, все тесты нафиг поломаются, если мы заменим одну либу модальных окон на другую с таким же API. Всё равно поломаются.

      Что конкретно призвана иллюстрировать статья? Что на простейший для понимания код легко и непринуждённо можно написать еще больше еще более сложного кода тестов?
        0
        Привет, JustDont! Спасибо за комментарий. Цели моей статьи — рассказать какие преимущества дает TDD, сравнить с test last подходом и показать на упрощенном примере с продакшена, как можно начать разрабатывать по TDD.

        В конце статьи я специально оставил опросник, чтобы в случае заинтересованности разобрать более сложные темы и примеры разработки по TDD.
          0
          рассказать какие преимущества дает TDD, сравнить с test last подходом и показать на упрощенном примере с продакшена, как можно начать разрабатывать по TDD

          Проблема в том, что ваш рассказ о преимуществах никак не коррелирует с приведенным примером. По преимуществам всё шоколадно, а в примере вы приводите тесты, которые во-первых не являются «юнит-» в строгом смысле определения, а во-вторых в половине случаев тестируют трюизмы.
            0
            Услышал ваш фидбэк, спасибо! Уже думаю над статьей продвинутого уровня со сложными задачами и тем, как TDD с ними справляется.
        0
        а вы как то формализовали что именно вы проверяете в тестах, а что нет? например css расположение элементов? сколько сценариев к компоненту?
        есть какие то критерии по которым вы говорите что тут полный тест а тут не полный?
          +1
          Привет, artemu78. Спасибо за вопросы! Касательно того, что тестируем, как я и сказал в статье, верстку мы тестами не покрываем. Покрывается все, что касается компонентов, логики и работы со стором. Количество сценариев зависит от сложности каждого конкретного компонента. При правильной разработке по TDD у вас не возникают вопросы о полноте тестов или количестве сценариев. Так как сначала вы пишите тест, а затем просто хотите этот тест “позеленить”, в таком случае, ваши тесты всегда будут полными.
          0

          Как мне кажется, минус в виде замедления скорости разработки, весьма существенный, с точки зрения бизнеса.

            0
            Modin, спасибо за комментарий! Вы правы, как я и сказал в статье, вначале будет заметное снижение скорости, и поэтому важно это бизнесу объяснить. Но в продуктовых компаниях, нацеленных на длительные рост и развитие, важнее не кратковременная просадка, а скорость и качество в длительном будущем. Поэтому в нашей компании, мы рассматриваем и пробуем любые варианты, которые в долгосрочной перспективе принесут нам больше пользы.
            +3
            Спасибо за статью! Вы молодцы! Я считаю, что использование в команде TDD — это уровень!

            Мне тоже нравится TDD, но я скептически отношусь к тестированию UI-компонентов. На мой взгляд, это слишком сложно. А инструменты для тестирования UI настолько наворочены, что из-за этого легко потерять суть. Поэтому я упоролся и попробовал перенести код компонента из примера на обычные объекты. Типа, упрощённая аналогия, чтобы проанализировать код и подумать головой при написании тестов. Сложно сказать, что из этого получилось. Но ясно одно — через TDD я бы так не написал.

            TDD — это не про то, что «пиши тесты на всё, пиши тесты впереди, будь героем!». TDD — это про то, что «пиши так, чтобы было максимально легко тестировать». В данном случае, видимо, мощность инструментов сводит TDD на нет.

            Упрощённая версия компонента
            export interface IModal {
            	close(): void;
            }
            
            export interface IModalParams {
            	opened: boolean;
            	width: number;
            	title: string;
            	buttonText: string;
            	onButtonClick: () => void;
            }
            
            export class ApplicationPublishModal {
            	constructor(
            		private appName: string,
            		private isPublic: boolean,
            		private createModal: (params: IModalParams) => IModal,
            		private onPublish: () => void,
            	) {}
            
            	// аналог render()
            	init(): void {
            		const modal = this.createModal({
            			opened: true,
            			width: 480,
            			title: this.getTitle(),
            			buttonText: this.getButtonText(),
            			onButtonClick: () => {
            				this.onPublish();
            				modal.close();
            			},
            		});
            	}
            
            	private getTitle(): string {
            		return !this.isPublic ? `Publish ${this.appName} app` : `Republish ${this.appName} app`;
            	}
            
            	private getButtonText(): string {
            		return !this.isPublic ? 'Publish' : 'Republish';
            	}
            }
            
            


            Тесты
            import { ApplicationPublishModal, IModal, IModalParams } from './q';
            
            export class ModalStub implements IModal, IModalParams {
            	opened: boolean = false;
            	width: number = 0;
            	title: string = '';
            	buttonText: string = '';
            	onButtonClick: () => void = () => {};
            	close(): void {
            		this.opened = false;
            	}
            }
            
            describe('ApplicationPublishModal', () => {
            	it('should open modal on init', () => {
            		const modal = new ModalStub();
            		const applicationPublishModal = createApplicationPublishModal({ modal });
            		applicationPublishModal.init();
            		expect(modal.opened).toBe(true);
            	});
            
            	it('should set modal width', () => {
            		const modal = new ModalStub();
            		const width = 480;
            		const applicationPublishModal = createApplicationPublishModal({ modal });
            		applicationPublishModal.init();
            		expect(modal.width).toBe(width);
            	});
            
            	it('should close modal on modal button click', () => {
            		const modal = new ModalStub();
            		const applicationPublishModal = createApplicationPublishModal({ modal });
            		applicationPublishModal.init();
            		modal.onButtonClick();
            		expect(modal.opened).toBe(false);
            	});
            
            	it('should call onPublish callback on modal button click', () => {
            		const modal = new ModalStub();
            		const spy = jasmine.createSpy();
            		const applicationPublishModal = createApplicationPublishModal({ modal, onPublish: spy });
            		applicationPublishModal.init();
            		modal.onButtonClick();
            		expect(spy).toHaveBeenCalled();
            	});
            
            	describe('should provide title to modal', () => {
            		it('if app not public', () => {
            			const modal = new ModalStub();
            			const applicationPublishModal = createApplicationPublishModal({
            				modal,
            				appName: 'AppName',
            				isPublic: false,
            			});
            			applicationPublishModal.init();
            			expect(modal.title).toBe('Publish AppName app');
            		});
            
            		it('if app public', () => {
            			const modal = new ModalStub();
            			const applicationPublishModal = createApplicationPublishModal({
            				modal,
            				appName: 'AppName',
            				isPublic: true,
            			});
            			applicationPublishModal.init();
            			expect(modal.title).toBe('Republish AppName app');
            		});
            	});
            
            	describe('should provide button text to modal', () => {
            		it('if app not public', () => {
            			const modal = new ModalStub();
            			const applicationPublishModal = createApplicationPublishModal({ modal, isPublic: false });
            			applicationPublishModal.init();
            			expect(modal.buttonText).toBe('Publish');
            		});
            
            		it('if app public', () => {
            			const modal = new ModalStub();
            			const applicationPublishModal = createApplicationPublishModal({ modal, isPublic: true });
            			applicationPublishModal.init();
            			expect(modal.buttonText).toBe('Republish');
            		});
            	});
            });
            
            interface ApplicationPublishModalParams {
            	appName?: string;
            	isPublic?: boolean;
            	modal: ModalStub;
            	onPublish?: () => void;
            }
            
            function createApplicationPublishModal({
            	appName = '',
            	isPublic = false,
            	modal,
            	onPublish = () => {},
            }: ApplicationPublishModalParams): ApplicationPublishModal {
            	return new ApplicationPublishModal(
            		appName,
            		isPublic,
            		(params) => Object.assign(modal, params),
            		onPublish,
            	);
            }
            
            



            Ну а теперь разбираемся… По сути ApplicationPublishModal — это враппер над переданной/захардкоженой модалкой. Он (почему-то) управляет открытием закрытием модалки, которая по идее могла бы управлять этим сама. Даже текст тестов намекает, что тут что-то не так. Ну и второе, что он делает, это определяет заголовок и текст кнопки. И тут самое оно! Если вы хотите быть уверенными, что заголовок/текст генерируется правильно в зависимости от флажка, вынесите это в отдельную чистую функция и покройте 10 тестами. Это будет супер легко, и по-TDD. Остальное — просто дублирование текста из файла с кодом, в файл с тестами. Не тратье свою жизнь на тестирование прокидывания параметров. Это ж рекурсия!)

            Извините, если слишком резко. Но это мой мнение. Вот пример моих тестов по TDD.

            Резюме: Вы на правильном пути. Но в системе есть места, типа функции main, которые сложно и бесполезно тестировать. Я считаю UI-компоненты такими местами. Лучший способ продолжать двигаться путем TDD — вытаскивать из компонентов всё что можно, оперировать чистыми функциями и объектами, тренироваться на них. Желаю удачи!
              0
              Самое любопытное, что в статье выдвинут тезис, что, мол, TDD лучше, потому что придётся сначала подумать над тем, что писать. И затем в качестве примера выдвигаются тесты, где тестируется прокидывание пропсов в захардкоженный библиотечный компонент.

              Страшновато себе представить, что вот это вот и выдаётся за результат «подумали». Что же там было бы без «подумали»?

              А юнит-тестирование чистых функций — это очень полезная штука, да. TDD или не TDD — дискуссионно, но сами юнит-тесты в этих случаях очень нужны. Насчёт остального (рендера и т.д.) — я придерживаюсь мысли, что упирать тут в «юнит-» не только бесполезно, но и вредно. Хорошие функциональные тесты реактовских компонентов я видел. Хорошие юнит-тесты — неа. Они или не «юнит», или ничего полезного не тестируют, или и то и другое.
                0

                Ещё не учтен момент про 40-90% багов, которые и в самих тестах проскочат, но не будут считаться багами, ведь покрыто)

                  0
                  Согласен, проще было тогда уж написать e2e тест на Cypress, по крайней мере было бы больше уверенности что с изменением компонента ничего не сломалось, критерием что компонент работает будет что-то в реальном браузере.
                  +1
                  Привет, Justerest! Спасибо за твой крутой содержательный комментарий и проделанный эксперимент! Понимаю откуда появляются вопросы к примеру, в этой статье я сознательно старался его упростить т.к. хотел, чтобы эта статья стала некоторым мотиватором начать, попробовать экстремальное программирование по TDD. Соответственно сам пример будет интересен в основном новичкам TDD, опытным разработчикам будет интереснее почитать о преимуществах и сравнении test first с test last. Однако теперь я вижу, что разбор более сложных примеров и тем будет интересен многим, поэтому уже начинаю думать над следующей статьей более продвинутого уровня, с упором на примеры.
                  Кроме того, спасибо за пожелания, и вам желаю удачи!
                  0
                  А есть какая-нибудь ретроспективная статистика? Объективная или хотя бы субъективная?
                  Тип по ощущением стало меньше мелких багов после изменений, или чё-то такое?
                    0
                    Привет, discopalevo. Спасибо за вопросы! Я думал над добавлением в заключение более точных цифр и графиков, но все-таки не стал перегружать этим статью. Если говорить о багах, то их число, в компонентах написанных по TDD, моментально снизилось и действительно приблизилось к нулю, благодаря чему нам удается не отвлекаться на это в спринте. Если говорить о скорости разработки, то этот график будет более плавным, из спринта в спринт кодовая база на TDD и опыт позволяют нам ускоряться, и если сравнивать емкость спринта сейчас и полгода назад, то при тех же ресурсах, мы делаем примерно в полтора раза больше задач.
                    0
                    Сам подход не нравится. Что будет если например потребуется отобразить предпросмотр перед публикацией и кнопку отменить? Или открыть модальное окно ещё раз?
                    И компонент Modal может превратиться в монстра с 20 возможными пропсами.

                    const ApplicationPublishModal = ({ actionLabel, appName, isPublic, publish }) => {
                      return (<Modal>
                        <Container width={480}>
                            <Title>{actionLabel} {appName} app</Title>
                    
                            <Button onClick={publish}>
                             `${isPublic ? 'Rep' : 'P'}ublish`
                            </Button>
                          </Container>
                        </Modal>
                      )
                    }
                    
                      +1
                      Эта методология разработки мотивирует в первую очередь подумать, а не приступить. Это позволяет глубже погрузиться в задачу и не упустить всевозможные корнер-кейсы.

                      А без TDD и тестов разве не нужно сначала подумать и вникнуть в задачу, перед тем как писать код? Разве не нужно продумывать всевозможные эдж-кейсы?

                      При test last подходе не нужно много думать — можно сразу переходить к написанию кода.

                      А если подумать много заранее? получается можно обойтись без TDD?
                      Т.е. получается какая разница где думать много — перед написанием кода, или перед написанием тестов? Всё-равно подумать много надо в любом случае. Получается TDD — лишнее звено?..)
                        0
                        Если код пишет толпа джуниоров то TDD и Typescript нужны чтобы гарантировать хоть какое-то качество кода.
                          0
                          Нет, это абсолютно никак не поможет.
                          Просто не надо делать так, чтоб код писала толпа джуниоров без присмотра. Иначе никакие best practices никого не спасут от грядущего «всё переписать», когда станет ясно, что проект неподдерживаем.
                        +1

                        Отличная мотивирующая статья!


                        Автор точно подметил психологические проблемы разработчиков при написании тестов, доступно описал все плюсы подхода TDD и показал на простом примере как это работает и какие результаты дает!


                        Многим авторам нужно поучиться так писать статьи.
                        Спасибо автору.

                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                        Самое читаемое