Написание спецификаций — утверждения или тестовые гипотезы — обычно обходят стороной в курсах TDD, поскольку тут мало программирования.
Однако они очень важны.
Они определяют, как и какие тесты будут написаны, а также определяют, насколько легко будет фиксировать поломку. Они почти не требуют времени для имплементации. Они — первое, что связывает юзер стори и код и первое, что показывает несостоятельность тестового кода. Они так же — то, что дополнительно охраняет нас от ошибок в коде самого теста.
Каким образом мы могли бы сделать наши утверждения лучше?
Во-первых, спецификации должны соотноситься с юзер стори в терминах. Если юзер стори пользуется термином «залогиниться», то ассерты не могут менять этот термин на authenticate/authorize/validate. Если стори написана неудачно, поменяйте ее.
Во-вторых, спецификация должны в явном виде содержать название компонента, который они описывают. Это обозначает область тестирования и сигнализирует об ошибке, если вдруг часто скоп включает в себя какие-то неожиданные компоненты.
Валидный сценарий хорошо сочетается с принципом Single responsibility, поэтому название компонента должно соответствовать описанию теста. Если валидных сценариев много, то, вероятно, у компонента слишком много ответственности.
Так же — один сценарий, как правило, описывает либо ui-событие, либо какой-то паттерн. Хорошие кандидаты на название компонента могут содержать название паттерна: Validator, Strategy, Builder, Transformer, Controller и тп. Или название события: Submitter, Login, ReadonlyOrderView и тп. Т.е. в зависимости от названия компонента должны быть определены и входные и выходные значения главной функции класса. Плохие названия: слишком абстрактные (Service,Component,Helper,Utility) и двойные (ValidatorAndRenderer).
В-третьих, спецификации должны в явном виде указывать условия.
Плохо:
Лучше:
Нам не придется вызывать эту функцию из другого места, поэтому сверхдлинные имена допустимы. Если же количество условий в тесте не помещается в строку, то, возможно, следует поменять структуру компонента.
Для javascript-тестов то же самое, но там можно вкладывать условия, получается удобнее.
Явно выраженные условия легче читать и находить пропущенные.
Кроме того, если условие не совпадает с написанным тестовым кодом, то код легко поправить.
Хороший describe — это, в общем, замена документации и комментированию кода. Он абстрактен, он не зависит от фреймворка и компилятора, и к нему следует относиться с той же серьезностью, как и к двум последним.
Сами условия должны быть консистентны в коде для легкого чтения, например GIVEN-WHEN-THEN-SHOULD и тп.
В-четвертых, спецификации следует упорядочить. Полезно написать их в обратном порядке:
Errors first, nulls first, happy path last. Тогда не возникает желания закончить тестирование после одного валидного юз-кейса. Для UI-компонентов: рендеринг, события. Для stateful компонентов — последовательно все состояния.
Таким образом, структура хорошей читаемой спецификации выглядит так: 5-10 ошибочных, один валидный сценарий (или несколько вариаций одного сценария)
Хорошей практикой при написании спецификации является написать ее полностью в виде текста, после чего дать прочитать коллеге на предмет понятности и пропущенных условий. Потом, собственно, можно уже приступить и к написанию имплементации. Довольно часто уже в этом виде задача является отчуждаемой, т.е. хорошо, когда имплементацию тестов напишет кто-то другой.
В-пятых, имплементация теста должна соответствовать тому, что написано в describe.
Хорошей практикой является разделение на arrange-act-assert, чтобы легко находить соответствие той или иной части имплементации. Текст ассертов и исключений, если таковой присутствует, должен быть тоже соотнесен с описаниями тестов, как правило, мы можем просто дублировать их.
Излишне говорить, что при добавлении условий или ассертов к тесту нужно так же апдейтить описания тестов.
Итак, последовательность написания спецификации может быть следующей.
Однако они очень важны.
Они определяют, как и какие тесты будут написаны, а также определяют, насколько легко будет фиксировать поломку. Они почти не требуют времени для имплементации. Они — первое, что связывает юзер стори и код и первое, что показывает несостоятельность тестового кода. Они так же — то, что дополнительно охраняет нас от ошибок в коде самого теста.
Каким образом мы могли бы сделать наши утверждения лучше?
Во-первых, спецификации должны соотноситься с юзер стори в терминах. Если юзер стори пользуется термином «залогиниться», то ассерты не могут менять этот термин на authenticate/authorize/validate. Если стори написана неудачно, поменяйте ее.
Во-вторых, спецификация должны в явном виде содержать название компонента, который они описывают. Это обозначает область тестирования и сигнализирует об ошибке, если вдруг часто скоп включает в себя какие-то неожиданные компоненты.
Валидный сценарий хорошо сочетается с принципом Single responsibility, поэтому название компонента должно соответствовать описанию теста. Если валидных сценариев много, то, вероятно, у компонента слишком много ответственности.
Так же — один сценарий, как правило, описывает либо ui-событие, либо какой-то паттерн. Хорошие кандидаты на название компонента могут содержать название паттерна: Validator, Strategy, Builder, Transformer, Controller и тп. Или название события: Submitter, Login, ReadonlyOrderView и тп. Т.е. в зависимости от названия компонента должны быть определены и входные и выходные значения главной функции класса. Плохие названия: слишком абстрактные (Service,Component,Helper,Utility) и двойные (ValidatorAndRenderer).
В-третьих, спецификации должны в явном виде указывать условия.
Плохо:
@Test public void testValidatePassword(){}
Лучше:
@Test public void loginController_whenValidUsername_andValidPassword_shouldLogUserIn(){}
Нам не придется вызывать эту функцию из другого места, поэтому сверхдлинные имена допустимы. Если же количество условий в тесте не помещается в строку, то, возможно, следует поменять структуру компонента.
Для javascript-тестов то же самое, но там можно вкладывать условия, получается удобнее.
describe('login UI component', () => {
describe('when username provided', () => {
describe('when valid password', () => {
it('should log user in', () => {
...
});
});
});
});
Явно выраженные условия легче читать и находить пропущенные.
Кроме того, если условие не совпадает с написанным тестовым кодом, то код легко поправить.
Хороший describe — это, в общем, замена документации и комментированию кода. Он абстрактен, он не зависит от фреймворка и компилятора, и к нему следует относиться с той же серьезностью, как и к двум последним.
Сами условия должны быть консистентны в коде для легкого чтения, например GIVEN-WHEN-THEN-SHOULD и тп.
В-четвертых, спецификации следует упорядочить. Полезно написать их в обратном порядке:
Errors first, nulls first, happy path last. Тогда не возникает желания закончить тестирование после одного валидного юз-кейса. Для UI-компонентов: рендеринг, события. Для stateful компонентов — последовательно все состояния.
Таким образом, структура хорошей читаемой спецификации выглядит так: 5-10 ошибочных, один валидный сценарий (или несколько вариаций одного сценария)
Хорошей практикой при написании спецификации является написать ее полностью в виде текста, после чего дать прочитать коллеге на предмет понятности и пропущенных условий. Потом, собственно, можно уже приступить и к написанию имплементации. Довольно часто уже в этом виде задача является отчуждаемой, т.е. хорошо, когда имплементацию тестов напишет кто-то другой.
В-пятых, имплементация теста должна соответствовать тому, что написано в describe.
Хорошей практикой является разделение на arrange-act-assert, чтобы легко находить соответствие той или иной части имплементации. Текст ассертов и исключений, если таковой присутствует, должен быть тоже соотнесен с описаниями тестов, как правило, мы можем просто дублировать их.
Излишне говорить, что при добавлении условий или ассертов к тесту нужно так же апдейтить описания тестов.
Итак, последовательность написания спецификации может быть следующей.
- прочитать юзер стори
- назвать компонент и валидный сценарий
- сформулировать условия для валидного сценария
- дополнить спецификацию ошибочными сценариями, поместив их сверху
- дать прочитать соседу и поправить пропущенное
- имплементировать тесты и компонент один за другим, используя red-green-refactor цикл.