Главная страница
Навигация по странице:

  • Тестируйте поведение, а не методы

  • Структурируйте тесты, чтобы подчеркнуть поведение

  • Давайте тестам имена, описывающие тестируемое поведение

  • Не включайте логику в тесты

  • Пишите понятные сообщения об ошибках

  • Листинг 12.18.

  • Делай как вGoogle


    Скачать 5.77 Mb.
    НазваниеДелай как вGoogle
    Дата31.05.2022
    Размер5.77 Mb.
    Формат файлаpdf
    Имя файлаDelay_kak_v_Google_Razrabotka_programmnogo_obespechenia_2021_Tom.pdf
    ТипДокументы
    #559735
    страница31 из 69
    1   ...   27   28   29   30   31   32   33   34   ...   69
    Листинг 12.7. Полный и краткий тест
    @Test public void shouldPerformAddition() {
    Calculator calculator = newCalculator();
    int result = calculator.calculate(newCalculation(2, Operation.PLUS, 3));
    assertThat(result).isEqualTo(5);
    }
    Идеи, которые мы обсудим позже, в частности совместное использование кода, тоже будут связаны с полнотой и краткостью. В частности, нередко имеет смысл нарушить принцип DRY (don’t repeat yourself — «не повторяйся»), если это поможет написать более ясный тест. Помните: тело теста должно содержать всю информацию, необходимую для
    его понимания, без любой отвлекающей или не имеющей отношения к тесту информации.
    Тестируйте поведение, а не методы
    Многие инженеры инстинктивно пытаются подогнать структуру своих тестов к структуре тестируемого кода, чтобы для каждого метода в коде имелся соответству- ющий метод тестирования. Поначалу этот паттерн может быть удобен, но по мере усложнения тестируемого метода тест также усложняется и читать его становится все труднее. Например, взгляните на фрагмент кода в листинге 12.8, который выво- дит результаты транзакции:

    Создание ясных тестов
    241
    Листинг 12.8. Фрагмент реализации транзакции public void displayTransactionResults(User user, Transaction transaction) {
    ui.showMessage("You bought a " + transaction.getItemName());
    if (user.getBalance() < LOW_BALANCE_THRESHOLD) {
    ui.showMessage("Warning: your balance is low!");
    }
    }
    Часто тест охватывает оба сообщения, которые могут выводиться методом.
    Листинг 12.9. Тест, проверяющий метод
    @Test public void testDisplayTransactionResults() {
    transactionProcessor.displayTransactionResults(
    newUserWithBalance(
    LOW_BALANCE_THRESHOLD.plus(dollars(2))),
    new Transaction("Some Item", dollars(3)));
    assertThat(ui.getText()).contains("You bought a Some Item");
    assertThat(ui.getText()).contains("your balance is low");
    }
    В подобных ситуациях изначально тест мог проверять только первое сообщение.
    Инженер мог расширить тест после добавления вывода второго сообщения (нарушив идею неизменности тестов, которую мы обсуждали выше). Но это изменение создало бы плохой прецедент: по мере усложнения и расширения возможностей тестируемого метода его юнит-тест становился бы все более запутанным и сложным.
    Проблема заключается в том, что тестирование, основанное на повторении структуры методов, может способствовать появлению неясных тестов, потому что часто един- ственный метод решает несколько разных задач и может иметь несколько худших случаев. Поэтому пишите тесты не для каждого метода, а для каждого поведения
    1
    Поведение — это гарантированная реакция системы на серию входных данных в определенном состоянии
    2
    . Выразить поведение можно словами «дано», «если» и «тогда» (
    https://oreil.ly/I9IvR
    ): «Дано: банковский счет пуст. Если предпринимается попытка снять с него деньги, тогда транзакция должна быть отклонена». Методы и поведения связаны отношением многие-ко-многим: большинство нетривиальных методов реализуют множество поведений, а некоторые поведения полагаются на взаимодействие нескольких методов. Предыдущий пример можно переписать, ис- пользуя подход, основанный на тестировании поведения (листинг 12.10).
    Дополнительный шаблонный код введен для разделения теста (
    https://oreil.ly/hcoon
    ).
    Тесты, проверяющие поведение, по ряду причин получаются более ясными, чем тесты, проверяющие методы. Во-первых, они читаются почти как текст на естественном языке,
    1
    См. https://testing.googleblog.com/2014/04/testing-on-toilet-test-behaviors-not.html и https://
    dannorth.net/introducing-bdd
    2
    Кроме того, особенность (или продукт) можно выразить в виде набора поведений.

    242
    Глава 12. Юнит-тестирование что позволяет легко понять их без скрупулезного анализа. Во-вторых, они более ясно вы- ражают причинно-следственные связи в ограниченном объеме теста (
    https://oreil.ly/dAd3k
    ).
    Наконец, краткость и описательность теста помогают понять, какие функциональные возможности он тестирует, и побуждают инженеров писать более оптимизированные новые методы, вместо того чтобы пытаться вместить новый код в существующие методы.
    Листинг 12.10. Тестирование поведения
    @Test public void displayTransactionResults_showsItemName() {
    transactionProcessor.displayTransactionResults(
    new User(), new Transaction("Some Item"));
    assertThat(ui.getText()).contains("You bought a Some Item");
    }
    @Test public void displayTransactionResults_showsLowBalanceWarning() {
    transactionProcessor.displayTransactionResults(
    newUserWithBalance(
    LOW_BALANCE_THRESHOLD.plus(dollars(2))),
    new Transaction("Some Item", dollars(3)));
    assertThat(ui.getText()).contains("your balance is low");
    }
    Структурируйте тесты, чтобы подчеркнуть поведение
    Подход, основанный на тестировании поведения, влияет на структуру тестов. По- ведение состоит из трех частей: компонент «дано» определяет настройки системы, компонент «если» определяет выполняемое в системе действие и компонент «тог- да» проверяет результат. Тесты тем яснее, чем более явно следуют этой структуре.
    Некоторые фреймворки, такие как Cucumber (
    https://cucumber.io
    ) и Spock (
    http://
    spockframework.org
    ), напрямую используют структуру дано/если/тогда. В других языках можно использовать пустые строки и комментарии, чтобы выделить струк- туру, как показано в листинге 12.11.
    Листинг 12.11. Хорошо структурированный тест
    @Test public void transferFundsShouldMoveMoneyBetweenAccounts() {
    // Дано: два счета, на которых находятся $150 и $20
    Account account1 = newAccountWithBalance(usd(150));
    Account account2 = newAccountWithBalance(usd(20));
    // Если предпринята попытка перевести $100 с первого счета на второй bank.transferFunds(account1, account2, usd(100));
    // Тогда суммы на счетах должны отражать факт перевода assertThat(account1.getBalance()).isEqualTo(usd(50));
    assertThat(account2.getBalance()).isEqualTo(usd(120));
    }

    Создание ясных тестов
    243
    Такое подробное описание не всегда необходимо, особенно в тривиальных тестах: часто комментарии можно опустить и использовать только пустые строки, чтобы выделить разделы. Однако если тесты сложные, явные комментарии сделают их более простыми для понимания. Следующий шаблон позволяет читать тесты на трех уровнях детализации:
    1. Для начала читатель смотрит на имя метода теста (обсуждается ниже), чтобы получить примерное представление о тестируемом поведении.
    2. Если информации недостаточно, читатель знакомится с формальным описанием поведения в комментариях, выраженным через дано/если/тогда.
    3. Наконец, читатель исследует код, чтобы точно узнать, как поведение выражается.
    Этот шаблон чаще всего нарушается чередованием вызовов системы и инструкций проверки (то есть слиянием блоков «если» и «тогда»). Такое слияние делает тест менее ясным, потому что затрудняет различие между выполняемым действием и ожидаемым результатом.
    Чередование блоков «если» и «тогда» допустимо, когда необходимо проверить каждый шаг в многоступенчатом процессе. Длинные блоки для удобства чтения можно разделить союзом «и». В листинге 12.12 показано, как может выглядеть тест относительно сложного поведения.
    Листинг 12.12. Чередование блоков «если» и «тогда» в тесте
    @Test public void shouldTimeOutConnections() {
    // Дано: два пользователя
    User user1 = newUser();
    User user2 = newUser();
    // И пустой пул соединений с 10-минутным тайм-аутом
    Pool pool = newPool(Duration.minutes(10));
    // Если к пулу попытаются подключиться оба пользователя pool.connect(user1);
    pool.connect(user2);
    // Тогда пул должен содержать два соединения assertThat(pool.getConnections()).hasSize(2);
    // Если прошло 20 минут clock.advance(Duration.minutes(20));
    // Тогда в пуле не должно остаться соединений assertThat(pool.getConnections()).isEmpty();
    // И оба пользователя должны быть отключены assertThat(user1.isConnected()).isFalse();
    assertThat(user2.isConnected()).isFalse();
    }

    244
    Глава 12. Юнит-тестирование
    Проявляйте особое внимание при написании таких тестов и убедитесь, что по слу- чайности не пытаетесь протестировать несколько разных поведений. Каждый тест должен охватывать только одно поведение, и подавляющее большинство юнит-тестов должны включать только один блок «если» и один блок «тогда».
    Давайте тестам имена, описывающие тестируемое поведение
    Тесты, проверяющие методы, обычно называются в честь тестируемых методов (на- пример, тест для метода updateBalance обычно получает имя testUpdateBalance
    ).
    В имени теста, проверяющего поведение, мы можем передать намного больше по- лезной информации. Имя теста играет важную роль в расследовании причин неудач тестирования, поскольку оно отображается в отчетах о сбоях. Также имя — самый простой способ выразить цель теста.
    Чтобы имя теста подсказывало тестируемое поведение, в этом имени нужно ука- зать не только действия, предпринимаемые системой, но и ожидаемый результат тестирования (
    https://oreil.ly/8eqqv
    ). Имена тестов могут включать дополнительную информацию, например состояние системы или ее окружения перед выполнением действий с ней. Некоторые языки и фреймворки позволяют вкладывать тесты друг в друга и давать им строковые имена, как показано в примере фреймворка
    Jasmine (
    https://jasmine.github.io
    ):
    Листинг 12.13. Некоторые примеры паттернов вложенных имен describe("multiplication", function() {
    describe("with a positive number", function() {
    var positiveNumber = 10;
    it("is positive with another positive number", function() {
    expect(positiveNumber * 10).toBeGreaterThan(0);
    });
    it("is negative with a negative number", function() {
    expect(positiveNumber * -10).toBeLessThan(0);
    });
    });
    describe("with a negative number", function() {
    var negativeNumber = 10;
    it("is negative with a positive number", function() {
    expect(negativeNumber * 10).toBeLessThan(0);
    });
    it("is positive with another negative number", function() {
    expect(negativeNumber * -10).toBeGreaterThan(0);
    });
    });
    });
    Другие языки требуют кодировать эту информацию в имени метода, как в следующем паттерне именования.

    Создание ясных тестов
    245
    Листинг 12.14. Некоторые примеры паттернов именования методов multiplyingTwoPositiveNumbersShouldReturnAPositiveNumber multiply_postiveAndNegative_returnsNegative divide_byZero_throwsException
    Такие имена слишком многословны для использования в коде, но подходят для описания назначения тестов, поскольку нам никогда не придется писать код, вы- зывающий тесты.
    Любые политики именования приемлемы, если они неуклонно используются в од- ном тестовом классе. Если вам трудно придумать подходящее имя для теста, по- пробуйте начать со слова «should» (должен). Использование этого слова с именем тестируемого класса позволяет прочитать имя теста как предложение. Например, имя теста shouldNot AllowWithdrawalsWhenBalanceIsEmpty для класса
    BankAccount можно прочитать как «
    BankAccount не должен разрешать снимать средства с пустого счета». Чтение имен тестов в наборе должно давать достаточно полное представление о поведении, реализуемом SUT. Такие имена также помогают отследить сосредото- ченность теста на одном поведении: если в имени теста потребовался союз «и», то есть большая вероятность, что этот тест в действительности тестирует несколько поведений, и его следует разбить на несколько тестов!
    Не включайте логику в тесты
    Корректность ясных тестов видна с первого взгляда. Это происходит потому, что тест обрабатывает только определенный набор входных данных. В свою очередь, код должен обрабатывать любой входной сигнал, и чтобы гарантировать правильную работу логики, мы должны писать сложные проверки. Но тестовый код лишен таких возможностей — если у вас возникает чувство, что было бы неплохо написать тест для теста, значит, с ним что-то не так!
    Сложность часто имеет вид логики. Логика определяется посредством императив- ных элементов языка программирования, таких как операторы, циклы и условные выражения. Чтобы понять, к какому результату приведет код, содержащий логику, недостаточно просто прочитать его с экрана — необходимо провести вычисления.
    Чтобы сделать тест сложным для понимания, не нужно использовать много логики.
    Например, взгляните на тест в листинге 12.15 и оцените, выглядит ли он коррект- ным (
    https://oreil.ly/yJDqh
    ).
    Листинг 12.15. Логика скрывает ошибку в тесте
    @Test public void shouldNavigateToAlbumsPage() {
    String baseUrl = "http://photos.google.com/";
    Navigator nav = new Navigator(baseUrl);
    nav.goToAlbumPage();
    assertThat(nav.getCurrentUrl()).isEqualTo(baseUrl + "/albums");
    }

    246
    Глава 12. Юнит-тестирование
    Здесь не так много логики — всего одна операция конкатенации строк. Но если упростить тест, удалив оператор, ошибка сразу станет очевидной.
    Листинг 12.16. Отсутствие логики раскрывает ошибку в тесте
    @Test public void shouldNavigateToPhotosPage() {
    Navigator nav = new Navigator("http://photos.google.com/");
    nav.goToPhotosPage();
    assertThat(nav.getCurrentUrl()))
    .isEqualTo("http://photos.google.com//albums"); // Вот она!
    }
    Когда видна вся строка, сразу становится заметно, что тест ожидает наличия двух символов слеша в URL вместо одного. Если в коде будет допущена аналогичная ошибка, этот тест не сможет обнаружить ее. Дублирование базового URL — невы- сокая плата за увеличенную описательность и наглядность теста (см. обсуждение тестов DAMP и DRY далее в этой главе).
    Распознать ошибки в операциях конкатенации строк, особенно в сложных конструк- циях, таких как циклы и условные выражения, очень трудно. Вывод: старайтесь в тестах писать прямолинейный код и не бойтесь повторения его фрагментов, если это сделает тест более описательным и наглядным. Идеи повторения и совместного использования кода мы обсудим далее в этой главе.
    Пишите понятные сообщения об ошибках
    Еще один аспект, способствующий увеличению ясности, связан не с написанием самого теста, а с сообщением, которое видит инженер, когда тест терпит неудачу.
    Инженер должен иметь возможность диагностировать проблему, просто прочитав сообщение об ошибке в журнале или отчете, даже не заглядывая в сам тест. Хорошее сообщение об ошибке содержит почти ту же информацию, что и имя теста: оно долж- но четко отражать желаемый результат, фактический результат и любые параметры, имеющие отношение к результату.
    Вот пример плохого сообщения об ошибке:
    Test failed: account is closed
    1
    Непонятно, то ли тест потерпел неудачу, потому что учетная запись была закрыта, то ли ожидалось, что учетная запись будет закрыта, а тест потерпел неудачу, потому что это не так. Сообщение об ошибке должно четко отличать ожидаемое состояние от фактического и давать больше информации о результате:
    Expected an account in state CLOSED, but got account
    2
    :
    <{name: "my-account", state: "OPEN"}
    1
    Ошибка тестирования: учетная запись закрыта. — Примеч. пер.
    2
    Ожидалась учетная запись в состоянии ЗАКРЫТО, а была получена учетная запись. —
    Примеч. пер.

    Повторное использование тестов и кода: DAMP, не DRY
    247
    Хорошие библиотеки могут помочь писать информативные сообщения об ошибках.
    Рассмотрим утверждения в листинге 12.17, в первом из которых используются клас- сические утверждения из JUnit, а во втором — из Truth (
    https://truth.dev
    ), библиотеки утверждений, разработанной в Google.
    Листинг 12.17. Использование утверждений из библиотеки Truth
    Set colors = ImmutableSet.of("red", "green", "blue");
    assertTrue(colors.contains("orange")); // JUnit assertThat(colors).contains("orange"); // Truth
    Первое утверждение получает только логическое значение, поэтому может выво- дить лишь обобщенное сообщение об ошибке, например ожидалось
    ,
    но было получено

    , которое не очень информативно в случае неудачи теста. Второе утверждение явно получает проверяемый предмет и может выводить гораздо более информативные сообщения (
    https://oreil.ly/RFUEN
    ):
    AssertionError:
    <[red,
    green,
    blue]>
    должно содержать

    Не во всех языках есть такие средства, но всегда должна быть возможность вруч- ную добавить важную информацию в сообщение об ошибке. Например, тестовые утверждения в Go выглядят так:
    Листинг 12.18. Тестовое утверждение в Go result := Add(2, 3)
    if result != 5 {
    t.Errorf("Add(2, 3) = %v, want %v", result, 5)
    }
    Повторное использование тестов и кода: DAMP, не DRY
    Последний аспект написания ясных тестов и предотвращения хрупкости связан с повторным использованием кода. При разработке ПО принято следовать прин- ципу DRY («не повторяйся»), согласно которому ПО проще в обслуживании, если каждая идея реализована в одном определенном месте и повторение кода сведено к минимуму. Этот подход, в частности, значительно упрощает внесение изменений, потому что инженеру достаточно изменить только один фрагмент кода, а не искать точно такие же фрагменты по всему коду. Однако следование этому принципу мо- жет сделать код менее ясным, требуя от читателей переходить по цепочкам ссылок, чтобы понять, что делает код.
    В коде неясность является небольшой платой за простоту его изменения и сопро- вождения. Но следование принципу DRY не дает большой выгоды в тестовом коде.
    Хорошие тесты должны сохранять стабильность и желательно терпеть неудачу при изменении SUT. Сложность в тестах обходится дорого: работоспособность кода (по мере его усложнения) гарантирует набор тестов, но у самих тестов нет гаранта их работоспособности, поэтому с их усложнением увеличивается риск появления оши-

    248
    Глава 12. Юнит-тестирование бок. Как упоминалось выше, если тесты становятся слишком сложными и возникает желание их протестировать, значит, в тесте есть проблемы.
    Разрабатывая тестовый код, следуйте принципу DAMP (
    https://oreil.ly/5VPs2
    ), то есть используйте «описательные и осмысленные фразы» (descriptive and meaningful phrases). Небольшое дублирование кода в тестах допустимо, если это дублирование делает тест более простым и понятным. Рассмотрим тесты, чрезмерно близко сле- дующие принципу DRY.
    1   ...   27   28   29   30   31   32   33   34   ...   69


    написать администратору сайта