Статьи

Тестируемость! = Хороший Дизайн

Забавно, тестируемость. Это не совсем определено, или, скорее, плохо определено. 

Если тестируемый код — это код, который мы можем протестировать, это означает, что весь код такой. Мы можем проверить это с помощью модульных тестов. Если это сложно, мы можем перейти к функциональным тестам. И если все не удается, мы можем сделать ручное тестирование. Даже тестирование производительности выполняет код. Может быть код, который тесты не могут выполнять, но тогда зачем мы его написали?

Когда мы говорим о тестируемости, мы обычно имеем в виду «трудно тестировать». Это целое обсуждение само по себе, потому что «трудно проверить» также субъективно. Если мы следуем теме тестирования как инвестиции, чтобы минимизировать будущие затраты на обслуживание, то «трудно проверить» переводится как «Дорого проверять» или «рискованно проверять».

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

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

Вот пример. У нас есть класс Customer со статическим методом, который получает остаток на счете от банка:

public bool isOverdrawn(String name, int limit) {
        return (Bank.getAccount(name).getBalance() > limit);
    } 

Это очень простой, читаемый метод. Использование статического метода ( getAccount ) делает его «непроверяемым» в Java и других языках. Опять же, под «непроверенным» мы понимаем «трудно проверить», что в дальнейшем означает «трудно поддразнивать». В нашем случае, используя обычные методы, будет сложно смоделировать статический метод и управлять вводом.

Если мы исключаем использование PowerMockito, нам нужно изменить наш код, чтобы сделать его «тестируемым». Мы можем реорганизовать его для передачи  учетной записи  в качестве параметра. Как только Account передается в качестве аргумента (на самом деле, как интерфейс), мы можем  смоделировать интерфейс IAccount  и передать его. Теперь у нас есть тестируемый код.

bool isOverdrawn(int limit, IAccount account) {
    return (account.getBalance() > limit);
}

Но дизайн был улучшен?

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

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

Теперь давайте попробуем другое изменение дизайна ради тестируемости. На этот раз вместо извлечения параметра мы добавим в него контейнер для внедрения зависимостей (я буду использовать Geuce). Для этого нам нужно изменить   класс Customer и добавить:

private IAccount account;

@Injectpublic void setAccount(IAccount account) {
    this.account = account;
}

bool isOverdrawn(int limit) {
    return (account.getBalance() > limit);
} 

ЕГО ДИЗАЙН УЛУЧШЕН СЕЙЧАС?

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

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

TESTABLE CODE IS NOT INHERENTLY DESIGNED BETTER

Sometimes the changes are risky and costly. We need to balance the need for testing with the risk, and how the tools we use impact the design.

And let’s remember: this code was not “untestable”. We set a constraint to not use PowerMockito, and tried to work around it. We could easily tested that code as-is.

For years I’ve heard that tools like PowerMockito and Typemock Isolator, encourage bad design, because they allow to test badly designed code. It sounds bad, but it maybe a better solution than making risky changes so you can just test. Sometimes the changes are not even risky, but will create a more complex code, where it should be simple. 

Testing and design are broad skills every developer should have. 

As long as you’re making a knowledgeable decision, not based on popular slogans, you’ll be fine.