Как стать автором
Обновить

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

НЛО прилетело и опубликовало эту надпись здесь
Почему это? Можно ведь «тесту после кода» просто не дать никакого кода и проверить его работу, не так ли?
Coverage ничего не покажет?
Ничего.
Например, написал я недавно тест:
self.assertEqual(str(obj), name)

потом немного дописал и решил упростить:
assert  str(obj), name

Ошибку видите? А покрытие отличное — тесты прошлись по этой строке.
Так TDD живо?! Ну, прям камень с души…
Статья о том что в интернете можно найти кучу исследований «высосанных из пальца», а еще кучу неверных интерпретаций? Дочитал до конца и понял что потратил свое время в пустую, если вы еще не читали не делайте этого. :)

Я вот после первого абзаца пролистал до комментариев :-)

Я бы представил пару тестов, а потом написал бы удовлетворяющий их код. Потом бы написал тесты.

Глупость какая-то. Так поступили бы только фанаты TDD. Обычный программист бы просто реализовал некоторую часть функцинала и когда он бы с ней закончил и понял, что более менять его API не будет — только тогда уже стал думать какие тесты нужно написать и какие моменты нужно протестировать. А тестирование белого ящика эффективней, так как программист знает все внутренние критические точки. TDD же провоцирует тестировать чёрный ящик, когда реализации ещё нет и соответственно ты не знаешь при каких значениях у тебя может всё сломаться.

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

Как быть с ситуациями, когда это знание ошибочно?

Так же как и с ситуациями, когда этого знания нет.

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

Эффективность выше, но не 100% от недостижимого максимума.

Такая же как и любых других тестов.

У меня есть ощущение что я не понял что вы имеете ввиду под тестами белого ящика. Можете поподробнее, чем это отличается от тестов "черного ящика"? И какой в них смысл?

Термин "белый ящик" — это антитеза "чёрному ящику". А "чёрный ящик" — это модель системы, игнорирующая внутреннее устройство. Соответственно, тестирование белого ящика — это просто тестирование. Без делания вида, что можно полноценно покрыть систему тестами, не учитывая её внутренние особенности.

не учитывая её внутренние особенности.

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


Я к тому что...


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

Я не вижу никакой разницы. Что те тесты что другие должны проверять пост/пред условия и инварианты, а что бы "не знать на чем будет ломаться" и не думать об этом можно просто использовать большие наборы рандомных данных (property based testing).


В целом кейс когда вы знаете что что может сломаться в вашем коде не так интересны обычно так как если вы это знаете — оно будет редко валяться.

Рандомные тесты — не очень надёжное и быстрое решение. К тому же рандомизировать надо не только данные но и то в какой последовательности они поступают. Короче говоря, попытка проверить все кейсы упирается в "комбинаторный взрыв". Поэтому-то и вводятся так называемые классы эквивалентности. Но они зависят от внутренней реализации. И даже если в конкретном случае вы не учитываете особенности конкретной реализации — вы всё равно берёте эти классы не с потолка, а основываясь на опыте реализации похожих задач в прошлом — с какими значениями могут быть потенциальные проблемы.

Проблема то в том, что вы покрываете только те проблемы, о которых знаете. А как я уже говорил — это не самые интересные проблемы.

(зависит от того, что вы понимаете под эффективностью)


Когда вы делаете только белый ящик, ваше тестовое покрытие основано на реализации. И поэтому пропустить какой-то реальный ошибочный кейс вполне реально.


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


(PS ваш кейс с четными/нечетными массивами в сортировке традиционно покрывается диапазоном автосгенеренных входных данных)

Когда вы делаете только белый ящик, ваше тестовое покрытие основано на реализации.

Оно учитывает реализацию, добавляя дополнительные классы входных параметров. Все остальные классы оно не исключает.


И поэтому пропустить какой-то реальный ошибочный кейс вполне реально.

В любом случае есть шанс пропустить какой-то реальный ошибочный кейс.


Так что эффективней — сначала тестирование как черного ящика, которое гарантирует соответствие требованиям

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


ваш кейс с четными/нечетными массивами в сортировке традиционно покрывается диапазоном автосгенеренных входных данных

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

Все остальные классы оно не исключает.

… а чтобы их сгенерить, вам нужна та же логика, что и для черного ящика. QED.


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

Вот вам ваш же ответ:


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

Или наоборот: чёрный ящик — это белый ящик из которого вырезали часть логики.

черный ящик это черный ящик. Например моки мы должны использовать для того, что бы какие-то предусловия задавать или постусловия проверять, если объект зависит или влияет на окружающим мир или должен с кем-то общаться. От этого тестируемый код как ящик "белее" не становится. Нет?

Вот у вас есть функция snakeToCamelCase( id : string ) : string. Что нужно замочить, чтобы протестировать эту функцию?

ничего, оно ж "чистое". Нет взаимодействия с внешним миром или зависимости от него — нет необходимости как-то что-то мокать.


Но и особенности реализации учитывать тут тоже не нужно. Функция snakeToCamelCase будет для нас черным ящиком. А если есть еще обратная операция, можно гонять на рандомных тестовых данных и тестить сразу две функции.

А теперь смотрим на реализацию:


const toLocaleUpperCase = str => {
    return str.replace( /i/g , "İ" ).toLocaleUpperCase()
}

const snakeTokenToUpperCaseCache = {}

const snakeTokenToUpperCase = token => {
    const cached = snakeTokenToUpperCaseCache[ token ]
    if( cached ) return cached

    const letter = snakeTokenToUpperCaseCache[ token ] = toLocaleUpperCase( token[1] )
    if( letter === token[1] ) throw new Error( `Wrong token: ${ JSON.stringify( token ) }`  )

    return letter
}

const snakeToCamelCase = id => {
    return id.replace( /(_.)/g , snakeTokenToUpperCase )
}

Всё ещё считаете её чистой?

В целом… если состояние изолировано в пределах этого модуля, то все ок. Сайд эффектов же снаружи это не будет вызывать. А вот если к "кэшу" имеет доступ не только эта функция — то тогда у нас проблемы, да. Точнее у автора кода проблемы.

Чистота — это не только отсутствие сайд эффектов, но и отсутствие зависимости от изменяемого состояния. Данный код, например, не проходит следующий тест: https://habrahabr.ru/post/314994/#comment_9907672

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

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

Вы свои слова можете подтвердить хоть какими-то исследованиями или же судите по себе?

Конечно, по себе сужу.

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

Тестировать черный ящик может только посторонний.
Тестировать черный ящик может только посторонний.

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

Зато и тесты, и реализация подвержены общим когнитивным искажениям.

> они менее подвержены влиянию этой самой реализации,

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

Это не изменится в том случае, если я буду писать тесты после реализации.


Вы так говорите, как будто это что-то хорошее.

Да, я считаю, что это что-то хорошее, потому что в этом случае основным, что влияет на тесты, будут требования.


(собственно, было более одного раза, когда я вылавливал ошибки в требованиях просто попытавшись написать на них тесткейсы)

> Да, я считаю, что это что-то хорошее, потому что в этом случае основным, что влияет на тесты, будут требования.

Боюсь основным, что влияет на тесты будут всё же когнитивные искажения )

> (собственно, было более одного раза, когда я вылавливал ошибки в требованиях просто попытавшись написать на них тесткейсы)

А разве TDD — это не по одному тесту за раз? (поприветствуем искажения)

Было более одного раза, когда ошибку в требованиях я находил только в процессе реализации. Написанные тесты внесение изменений не упрощали.
А разве TDD — это не по одному тесту за раз? (поприветствуем искажения)

А я и не говорил, что я придерживаюсь классического TDD. Я говорил "пишу тесты до реализации".


Было более одного раза, когда ошибку в требованиях я находил только в процессе реализации.

Так у меня тоже было, несомненно.

А разве TDD — это не по одному тесту за раз? (поприветствуем искажения)

Ну Кент Бэк в своей книжке говорит что размер цикла определяется разработчиком. Если вы делаете что, и вы уверены в том что делаете — вы можете писать сразу больше тестов, больше кода и больше рефакторить, но как-только уверенность гаснет (слишком часто все идет немного не так как вы планировали) — лучше уменьшать размеры цикла.


Ну то есть жесткого "сначала один тест и потом один код" нет. Например вы вполне можете воспользоваться триангуляцией и написать сходу 2-3 тест кейса, что бы не отвлекаться потом.

Если есть чёткие требования, то тесткейсов можно уверенно сотню написать.

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


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


Но такое было всего пару раз.

Не подверженность влиянию реализации — не самоцель.

Однако… давайте рассуждать логически. Я когда пишу тесты, мне важно не то, как это сейчас работает, а что это будет работать и дальше. Даже если я вдруг решу "поменять" способ, которым это реализовано от слова совсем. В этом случае "знания о реализации в тестах" будут скорее обузой нежели полезным свойством.

Я вижу в принципе затруднительным написать тест, который всерьёз знает не только о контракте метода, но и о внутренностях. И это не в контексте TDD/TLD и черно-белого ящика, а профпригодности.

Да-да, всякий, кто не согласен с вашими убеждениями — профнепригоден.

Раз уж вы хотите об этом поговорить — приведите пример хорошего теста, который знает о внутренностях метода?

Простите, а какова ценность данного тест кейса? Что вы пытаетесь проверить оным? В вашем примере вы получите исключение и результат никогда не будет записан в кэш. И второй вызов так же не дойдет до кэша. А в названии тест кейса что-то про кэширование.


Вывод я могу сделать такой — тесты ради тестов. По факту вы ничего не проверяете. И это как раз следствие того, что вы знаете что внутри используется кэш. И вы как бы знаете так же что при исключении до кэша не дойдет. Тогда зачем это проверять?

Что вы пытаетесь проверить оным?

Вот это:


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



По факту вы ничего не проверяете.

А почему тест падает? ;-)


И вы как бы знаете так же что при исключении до кэша не дойдет. Тогда зачем это проверять?

Я знаю, что оно не должно доходить. А чтобы убедиться, что это так, и нужны тесты.

А почему тест падает? ;-)

а он падает?)


Я знаю, что оно не должно доходить. А чтобы убедиться, что это так, и нужны тесты.

тесты нужны не для этого. Тесты нужны чтобы регрессии искать. Простая проверка. Если вы удалите свой кэш из реализации, и прогоните тесты, и они не упадут (а они не упадут из того что вы написали) — значит вы хреново покрыли код тестами.


Да и с точки зрения покрытия кода тестами ваш тест кейс который всего-лишь два раза вызывает код, всеравно не дойдет до кэша и его не покроет. А позитивные тест кейсы — могут покрыть.

> Если вы удалите свой кэш из реализации, и прогоните тесты, и они не упадут

А при рефакторинге у вас регрессий не случается?

случается, но это значит что мне надо фиксить регрессию. Тесты трогать в этом случае нельзя.

а он падает?)

Проверьте.


Если вы удалите свой кэш из реализации, и прогоните тесты, и они не упадут (а они не упадут из того что вы написали) — значит вы хреново покрыли код тестами.

Ну да, по хорошему надо ещё проверить, что значения действительно кешируются. И для этого (внезапно) нужен доступ к приватному состоянию модуля (и как следствие — зависимость от реализации). :-D

> В требованиях лишь «экран должен открываться меньше, чем за секунду».

Не проверяйте кеш, проверяйте, что экран открывается за секунду.

В данном случае проверяется не кеш, а отсутствие его влияния на результат.

в «Ну да, по хорошему надо ещё проверить, что значения действительно кешируются»? Влияние на результат?

Вопрос в чём?

Нет. Нужна возможность замокать кеш. и проверить потом его состояние. Dependency Injection вам в помощь.

скорее вынести кэш в отдельную функцию и задекорировать. Это будет и с точки зрения читабельности лучше и с точки зрения тестируемости.


p.s. dependency injection не нужен в языках с модулями. Просто инверсия управления.

Вы предлагаете изменить интерфейс на такой исключительно ради тестов?


snakeToCamelCase( id : string , cacher : ICacher ) : string
Если говорить таким тоном — что угодно будет звучать глупостью.

Если вы пишите не библиотеку, а код испоьлзуется в 2-х местах (один раз в боевом, и 42 раза в тестах), то… Изменять интерфейс «только ради тестов» — может быть и не такая плохая идея, по сравнению с другими способами залезть в приватное состояние.

Впрочем, ваш пример переносит ответственность за _хранение_ состояния в вызывающий код.

Я не вижу принципиальной разницы между "проверяем приватное состояние" и "предоставляем публичный доступ к приватному состоянию и проверяем его", даже если мы замаскировали предоставление публичного доступа к приватному состоянию через "инъекцию состояния извне".


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

1) Вы приватное состояние, простите, рефлекшеном проверять хотите?
2) Инъекция, обычно, происходит в конструктор, а не как вы написали. Конструктор можно самому и не вызывать. Если у вас тесты зависят от внутренней реализации — вы их такими написали.
Вы приватное состояние, простите, рефлекшеном проверять хотите?

вы не путаете с java/c#/php? в node.js доступ к любому приватному состоянию модуля можно получить. Вопрос надо ли это.


Инъекция, обычно, происходит в конструктор

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

> в node.js доступ к любому приватному состоянию модуля можно получить.

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

> Просто подгружаем модуль с зависимостью, и если что — мы всегда можем переопределить какой модуль загружать.

Ну, то есть никакого «snakeToCamelCase( id: string, cacher: ICacher )» тоже нет, и интерфейс методов менять от наличия кеша тоже не надо?
Просто подгружаем модуль с зависимостью, и если что — мы всегда можем переопределить какой модуль загружать.

Фактически это service locator.


Ну, то есть никакого «snakeToCamelCase( id: string, cacher: ICacher )» тоже нет, и интерфейс методов менять от наличия кеша тоже не надо?

Но нужно знать какой модуль мокать, а это детали внутренней реализации.

> Но нужно знать какой модуль мокать, а это детали внутренней реализации.

Нет, это зависимости.

Зависимости не объявленные в интерфейсе — детали внутренней реализации.

Что вы здесь называете интерфейсом?

Объект подразумевает, что его кто-то где-то создаёт, и этот кто-то знает о зависимостях. Зависимости — это не детали внутренней реализации.

А уж где и как вы их объявляете…

Fesor, говорил про модули вида:


const cacher = require( 'cacher' )
exports.snakeToCamelCase = id=> {
    ...
}
Это ответ на что? Fesor был комментария 4 назад.
Объект подразумевает, что его кто-то где-то создаёт, и этот кто-то знает о зависимостях. Зависимости — это не детали внутренней реализации.

Вы оперируете "классическим ООП на классах". Если под "объектом" мы будем понимать любую хрень, которая инкапсулирует в себе данные и поведение, то модуль — это объект. А его интерфейс — то что он экспортирует. В этом случае "зависимостями модуля" будет орудовать сам модуль, у него есть require который по сути является сервис локатором.


И по сути зависимости модуля дело самого модуля. Inversion of control это конечно хорошо, но так или иначе вы должны объявить эти зависимости. В языках с динамической системой типов, где вы не можете явно задать типы зависимостей, вам все равно придется иметь дело с названиями модулей.


Ну то есть как ни крути, а настоящий dependency injection в nodejs не выйдет сделать, и вместо того, чтобы пытаться с этим бороться, лучше воспользоваться преимуществами того что есть.


Ну и опять же, если уж говорить о "знаниях о зависимостях в тестах", вы же когда мокаете что-то явно указываете знания о том, кто как будет этими зависимостями пользоваться. Ну то есть… я не вижу ничего плохого в том, что мы будем "подменять модуль" вместо того, чтобы "передавать другой модуль явно".


С другой стороны опять же. Я не знаю о чем дискуссия. В данной реализации "кэш" это не отдельный компонент а деталь реализации модуля. Он нужен только там, и он не влияет на окружающий мир, не привносит сайд эффектов и т.д. Я бы даже идемпотентность в этом случае тестировать бы не стал, поскольку изменения этого модуля будут полностью изолированы.


Я возможно и не прав в том, как я трактую это дело… но мне важно что изменения которые я делаю в одном модуле, не затронули другие. И тесты я пишу исключительно чтобы ответить на этот вопрос. Ну и что бы руками тупые кейсы не проверять.


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

> В этом случае «зависимостями модуля» будет орудовать сам модуль, у него есть require который по сути является сервис локатором.

Тут какая фигня… зависимость существует и до её «инверсии». Что бы из сервис локатора что-то достать — надо что бы кто-то это туда положил.

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


А реализации базирующиеся на dependency injection всеравно орудуют именами модулей, потому что подругому в javascript не выходит. Аналогичная история в python.

  1. В зависимости от языка и тестового фреймворка. В D, например, тесты можно писать внутри модуля/класса. В JS/TS приватное состояние обычно не изолируется от внешнего мира.


  2. А, так вы предлагаете ещё и одну простую функцию завернуть в класс, завязать на какой-то DI и приправить пачкой интерфейсов только ради тестов? :-)

interface IConverter {
    convert( str : string ) : string
}

interface ICacher< Value > {
    get( key : string ) : Value
    set( key : string , next : Value ) : void
    has( key : string ) : boolean
}

@DI.Provide( 'IConverter' )
class SnakeToCamelCase {

    @DI.Inject( 'ICacher' )
    cacher : ICacher

    convert( str ) {
        // ...
    }

}
1) В JS/TS приватное состояние обычно не изолируется от внешнего мира.

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

Правда, тогда не вижу и нужды интерфейс переделывать ради инъекции.

2) вы предлагаете ещё и одну простую функцию завернуть в класс

Во-первых, у вас этой «простой» функции пока не выделено. Это стоит сделать хотя бы для читабельности.
Во-вторых, новый класс — это 2 слова и 2 скобочки.

> завязать на какой-то DI

Лично у меня классы не знают про существование «какого-то» DI. Если вы берёте инструменты требующие завязки — не меня в этом винить надо )

> и приправить пачкой интерфейсов только ради тестов?

Ну, если вы хотите модно, молодёжно и с ICacher — приправляете, если нет — инжектируйте snakeTokenToUpperCaseCache «как есть».

Я бы предложил вынести кеширование за пределы знания класса SnakeToCamelCase просто по SRP, но мы не ищем легких путей.

Давайте вы лучше код приведёте, а то телепат из меня не важный.

код чего?

Код всего, что вы пока что описываете словами.

Ну да, по хорошему надо ещё проверить, что значения действительно кешируются

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


Я, впрочем, для второго пункта часто жульничаю, и проверяю не то, что выполняются метрики по производительности (потому что, будем честными, это не очень просто в юнит-тесте, выполняющемся в неизвестных условиях), а то, что ресурсоемкая операция выполняется единожды.

Нужно проверить, что кэш не имеет отрицательного влияния на работоспособность

Для этого надо ещё и реализацию без кеша сделать.


и что метод попадает в нужные границы производительности.

Это вообще ни о чём. Это не показывает правильно ли работает кеширование и работает ли вообще.


Я, впрочем, для второго пункта часто жульничаю, и проверяю, что ресурсоемкая операция выполняется единожды.

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

любой тест должен тестировать исключительно публичный интерфейс. Эта догма ничем не обоснована.

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


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

Для этого надо ещё и реализацию без кеша сделать.

Нет, не надо. Надо всего лишь проверить на соответствие требованиям (в том числе — неявным).


Это вообще ни о чём. Это не показывает правильно ли работает кеширование и работает ли вообще.

Это как раз о том, что нужно в реальности. Ну не нужно никому ваше кэширование само по себе, нужна производительность.


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

Не, это "жульничество" с той позиции, что я тестирую конкретную реализацию, при этом не проверяя соответствие реальным требованиям.

Нет, не надо. Надо всего лишь проверить на соответствие требованиям (в том числе — неявным).

Я зря цитирую что ли?


Нужно проверить, что кэш не имеет отрицательного влияния на работоспособность

Чтобы зафиксировать отрицательное влияние чего-то, нужно сравнить наличие чего-то с отсутствием чего-то. Это две реализации.




Это как раз о том, что нужно в реальности. Ну не нужно никому ваше кэширование само по себе, нужна производительность.

Мне нужно, чтобы код, который я написал, работал так, как я запланировал. Если он работает не так, то это мина замедленного действия. Задача тестов — проверка корректности логики работы, а не только лишь проверка внешнего интерфейса. Второе бы сгодилось в случае 100% покрытия, но оно невозможно, кроме совсем тривиальных случаев (тут речь не о покрытии строк кода, а о полноценном покрытии). Поэтому приходится покрывать лишь по одному случаю из каждого класса эквивалентности и убеждаться, что модуль не остаётся после этого в некорректном состоянии.


при этом не проверяя соответствие реальным требованиям.

Как я обожаю эти разглагольствования про требования в свете того, что 99% этих требований программист ставит себе сам исходя из своего представления "правильности" (как логики, так и архитектуры). Я вот себе ставлю требование "кеш должен кешировать, но не должен влиять на результат (в том числе и на интерфейс) — быть абсолютно прозрачным для потребителя". Единственный способ протестировать кеш — так или иначе получить доступ к его состоянию, а это требует знания внутренней реализации.

Я зря цитирую что ли?

Зря. Потому что "отрицательное влияние" того же кэша — это всего лишь несоответствие требованиям. Поэтому, если есть тесты на требования, сравнивать с референсом не надо. Есть (я могу себе представить) только один кейс, когда полезно сравнивать с референсом — это когда есть несколько разных реализаций, и мы хотим перформанс сравнивать (например, чтобы понять, что кэширование в реальности приносит пользу, а не ухудшает).


Мне нужно, чтобы код, который я написал, работал так, как я запланировал.

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


Я вот себе ставлю требование "кеш должен кешировать, но не должен влиять на результат (в том числе и на интерфейс) — быть абсолютно прозрачным для потребителя".

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


Единственный способ протестировать кеш — так или иначе получить доступ к его состоянию, а это требует знания внутренней реализации.

Ну, вот это точно неверно. Я уже упоминал как минимум еще один способ тестировать кэширование.

Единственный способ протестировать кеш — так или иначе получить доступ к его состоянию, а это требует знания внутренней реализации.

Разделяйте задачи тестирования кеша, модуля его использующего и их интеграции. Если кеш полностью инкапсулирован в модуль, то тестировать (речь про юнит-тесты) нужно только модуль, наличие у него сокрытого кеша никак на тесты влиять не должно, ведь «кеш должен… быть абсолютно прозрачным для потребителя», а юнит-тест такой же потребитель как основное приложение. Для юнит-теста наличие или отсутствие внутреннего кеша должно быть прозрачным. Если юнит-тест сможет получить доступ к кешу, значит и потребитель сможет.
1) Не вижу проблемы с «Даже если я вдруг решу „поменять“ способ, которым это реализовано от слова совсем.». Плохой пример, надо более лучше знать о внутренностях.
2) Идемпотентность — это контракт.

Поздравляю, вы профгодны.
  1. А должна быть проблема?


  2. Вы проверяете идемпотентность каждой функции? Вот только честно.


  3. Но как же так, я же использовал знание внутренностей, для обнаружения условий возможного нарушения контракта.
1) Должна быть, иначе нет знания о внутренностях. Ну, по определению из https://habrahabr.ru/post/314994/?reply_to=9907672#comment_9907552

2) Я вообще тесты пишу только если Care-o-meter шевелится. Идемпотентность там не единственное исключение.

3) Вы такой тест и в TDD могли написать. Ну и см. п.1.
  1. Это не мои слова. Я сагрился лишь на ваш комментарий :-)


  2. Вот о том и речь. Наличие изменяемого состояния — сигнал к тому, что надо проверить идемпонетность.


  3. Так я любой тест могу написать с любой методологией. Но для чёрного ящика я этот тест скорее всего не напишу, ибо если буду проверять идемпотентность каждой функции, то у меня будет куча "тестов ради тестов". Куда рациональней, проверять идемпотентность лишь тех функций, которые зависят от изменяемого состояния.
1) Ну, если вы не читаете тред даже на коммент выше… В общем, это ваша проблема.

2, 3) Наличие требования о кеше — тоже сигнал к тому, что надо проверить идемпонетность. А требование у вас есть, иначе кеша бы не было

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

Вы проверяете идемпотентность каждой функции? Вот только честно.

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


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

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


Иначе как-то странно выходит. Мы должны учитывать одни знания о реализации и игнорировать те, которые намекают что эти знания не существенны.

Идемпотентные функции проверять легко.

Ага, только увеличивает число тестов вдвое.


И можно просто пользоваться всякими фэйкерами чтобы увеличить количество тест кейсов без необходимости увеличивать количество тестов.

Что за самцы феек?


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

Давайте без телепатии. Запустите тест и убедитесь, что он падает, если не верите мне на слово.


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

Ну или внимательнее посмотрите на реализацию.

давайте по другому. Когда у разработчика есть какие-то убеждения, которые он считает абсолютными — он профнепригоден. В общем, аля "TDD никогда не работает" или "TDD работает всегда". "Только черный ящик", "только белый ящик"… "Только ООП", "только функциональное программирование"....

только белый ящик

А наука — это вера в отсутствие бога. ;-)

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


И если мне, как разработчику, после того как я что-то отрефакторил для удобства приходится переписывать десяток тест кейсов, которые на реализацию были завязаны — я трачу дополнительное время. И тесты становятся дорогими.


У многих потом просто возникает желание удалить неработающий тест кейс… и многие так и делают. А толку тогда в таких тестах?

Во мне есть уверенность, что надо делить тесты на тесты из требований, и «случайные» тесты. У них разных жизненный цикл. Первые удалять нельзя (без согласования с заказчиком). Но в требованиях написано 10%, остальное приходится додумывать, и писать на придуманное тесты.

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

Что вы называете техническим требованием не от клиента?

От клиента есть требование, что экран должен открываться за секунду. Больное воображение разработчика родило кеш.

Тут любой тест на кеш — «излишне привязан к реализации». Но проблема то не в этом. Точнее, тут нет решения.

Требование заказчика — объективно, его мы проверять хотим. А вот кеш — решение субъективное, это не «тоже требование». Да что уж там, костыль. Если мы этот костыль и хотим защитить от регрессии, то как-то иначе, чем если нам заказчик написал, что это должен быть «кеш LRU размером 1000».

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


В функциональном программирования результат у нас только один — то, что вернула функция. В императивном программировании часто забывают, что производимые функцией побочные действия — это тоже результат её работы. Только он, в отличие от возвращаемого значения — не явный. Так вот, неявный результат тоже надо проверять.

> Тесты должны покрывать всю логику

Кому должны?

> Так вот, неявный результат тоже надо проверять.

Если надо — проверяйте.

У меня программа, например, логи пишет, на них тестов нет.

Да, и у вас какая-то мешанина: императивное программирование не обязано приводить к побочным эффектам (а функциональное, на практике, может к ним приводить).
Кому должны?

Тому, кто хочет быть уверен в корректности работы программы.


У меня программа, например, логи пишет, на них тестов нет.

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


императивное программирование не обязано приводить к побочным эффектам

А где я говорил, что оно должно?


а функциональное, на практике, может к ним приводить

функция в ФП не может. По определению. А вот монада — может. Но монада — не функция.

> А если вам плевать пишутся логи корректно или нет, то скорее всего вам логи и не нужны.

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

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

А потом окажется, что в лог вместо айпишника выводится пароль пользователя в открытом виде. Или приложение падает из-за того, что место на диске закончилось, так как в логи сваливался дамп всех файлов, что посылал пользователь.

И вы, конечно, предлагаете написать на это явные тесты, несмотря на то, что на это логирование нет никаких требований, а оно написано "на всякий случай".

Всегда есть неявные требования. Например: в приложении не должно быть бэкдоров, оно не должно втихоря майнить биткоины, оно не должно быть подвержено xss. И отсутствие явного указания на это в ТЗ вас нисколько не спасёт от укоризны.

И вы, конечно, предлагаете написать на все эти неявные требования явные тесты?

Я предлагаю не притворяться будто этих требований нет.

А не важно, в общем-то, есть они или нет — я спрашиваю, предлагаете ли вы написать на них явные тесты.

Разумеется. Если соответствующие проблемы не исключены архитектурно.

Как вы видите тест на то, что в приложении нет бэкдоров или оно не майнит биткоины?

Если вы это не реализовывали, то и тесты на это не нужны ;-)

А если реализовывал по собственной инициативе и хотел бы скрыть от других? :)

Значит тесты тоже стоит скрыть от других.

Извините, но вы только что опровергли собственный же тезис, что надо писать на это явные тесты.

От себя-то их скрывать не надо.

Теперь посмотрите, на какой именно комментарий я отвечаю, и задумайтесь, при чем тут вообще сокрытие.

Все претензии к Хабру.


Если вы используете архитектуру, где XSS может возникнуть сам по себе из-за невнимательности, то надо написать на это тесты. Если вы бэкдор не реализовывали, то ему взяться не откуда (разве что вы используете архитектуру, где бэкдор может возникнуть сам по себе), а значит тесты не нужны.

Ага, то есть если я запись файлов пользователя в логировании не реализовывал, или пароли не пишу, то тестов на это тоже не надо, правильно?

И если написаны тесты на то что именно выводится. А то мало ли что в какой-то переменной вдруг окажется.

Выводится то, что передали, с логгера взятки гладки.

А при чём тут логгер?

Logger: one that logs.

Spoon: one that eats.

казалось бы, причем тут TDD раз уж речь уже не о юнит тестах и о тестировании нефункциональных требований?

Вполне себе функциональные. Да и в TDD можно не только юнит тесты применять.

Вполне себе функциональные.

Это с точки зрения разработчика, с точки зрения пользователя это нефункциональные требования. Защита от XSS и другого рода атак, производительность, все это является нефункциональными требованиями. Их отдельно описывают и отдельно тестируют.


Вы можете разрабатывать библиотеки, которые помогут вам добиться этих нефункциональных требований, и с точки зрения библиотеки то, что они делают будут уже функциональными требованиями (фильтрация XSS например, или кэш, или балансировка какая-либо). Но в целом это далеко выходит за рамки юнит тестов, обычно используется в контексте e2e/приемочных тестов. А это уже совсем другая история.


Да и в TDD можно не только юнит тесты применять.

и называется это уже ATDD/BDD.


p.s. я просто уже не вижу нити дискуссии. То что я вижу — тесты ради тестов.

с точки зрения пользователя это нефункциональные требования.

https://ru.wikipedia.org/wiki/Требования_к_программному_обеспечению#.D0.92.D0.B8.D0.B4.D1.8B_.D1.82.D1.80.D0.B5.D0.B1.D0.BE.D0.B2.D0.B0.D0.BD.D0.B8.D0.B9_.D0.BF.D0.BE_.D1.83.D1.80.D0.BE.D0.B2.D0.BD.D1.8F.D0.BC


Защита от XSS

Реализуется архитектурно. Или необходимо тестирование каждого вывода данных пользователя (экранирование спецсимволов и тп)


производительность,

Логика работы кеша — вполне себе функциональные требование. Даёт ли он положительный прирост производительности — другой вопрос.


Вы можете разрабатывать библиотеки
Но в целом это далеко выходит за рамки юнит тестов

Библиотекам юнит тесты не нужны?


и называется это уже ATDD/BDD.

Это подвиды TDD, наряду с UTDD.


я просто уже не вижу нити дискуссии. То что я вижу — тесты ради тестов.

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


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

Если нет требования «пароли не должны долговременно храниться где-либо в открытом виде в принципе», то, в принципе, ничего страшного с точки зрения поставленных задач. А следить за местом на диске и(или) настраивать ротацию логов по идее вообще не разработчик должен. В любом случае юнит-тестом нельзя проверить, что программа или её часть не производит каких-то сайд-эффектов. Можно убедиться, что производит определённые, можно даже убедиться, что не производит определённые, но нельзя убедиться, что не производит неопределённые.
и называется это уже ATDD/BDD.

Это подвиды TDD, наряду с UTDD.

Все же BDD — это не подвид TDD. Это скорее дополнительный уровень абстракции для того, чтобы тесты были человеко-понятными. А вот если мы сочетаем BDD и TDD — тогда и получается ATDD.
А можно пример критических точек, которые делают более выгодным тестирование белого ящика?

Видимо мой комментарий с примером не долетел до интернета..


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


Сможете ли вы учесть все кейсы вслепую, имея только интерфейс, не зная какие именно и в каком порядке вычисления должны быть произведены?

Попробую предположить:
— остроугольный
— прямоугольный
— вырожденный (C=A+B)
— невозможный (C^2!=A^2+B^2)

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

Сам я довольно нуб и использовать тестирование начал не так давно, опыта мало. Случаются ситуации, когда я написал тесты метода, а при реализации спокойно добавил обработку ситуаций, которые не учитывал в тесте (еще один не самый удачный пример: проверка на null).
невозможный (C^2!=A^2+B^2)
в каком смысле невозможный?
Я сморозил глупость с формулой, это касается только прямоугольных треугольников.

Но все равно можно взять такие длины отрезков, что получить треугольник будет нельзя (например, 10, 1, 1)
Заголовок спойлера
image


> (C^2!=A^2+B^2)
Может быть С > A + B?

Бонус:
Данные могут быть некорректными (A, B или C < 0)
Данные могут быть «достаточно большими», что может вызвать переполнение (а могут и не вызвать, зависит от реализации)

Вы забыли тупоугольный, вырожденный с одним нулевым ребром, вырожденный в точку. А вместо "невозможного" вы описали "с углом ABC не равным полуПи" :-) Невозможный — это C>A+B


Кроме того, надо проверить всё это с отрицательными значениями. И со значениями близкими к максимальным для заданного типа и минимально близким к 0. Ещё есть значения "бесконечность" и "не число", на которые тоже надо адекватно реагировать.

Тупоугольный хотел написать, сам удивлся. Про «невозможный» выше написал, что ошибся.

По остальному — согласен.
А тестирование белого ящика эффективней, так как программист знает все внутренние критические точки. TDD же провоцирует тестировать чёрный ящик, когда реализации ещё нет и соответственно ты не знаешь при каких значениях у тебя может всё сломаться.

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

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

Третье, я не вижу вообще никакой причины использовать только чёрный или белый ящик, только TDD или TLD. Почему бы не написать тесты в начале, до того как вы знаете что внутри, а потом — после написания кода, обеспечивая покрытие?
В дополнение — пару раз были случаи на работе — когда белый ящик проходил на отлично, а на черном ящике — оказывалось, что функционал работает неправильно. Потом разработчикам приходилось разбираться, что же за магия творится. Так что все верно — и белый и черный ящик нужны и желательно вместе.
Вообще говоря, необходимость в белом ящике зачастую может свидетельствовать об изъянах проектирования или недостаточно тщательном рефакторинге (который, напомню, является обязательным третьим шагом в TDD). Т.е. если вам понадобилось знание нюансов реализации, возможно не все в порядке в датском королевстве, и метод/класс берет на себя больше, чем должен.
  1. Тестировать чёрный ящик может только телепат. Вот вам ещё пример — реализуете вы алгоритм сортировки. Тестируя чёрный ящик, вы возьмёте: пустой массив, отсортированный массив из 2 элементов, обратносортированный массив из 2 элементов, может ещё добавите сортировку массива из 10 элементов, поиграетесь с отрицательными числами, с дубликатами. Тесты написаны, реализация готова, запускаем в продакшен, продакшен падает, когда число элементов становится нечётным больше 2. А всё потому, что вы реализовали quicksort, который активно использует половинное деление, но округляете не в ту сторону. Чтобы узнать, что входные параметры делятся на два класса (с чётным и нечётным числом элементов больше 2), вам нужно быть в курсе внутренней реализации, а именно наличия деления числа элементов на 2.


  2. Я вам привёл пример, как знание внутренней кухни позволяет учесть больше вариантов. А вот когда тесты уже написаны, а реализация их проходит, то и появляется соблазн больше тестов не писать, чтобы никто не заподозрил тебя в белом ящецизме. :-)


  3. Вот и получается, что в случае, когда ты ещё не знаешь точно какой у тебя будет интерфейс (а это большинство случаев), все сколь-нибудь полезные тесты будут написаны уже после реализации ;-)
1) А спецификации на что? Если я знаю спецификацию, и если она создана качественно( а я непременно обращусь с вопросами к аналитику, если увижу в ней неоднозначности ), то я напишу тесты согласно этой спецификации, и их внутренняя реализация меня не будет волновать, какая разница для заказчика, как реализовано, если удовлетворяет требованиям?
2) Чтобы не впасть в такой соблазн — нужно просто помнить о «парадоксе пестицида»
3) Опять же — это если нет спецификации

1) Каким образом вам тут поможет спецификация? Или вы ожидаете детализированное описание алгоритма сортировки? А программист в этой цепочке тогда зачем? Скармливаем "качественно созданную спецификацию" парсеру спецификаций и на выходе получаем готовую программу. В былые времена такие парсеры спецификаций называли компиляторами. Спецификации — исходными кодами. А аналитиков, которые их писали — программистами. Как сейчас — не знаю, видимо я отстал от жизни. :-)


3) Вы точно не робот? Я не видел ещё ни одной спецификации с таким уровнём детализации.

>Каким образом вам тут поможет спецификация?
Ну как бы спецификация описывает то, что пользователь ожидает то программы. При тестировании — проверяется соответствие требованиям. Если программа соответствует требованиям — значит можно релизиться. Нюансы реализации могут интересовать или разработчиков или архитектора, но зачем он заказчику?
Я не понимаю, зачем вы утрируете? Или вы пишите ПО без ТЗ? Мне если честно сложновато в это поверить.

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


А так, ТЗ у меня выглядит так: заказчик хочет приложение с примерно таким функционалом, проверь реализуемость, напиши "проектное решение", согласуй с заказчиком, реализуй, а потом пройди опытную эксплуатацию. При это если в результате всё работает не так как ожидает пользователь, то это надо исправить. Даже если в "проектном решении" написано по другому — значит неправильно его составил и нужно исправлять его тоже и пересогласовывать.

Что-то не могу представить спецификацию ПО с квантором всеобщности.

Я прочитал ваш процесс разработки, и возник вопрос — то есть задачи действительно согласовываете на уровне «хочу примерно это»? Тогда вы получается делаете двойную работу. Получается может быть так — вы сделали, заказчик передумал какую-то фичу, и вы за те же деньги ее переделываете, я правильно понял? Просто в случае согласования заранее, перед началом работ — у вас будет что предъявить заказчику, и в случае, если он захочет что-то поменять — это будет уже новая работа.

"Щёлчок по любому элементу списка должен приводить к открытию инфо панели с подробной информацией по этому элементу" и тд.


Всё сложно. Бывает важные вещи согласуются месяцами, а сроки двигать никто не хочет. И приходится реализовывать несколько вариантов или же самый вероятный с прицелом, что придётся всё переиграть. Этакий Корпоративный Эджайл :-D

«Щёлчок по любому элементу списка должен приводить к открытию инфо панели с подробной информацией по этому элементу» и тд.


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

Всё сложно. Бывает важные вещи согласуются месяцами, а сроки двигать никто не хочет. И приходится реализовывать несколько вариантов или же самый вероятный с прицелом, что придётся всё переиграть.

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

У вас в спецификациях описываются классы эквивалентности для каждой функции?


Это особенность крупных заказчиков. Чем курпнее организация, тем больше там бардака.

У вас в спецификациях описываются классы эквивалентности для каждой функции?

В спецификации ПО — нет, в спецификации тестирования — уже может иметь смысл. По проще всего выделить классы эквивалентности в момент написания тесткейсов.

Это особенность крупных заказчиков. Чем курпнее организация, тем больше там бардака.


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

И вы дальше описываете, очевидно, действия телепата, верно? Про кучу тестовых кейсов, хоть и не полную?
Тесты написаны, реализация готова, запускаем в продакшен
Без интеграционных тестов? Без тестирования вручную? Да, похоже на работу телепата.

В итоге, вы ограничили себя только одним методом поиска ошибок, за что и поплатились. Я даже не хочу рассыпаться рассуждениями о том, что после изменения алгоритма, вам, судя по всему, придётся переписывать тесты.
Я вам привёл пример, как знание внутренней кухни позволяет учесть больше вариантов.
А может и меньше. В вашем примере вы могли учесть только два своих «класса» данных — чётный и нечётный, и забить на, например, совпадения.
Вот и получается, что в случае, когда ты ещё не знаешь точно какой у тебя будет интерфейс (а это большинство случаев)
В смысле — интерфейс? У вас функция сортировки какой-то необычный интерфейс имеет, который изменился во время реализации?
Без интеграционных тестов? Без тестирования вручную? Да, похоже на работу телепата.

Всё это такая же стрельба вслепую. Может попадут, а может и нет. Белый ящик позволяет (но не ограничивает) бить прицельно по конкретному классу ошибок.


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

То, что ошибка пролезла через все стадии тестирования не говорит о том, что этих других стадий не было. Реальная ошибка может проявляться лишь на классе "число задач на сегодня кратное 256" и вы будете очень долго ловить этот баг в "чёрном ящике" по багрепортам вида "иногда не показывается последняя задача, не понятно при каких обстоятельствах".


Я даже не хочу рассыпаться рассуждениями о том, что после изменения алгоритма, вам, судя по всему, придётся переписывать тесты.

Именно так. У нового алгоритма будут другие слабые места и неплохо было бы шмальнуть по ним для профилактики.


В вашем примере вы могли учесть ТОЛЬКО два своих «класса» данных — чётный и нечётный, и забить на, например, совпадения.

Забить на совпадения я могу независимо от цвета ящика.


В смысле — интерфейс? У вас функция сортировки какой-то необычный интерфейс имеет, который изменился во время реализации?

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

Ну отлично. Давайте из одной в крайности — другую. Но начнём с другого, товарищ удобный интерфейс. Потому что то, что вы описали не совсем-то интерфейс. И хотя возможность задавать компаратор и является интерфейсом, товарищ, любая сортировка используя механизм сравнения пар по умолчанию должна иметь некий компаратор. Пускай он будет стандартный, пускай он будет задаваться — это не то, чтобы интерфейс, это здравый смысл.
Продолжим тем, что когда любому методу задаётся на вход контейнер, где-то сразу в тесте после подачи нулей и нулов проверяется работа с чётным-нечётным. Это такая галочка есть у опытных писателей тестов.
Было бы куда более убедительно, если бы вы взяли, например, некий специфичный процессор, у которого, например, 7 битовые слова с каким-нибудь особенным управляющим сигналом. Чёрт его знает, что это за зверь, но не имея представления о реализации сигнала или о доступных машинных словах — довольно тяжело делать тесты. Но признаемся сами себе — это, как говорят американцы, булшит. Не представляю, кто доверил бы написание тестов такому специфичному процессору не удостоверившись в вашей компетенции хотя бы по чтению даташитов.
И, наконец, покуда вы знаете узкие места вашей реализации, в чём проблема включить грёбанный котелок и громко вслух проговорить узкие места алгоритма? Совесть за слух окружающих скорбит? Гордость за чувства окружающих теребит? Или банальное неумение читать собственный код? Ну, тут уж кто во что горазд, товарищ универсальная реализация.
то, что вы описали не совсем-то интерфейс

Да нет, я описывал исключительно интерфейсы:


int[] sort( int[] ints );

Item[] sort( Item )( Item[] numbers ) if( isNumeric!Item );

Item[] sort( Item )( Item[] items ) if( is( typeof( Item.init > Item.init ) ) );

Item[] sort( Item )( Item[] items , int delegate( Item a , Item b ) comparator ) if( is( typeof( Item.init > Item.init ) ) );

Item[] sort( Item , alias comparator = q{ a > b } )( Item[] items ) if( is( typeof( Item.init > Item.init ) ) );

ElementType!Range[] sort( Range , alias comparator = q{ a > b } )( Range range ) pure if( isInputRange!Range && is( typeof( ElementType!Range.init > ElementType!Range.init ) ) )

когда любому методу задаётся на вход контейнер, где-то сразу в тесте после подачи нулей и нулов проверяется работа с чётным-нечётным.

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


Дальнейший бессвязный поток желчи оставлю без комментариев.

У этого подхода один минус: придумывая API этой части из головы велика вероятность сделать его неудобным. Но неудобство это вскроется только на практике: что вот тут не хватает параметра, этот метод вообще лишний, а чтобы сделать такой вот простейший сценарий использования приходится извращаться.
TDD в какой-то степени от этого защищает, так как вы придумываете апи одновременно с написанием кода, который это апи использует. И такие косяки всплывают на самом раннем этапе, когда исправить их просто. А не когда написано уже пара тысяч строк кода, которые придется переделывать.

Обычно "эту часть" пишут не просто так, а когда в ней появилась потребность. И исходя из этой потребности программист уже представляет, как бы ему было удобно её использовать. Типичный кейс — программист пилит модуль, одновременно прикручивает его использование в программе, потом запускает программу и убеждается, что всё работает как надо. Если это модуль для работы со внешним апи (например, картографический сервис), то это единственный способ проверить, что всё действительно работает. Далее уже можно прикрутить тесты, замочив внешний сервис, чтобы не приходилось каждый раз запускать приложение. TDD же как раз и провоцирует написание модулей в отрыве от реальности. А реальность не идеальна. В реальности нужны костыли, оптимизации, дополнительные параметры и даже целые сущности, о которых ты даже не догадывался, пока не взялся за реализацию.

НЛО прилетело и опубликовало эту надпись здесь

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


Так много всего возможно. И всё так неоднозначно. И столько всего нужно учитывать, принимая решение о рефакторинге. Более важного, чем догмат "тестирование приватного метода — зло".

НЛО прилетело и опубликовало эту надпись здесь
А у хорошо спроектированного и реализованного решения разве должны быть скрытые, критические точки, о которых знает только программист?
Не должны, но могут. В задаче, например, поведение в критических точках не описано и тесты:
а) фиксируют конкретное поведение в неопределенной ситуации
б) документируют его для других программистов, включая себя будущего
Поведение в критических точках и не должно быть описано, они просто должны быть объявлены таковыми, а тесты проверят поведение, и не важно, написаны они до или после реализации.

Ничто не работает, если используется игнорируя сигналы мозга.
TDD, BDD, DDD, и всё всё всё остальное — есть инструменты. Однако в последние годы у программистов появилась чёткая тенденция всё воспринимать как серебряную пулю. Доходит до абсурдных ситуаций. Но не буду вдаваться в программирование.
Попробуйте построить дом только молотком. Или только отвёрткой. Или только пилой. Выкиньте все остальные инструменты — оставьте только болгарку! Провозгласите всех, пользующихся наждачной бумагой — пережитком прошлого. Вы видите каменщика? Объясните ему, что его подход устарел, а уже через полгода он не найдёт работу. Будьте тру-айтишником в строительстве.
"Ваши методы фуфло!" (ваши методы по версии тех, кто пользуется другими методами.


Перестаньте искать серебряные пули. Просто беритесь и делайте. Уместные инструменты вам в помощь.

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

Вы правы — я гиперболизирую. Но именно для того, чтобы донести мысль. Представьте себе, что сегодня выпустили новый, великолепный инструмент, полностью заменяющий молоток — забивает идеально, не бьет по пальцам, работает быстрее быстрого. И представьте, что строители поступят типично по айтишному — мало того, что начнут повсеместно выкидывать молотки ([irony]ведь у всех есть время и деньги на внедрение нового инструмента[/irony]), но и начнут сносить все строения, построенные молотком — с целью сейчас же перестроить их с помощью нового инструмента. Глупо будет, не так ли?


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


Моя мысль проста: не выкидывайте старое. Изучайте новое — и объединяйте. Слепая замена — стоит очень дорого. И каждому инструменту — своё место.


Не идите на поводу у статей. Прочитав статью подумайте: прав автор или ошибается.

Ну если бы IT-компаниями управляли программисты, то может так оно и было, но вот не такое уж и частое явление, что руководство компании готово выделить ресурсы на «актуализацию» технологий. Например, на моей текущей работе уже достаточно долго никак не могу перейти с SVN на Git, не говоря уже о многих других технологиях, которые все еще используются, хотя потеряли свою актуальность уже не один год назад, из-за чего поддержка и развитие проекта обходится куда дороже, чем могла бы… Ну а вообще не спорю с Вами, что нужно выбирать подходящий инструмент под задачу, а не решать задачу каким либо инструментом только потому, что он новый и да, нужно находить возможность вводить более актуальные технологии параллельно с уже имеющиеся кодовой базой, не задаваясь целью просто взять и переписать все наработки.
Такие исследования генерируют холивары.
Думаю, практически нет разницы когда физически писать тесты, если продумываешь их перед написанием кода. Ну то есть в одних ситуациях лучше бы было до, в других после, а в целом то на то и выходит. Но TDD обязывает продумывать тесты перед написанием кода, а TLD — нет.
>Тесты показали как минимум то, что не нужно обязательно писать тесты вначале – как минимум если работа проходить короткими циклами.

Об этом я и всегда пишу.
Но TDD-проповедники в каких-то книжках читали, что тесты нужно писать перед кодом. :) И непонятно какой размер итерации. Обычно просто говориться, что тесты перед кодом.

Если это использовать как культ карго — то выходит ерунда. :)
Впрочем, как и везде. :)
А разве сама структура тест кейса не определяет размерность итерации?
Каким же образом?
Где это явно описано? :)
Ну конечно, есть паттерны тестирования, которые описывают размерность конкретных тест-кейсов, а так же ограничивают функциональность, которую они охватывают.
О, и тут есть паттерны. :)
Конкретно на хабре можете привести ссылки, где говорится о размере итерации.
На хабре не встречал статей на эту тему, могу порекомендовать книгу: «Шаблоны тестирования xUnit. Рефакторинг кода тестов» Джерард Месарош.
мне кажется что без разницы, что писать сначала — код или тесты — главное чтоб выбранная методология повышала эффективность и стабильность конечного продукта
Ну вообще — чем раньше начинается тестирование(и соответственно находятся дефекты), тем ниже цена исправления ошибки, в этом плане TDD просто старается минимизировать её.

Requirements Review и Design Review в этом плане гораздо выгоднее, но что-то не видно вокруг них такого хайпа.

Выводы неверны… юнит тесты надо дополнять интеграционными, это да, но интеграционные тесты не замена юнит тестам.


Be humble about what tests can achieve. Tests don’t improve quality: developers do.

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


В целом по теме юнит тестов я обычно рекомендую посмотреть сие видео: The Magic Tricks of Testing by Sandi Metz

НЛО прилетело и опубликовало эту надпись здесь
Можете пояснить, что такое TBD — я несколько вариантов нашел, но думаю, что они неверные…
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
TDD убивает творческую составляющую разработки, это все что нужно о нем знать.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Изменить настройки темы

Истории