Почему многие команды отказываются от тестирования

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

Вы сталкивались с подобным? Вот типичная история. Семь часов вечера, на улице холодно и темно. Сара — относительно новый член команды, и она пытается развернуть систему. Кажется, что все хорошо, только вот несколько тестов, написанных кем-то другим, не работают. Сара открывает файл с тестами, просматривает их и не верит своим глазам — каждый тест состоит из 50 строк кода (с Prettier это стало 100 loc), включая циклы, if/else, коллбэки. Это раздражает, потому что она думала, что самое сложное — кодирование фичей — уже позади. Подождите, теперь она еще и заметила, что тест вызывает другие внешние классы, которые наследуются от других классов и имеют общее состояние с другими тестами. Становится трудно смотреть трейсы — уже восемь вечера, Сара голодная, начинает уже злиться, а еще на улице хмуро и идет дождь. В отличие от тестов, новый код, похоже, работает нормально — да, она также протестировала его с помощью Postman и браузера. В отчаянии она пытается воспользоваться магическим заклинанием, которое заставит все печали закончиться. Она добавляет одно словечко — test.skip. Теперь все тесты пройдены. Она разворачивает систему!

Угадайте, что произошло?

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

Какой вывод из этого можно сделать? Возможно, вы считали, что самое страшное, что может случиться с командой, — это ошибка в продакшене. Нет, это тесты, которые пропускаются (skipped) и вызывают потерю к ним доверия. Я работаю как консультант примерно с 20 различными командами в год над различными задачами в области Node.js и часто сталкиваюсь с подобным. Это замечаю не только я. Существует явление под названием «Разрушающаяся пирамида тестирования», которое описывает исчезновение тестов из кодовой базы.

Я слышу, что вы думаете: с нами такого не случится — мы дисциплинированы, у нас ежедневные скрам-митинги, мы измеряем покрытие! Ничего из этого не поможет. Тесты, как и оргазм, очень легко подделать (я что, правда это написал?). Эти иллюзорные тесты бывают разными: разработчики могут действительно их написать, но не будут им доверять. Затем они проверят код с помощью браузера или Postman, чтобы обрести настоящую уверенность. Такое ручное тестирование сводит на нет все причины проведения тестирования (скорость, автоматизация). Другие пишут тесты после написания кода, по факту, непосредственно перед продакшеном. Такой порядок работы подразумевает, что во время написания кода не было тестов, которые бы служили защитой от регрессии (p.s. Писать тесты до кода не обязательно, писать их во время написания кода — тоже нормально. Но не в самом конце). Со временем тесты становятся практически бесполезными — это налог, который приходится платить бедным разработчикам. Затем, со временем, тестирование уходит из повестки дня, оно существует только физически, как зомби.

Что стоит команде делать по-другому?

Почему случается это неприятное явление? Потому что разработчики — умные, избалованные и крайне эффективные существа, которые ищут кратчайший путь к цели. Мы все очень заняты основным кодом, и у нас не остается места для дополнительных систем, не говоря уже о системе тестирования. Если случайно и найдется дополнительный умственный потенциал для большего количества кода, который обещает ускорить их рабочий процесс, то он должен быть крошечным и очень простым. Позвольте мне сказать громко и ясно: тестирование может проявиться в наилучшем свете только тогда, когда разработчики убеждены в этом и с готовностью перенимают эту привычку. Это может произойти, если тесты не будут обузой для их занятого ума, а станут простой техникой, приносящей значительную пользу. Первый принцип тестирования — планировать выдающийся ROI (пояснение «Return On Investment», возврат инвестиций) — коэффициент рентабельности инвестиций, который помогает рассчитать окупаемость вложений), и извлекать значительные выгоды. Только в этом случае команды действительно будут его поддерживать.

Какие громкие слова, но как этого добиться? Следуя трем принципам. Во-первых, стремиться к минимальному объему тестирования, необходимому для получения уверенности (об этом говорил еще Кент Бек, «отец» TDD). Во-вторых, тесты должны отражать условия в продакшене, чтобы обеспечить уверенность (см. концепции shift-right и testing-in-production). Третий принцип заключается в написании предельно простых тестов — это тема данной статьи.

Говоря «простой тест», мы имеем в виду действительно элементарный код. Абсолютно плоский, уровень сложности равен единице, автономный, почти без зависимостей, который содержит 7-10 операторов и который написан на человекопонятном языке. Если получится соблюсти эти условия, то у Сары не останется шанса пропустить этот тест — ведь он слишком прост. Однако в реальности многие силы заставляют разработчиков отклоняться от золотого пути простоты: тесты будут включать циклы и логику, потому что не было найдено другого способа проверить то или иное поведение. Тесты станут длинными, хотя автор действительно старался этого избежать — просто слишком много чего нужно подготовить для теста. Другие задачи будут побуждать к написанию императивного и не очень простого кода. Когда это начинает происходить, остановитесь. Высокая сложность тестов случается с теми, кто готов ее принять. Эти негодяи придут, чтобы измолоть вас, но вы должны дать им отпор и уничтожить их на ранней стадии. Чтобы быть готовым к этой битве, я собрал несколько принципов и приемов под акронимом BASIC. Давайте рассмотрим эти принципы, а затем приведем пример неприятного реального теста, наполненного ненужной сложностью. Затем применим к нему принципы BASIC и посмотрим, как он уменьшится и превратится в красивый короткий тест.

Акроним BASIC

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

Черный ящик (Black-box, B) — нас волнует только то, что производит ящик, наш компонент или тестируемый объект, и что видно вызывающей стороне. Будь то API (интеграция) или объекты кода (юнит). Если ошибка не видна извне, она не будет иметь значения для наших пользователей, то не все ли равно? Избегайте тестирования того, КАК работают вещи, сосредоточьтесь на том, ЧТО они делают. Неважно, какие функции были вызваны, важен только результат: извне заметные вещи. Например, ответ тестируемой части, наблюдаемое новое состояние, значимые обращения к внешнему коду и др. Когда автор теста сосредотачивается на внешнем слое, количество деталей резко сокращается, и ему приходится расставлять приоритеты в отношении важных факторов, которые могут повлиять на UX. Неизбежно снижается продолжительность и сложность теста.

Аннотативность (Annotative, A) — тест должен иметь предсказуемую структуру с декларативным языком. Он должен быть больше похож на аннотации, чем на код. Просмотр продакшен кода — это путешествие с четким началом и неизвестным концом. Стек-трейс может разворачиваться бесконечно долго и привести сосредоточенного читателя во многие уголки приложения. За это время может потребоваться напрячь мозги, чтобы уловить смысл. Тестирование не должно быть таким, скорее, оно должно напоминать декларативный синтаксис, например... HTML. При чтении HTML-документации наш мозг без труда перебирает различные теги, почти ожидая, что будет дальше. Мало кто из разработчиков избегает или не читает фрагменты HTML из-за их сложности.

Как сделать тестирование более похожим на декларативное и структурированное? Используйте 6 повторяющихся частей в каждом тесте с использованием паттерна AAA (см. ниже), декларативные утверждения, до 7 утверждений. О других подобных паттернах читайте в статье JavaScript Testing Best Practices.

6 повторяющихся частей в каждом тесте
6 повторяющихся частей в каждом тесте

Один тест — одна проверка (Single Door, S) — каждый тест должен быть сфокусирован на чем-то одном. Обычно он должен вызывать одно действие в приложении и проверять один результат, который произошел в ответ на это действие. Мой друг Шай Резник (Shai Reznik) называет это «реакцией». Действием может быть вызов функции, нажатие кнопки, вызов REST-маршрута, новое сообщение в очереди, запланированное время начала работы или любое другое системное событие. После выполнения этого действия может произойти до трех событий: ответ, изменение состояния (в памяти или в БД) или обращение к стороннему сервису (Credit: Roy Osherove entry and exit point). В интеграционных тестах может быть шесть типов исходов, подробнее см. мой чеклист. Эти потенциальные исходы проверяются с помощью утверждений. В чем заключается выгода? Сужение каждого теста до одного действия-результата способствует сокращению времени тестирования и лучшему анализу первопричин — в случае неудачи становится ясно, что именно не работает. Это хорошее эмпирическое правило, которому нужно следовать, но не слишком жестко — можно протестировать 2 результата в одном тесте, можно 3, но ни в коем случае не 10. Сосредоточьтесь на цели, а не на количестве.

Независимость (Independent, I). Тест — это «проблема» длиной в 7-10 строк, крошечная вселенная, которая не делит ничего с другими. Если тест независим, краток и декларативен — его приятно читать случайному читателю. Однако следует помнить, что если просто связать тест с каким-то глобальным объектом, то он внезапно подвергнется побочным эффектам со стороны многих других тестов, и сложность резко возрастет. Связь также может возникнуть из-за общих записей в БД, зависимости от порядка выполнения, состояния пользовательского интерфейса, несброшенных ожиданий на тестовых моках и т.д. Избегайте всего этого. Вместо этого позвольте каждому тесту учитывать свои зависимости, потенциально используя хелперы, включая собственное состояние БД, чтобы сохранить его как интуитивно понятную вселенную.

Копируйте только необходимое (Copy only what’s necessary, C). Включайте в тест все необходимые детали, которые должны быть понятны читателю в пределах теста. Ничего лишнего, только необходимое. Излишнее дублирование приведет к тому, что тесты станут неуправляемыми; с другой стороны, извлечение важных деталей наружу заставит читателя искать информацию в разных файлах. В качестве примера можно привести тест, который должен обработать 100 строк входного JSON — вставлять это в каждый тест очень утомительно. Извлечение его наружу в transferFactory.getJSON() оставит тест неясным — без данных трудно соотнести результат теста с причиной («почему он должен вернуть статус 400?»). В классической книге по паттернам x-unit этот паттерн назван «таинственный гость» — что-то невидимое повлияло на результаты тестирования, но мы не знаем, что именно.

Чтобы добиться большего, необходимо извлекать повторяющиеся большие части наружу и явно указывать, какие именно детали важны для теста. В соответствии с приведенным выше примером, тест может передавать параметры, которые подчеркивают, что именно важно: transferFactory.getJSON({sender: undefined}). В этом примере читатель должен сразу понять, что пустое поле отправителя — это причина, по которой тест должен ожидать ошибку валидации или любой другой подобный адекватный результат.

Пример из реальной жизни: как неприятный тест превращается в красивый

Если отбросить теорию, то давайте превратим неприятный тест высокой сложности в простой, применив пять принципов BASIC

Тестируемый код

Это приложение, которое мы будем тестировать: сервис для денежных переводов. Код будет подтверждать запрос на перевод денег, применять некоторую логику, запрашивать банковский HTTP-сервис для перевода денег и, наконец, сохраняться в базе данных. Это намеренно не совсем «тестируемый» код, если не сказать больше — вероятно, вы хотите научиться тестировать любой код, а не только «идеальный».

class TransferService {
async transfer({ id, sender, receiver, transferAmount, bankName }) {
    // Validation
    if (!sender || !receiver || !transferAmount || !bankName) {
      throw new Error("Some mandatory property was not provided");
    }
  
    // Handle insufficient credit
    if (this.options.creditPolicy === "zero" && sender.credit < transferAmount) {
      this.numberOfDeclined++; //incrementing interal metric
      return {id, status: "declined",date};
    }

    // All good, let's transfer using the bank 3rd party service + save in DB
    await this.bankProviderService.transfer(sender, receiver, transferAmount, bankName);
    await this.repository.save({id,sender,receiver,transferAmount,bankName});

    return {id,status: "approved", date: new Date()};
  }  
  
  getTransfers(senderName){
    // Query the DB
  }
}

Неприятный тест

Приведенный выше тест проверяет, что если отправитель пытается перевести сумму, превышающую его средства, то отклоненный перевод не будет отображаться в истории пользователя. Другими словами, он не должен сохраняться. Это длинный и громоздкий тест, антипаттерн, но каждая строчка здесь якобы разумна и учитывает возможные риски. Прочитайте его, постарайтесь понять замысел автора — каждая строчка была включена в него вдумчиво сильным разработчиком, внимательным к деталям. Только после этого думайте о том, как ее улучшить. Символ X означает, что в этой строке есть что улучшить.

// ❌
  test('Should fail', () => {
    const transferRequest = testHelpers.factorMoneyTransfer({}); // ❌
    serviceUnderTest.options.creditPolicy = 'zero'; // ❌
    transferRequest.howMuch = 110; // ❌
    // Let's use the library sinon to listen to calls to the DB save function
    const databaseRepositoryMock = sinon.stub(dbRepository, 'save');//❌
    const transferResponse = serviceUnderTest.transfer(transferRequest);
    expect(transferResponse.currency).toBe('dollar'); // ❌
    expect(transferResponse.id).not.toBeNull(); // ❌
    expect(transferResponse.date.getDay()).toBe(new Date().getDay()); // ❌
    expect(serviceUnderTest.numberOfDeclined).toBe(1); // ❌
    expect(databaseRepositoryMock.calledOnce).toBe(false); // ❌

    // Let's get the user transfer history
    const allUserTransfers = serviceUnderTest.getTransfers(transferRequest.sender.name);
    expect(allUserTransfers).not.toBeNull(); // ❌ Overlapping
    expect(allUserTransfers).toBeType('array'); // ❌ Overlapping

    // check that declined transfer is not in user history array ❌
    let transferFound = false;
    allUserTransfers.forEach((transferToCheck) => {
      if (transferToCheck.id === transferRequest.id) {
        transferFound = true;
      }
    });
    expect(transferFound).toBe(false);

    // Let's check that an email was sent ❌
    if (transferRequest.options.sendMailOnDecline && transferResponse.status === 'declined') {
      const wasMailSent = testHelpers.verifyIfMailWasSentToTransfer(transferResponse.id);
      expect(wasMailSent).toBe(true);
    }
  });

Улучшаем тест с помощью принципов BASIC

Давайте пройдем от строки к строке и исправим их, применяя принципы BASIC

Строка 2: двусмысленность

❌ Плохо (строка 2):

test(‘Should fail’, () => {

✅ Лучше:

describe(‘transferMoney’)// The operation under test
{  test(‘When the user has not enough credit, then decline the   
request’, () => 
}

????????‍???? Применяемый паттерн: Аннотативность

???? Пояснение: Что здесь не так? Тест должен четко сообщать о своих намерениях в структурированном и известном формате, как объясняется в паттерне "Аннотативность" (шаблон 6 повторяющихся частей).

Строка № 3: Загадка

❌ Плохо (строка 3):

const transferRequest = testHelpers.factorMoneyTransfer({});

✅ Лучше:

const transferRequest = testHelpers.factorMoneyTransfer({credit: 50, 
transferAmount: 100});//Explictily specified the fields that matter here

????????‍???? Применяемый шаблон: Копируйте только необходимое

???? Пояснение: Что здесь не так? В тесте речь идет о переводе денег без достаточного количества средств на счете, эта строка абстрагирует нас от входного JSON, поэтому нет понимания, как он будет выглядеть. Читатель должен догадаться, что с этим JSON будут какие-то проблемы, но какие именно? Помните, что тест — это 7 утверждений, короткая и емкая история, которую читатель может понять, не отходя от теста. Известный прием повествования — предзнаменование (Foreshadowing) — предполагает, что все значимые детали должны «появляться в начале рассказа или главы, поскольку это помогает читателю сформировать ожидания относительно предстоящих событий».

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

Строка № 4: Сцепка

❌ Плохо (строка 4):

serviceUnderTest.options.creditPolicy = ‘zero’; 

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

????????‍???? Применяемый паттерн: Независимый

???? Пояснение: Что здесь не так? Эта строка резко повышает сложность теста. Поскольку все тесты имеют общий глобальный объект и могут изменять его свойства, сбои в тестировании могут возникнуть в любом другом тесте в файле. Вместо проблемы длиной в 7 строк вы просто получили спагетти из 50 тестов. Может быть, в каком-то тесте выше состояние системы изменилось на то, что мы не предусмотрели? Теперь вместо того, чтобы исследовать сбой в рамках одного короткого теста, необходимо просмотреть другие тест-кейсы в том же файле и определить, где все пошло не так.

Строка № 5: Магия чисел

❌ Плохо (строка 5):

 transferRequest.howMuch = 110; // 

✅ Лучше:

const amountMoreThanTheUserCredit = 110;
transferRequest.howMuch = amountMoreThanTheUserCredit;

или

transferRequest = {credit:50, howMuch: 100}

????????‍???? Применяемый паттерн: Копировать только то, что необходимо

???? Пояснение: Что здесь не так? Магическое число. Эта строка с произвольным числом '110' ничего не говорит читателю, это просто число. Высокое ли оно, низкое ли? Что оно обозначает? Автор хотел выбрать значение выше, чем баланс пользователя, критическая часть информации, которая тесно связана с результатами теста, но не сообщил об этом. С помощью некоторых приемов можно было бы рассказать эту историю лучше: константа с описательным именем (например, amountMoreThanTheUserCredit) или размещение суммы перевода рядом с недостаточным балансом.

Строка №6: Тестирование «белого ящика»

❌ Плохо (строка 6):

const databaseRepositoryMock = sinon.stub(dbRepository, ‘save’);
//This was meant to listen to the ‘save’ function call and ensure it 
was never called because the transfer should fail

✅ Лучше:

// Assert — Check that the transfer was not saved using the public 
API
const senderTransfersHistory = 
transferServiceUnderTest.getTransfers(transferRequest.sender.name);
expect(senderTransfersHistory).not.toContain(transferRequest);

????????‍???? Применяемый паттерн: Черный ящик

???? Пояснение: Что здесь не так? Намерения автора прекрасны, он хочет проверить, что отклоненный перевод не будет случайно сохранен в БД. Именно для этого и предназначен тест. Сомнение вызывает выбранная техника — использование моков для прослушивания вызова метода доступа к данным и проверки того, что он не был вызван. Всякий раз, когда мы используем дублеры теста (моки), у теста есть два слабых места: ложноположительные сигналы тревоги будут срабатывать всякий раз, когда мы модифицируем внутренний вызов метода 'save', и хотя все по-прежнему работает, тесты будут провалены. Хуже того, тесты будут приводить к ложноположительным сценариям, когда данные сохраняются в БД по другому пути или методом, но тест проходит. Вместо того, чтобы проверить естественный путь пользователя через публичный API, наш тест ограничивается внутренним и увеличивает сложность поддержки. Вот золотое правило тестирования: При любой возможности тестируйте путь пользователя так, как это происходит в продакшене — в этом и заключается суть тестирования методом «черного ящика».

Строки 8-10: Не релевантны

❌ Плохо (строка 6):

expect(transferResponse.currency).toBe(‘dollar’); 
 expect(transferResponse.id).not.toBeNull(); 
 expect(transferResponse.date.getDay()).toBe(new Date().getDay()); 

✅ Лучше: Просто удалить, так как эти проверки не являются частью истории тестов.

????????‍???? Связанный паттерн: Одна проверка

???? Пояснение: Что здесь не так? Автор данного теста амбициозен и стремится поймать любой баг во всем потоке передачи данных. Если валюта перевода не доллар или ответ не содержит обязательных полей — что-то не так. Конечно, нужно все проверить. Но есть еще один момент — тесты должны быть короткими и целенаправленными. Если мы будем тестировать длинный поток с различными конечными результатами в одном тест-кейсе, это снизит читабельность теста. Если произойдет сбой — это незначительная деталь, а может быть, вся система вышла из строя? Выявить первопричину станет сложнее. При тестировании вполне допустимо пожертвовать некоторыми деталями, чтобы выделить нужные деревья в огромном лесу.

Строка 11: Детали реализации

❌ Плохо (строка 11):

expect(serviceUnderTest.numberOfDeclined).toBe(1);

✅ Лучше: Убрать их, реализация нас не волнует — если результат удовлетворительный, то реализация, скорее всего, будет в порядке.

????????‍???? Применяемый паттерн: Черный ящик

???? Объяснение: Что здесь не так? Похоже, что реализация внутри использует поле 'NumberOfDeclined' для хранения отказов. Возможно, оно будет использоваться для сбора метрик или отчетов об ошибках. Это утверждение проверяет реализацию программы, а не общедоступный результат. По своей природе деталей реализации гораздо больше, чем результатов. Если тесты будут проверять каждую функцию, каждое поле и каждое взаимодействие, то у нас получится десятки или сотни тестов на каждую функцию, и, следовательно, файлы тестов тоже станут гораздо больше. 

В противном случае, если проверять только общедоступный результат, то тестов будет меньше, а проблем будет найдено столько же. Может быть, что-то не так внутри, но снаружи это не отражается? Если да, то ошибка не затрагивает пользователя; а нас это вообще волнует? Обратите внимание, что проверка результатов не означает проверку только API и UX — операционный отдел также является важным пользователем, и проверка срабатывания метрик, ошибок, логирования является обязательной. Это результаты, то, что производит сам черный ящик, а не детали реализации.

Строки 16-17: Наложение

❌ Плохо (строки 16-17):

// Let’s get the user transfer history 
const allUserTransfers = 
serviceUnderTest.getTransfers(transferRequest.sender.name); 
expect(allUserTransfers).not.toBeNull(); // ❌ Overlapping 
expect(allUserTransfers).toBeType(‘array’); // ❌ Overlapping

✅ Лучше:

expect(senderTransfersHistory).not.toContain(transferRequest);

????????‍???? Применяемый паттерн: Один тест — одна проверка

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

Строки 20-26: императивный код 

❌ Плохо (строки 20-26):

//Assertion: check that declined transfer is not in user history 
//array ❌
 let transferFound = false;
 allUserTransfers.forEach((transferToCheck) => {
   if (transferToCheck.id === transferRequest.id) {
      transferFound = true;
   }
 });
 expect(transferFound).toBe(false);

 ✅ Лучше:

expect(senderTransfersHistory).not.toContain(transferRequest);

????????‍???? Применяемый паттерн: Аннотативность

???? Пояснение: Что здесь не так? Тестировщица хочет убедиться, что отклоненный перевод не сохранился в системе и может быть восстановлен, поэтому она перебирает все переводы пользователя, чтобы убедиться, что это не так. Вроде бы все хорошо? Нет, это императивный код, и для того, чтобы понять его смысл, необходимо просмотреть реализацию. Если команда пойдет по этому пути и пустит в ход все орудия программирования — циклы, if/else, наследование, try-catch, — то потенциальной сложности не будет предела.

Более эффективный путь — сохранение супер простоты тестов с помощью декларативного (аннотативного) кода. Этот стиль не предполагает никаких деталей реализации, просто читай и понимай сразу. На практике следует придерживаться библиотек декларативных утверждений (Chai expect, Jest matches). Если сообщество не предлагает нужной логики утверждений — напишите собственную. Настройте линтеры, запрещающие императивный код в тестировании, например, jest/no-if. Ниже на видео показано, какие проблемы можно выявить с помощью линтеров. Если используется проверка сложности, разрешите уровень сложности не более 1 (абсолютно плоский код).

✨ Результат: Короткий, абсолютно простой и красивый тест

Помните тот громоздкий тест, с которого мы начали? Вот он после применения принципов BASIC.

test("When no credit, the declined transfer does not appear in sender history", () => {
      // Arrange
      const transferRequest = testHelpers.factorMoneyTransfer({
        sender: { credit: 50 },
        transferAmount: 100,
      });
      const transferServiceUnderTest = new TransferService({ creditPolicy: "NoCredit" });

      // Act
      transferServiceUnderTest.transfer(transferRequest);

      // Assert
      const senderTransfersHistory = transferServiceUnderTest.getTransfers(transferRequest.sender.name);
      expect(senderTransfersHistory).not.toContain(transferRequest);
    });

Несколько слов напоследок

Искусство тестирования заключается еще и в том, чтобы добиться нужной степени уверенности при минимальных усилиях. Даже Кент Бек, «отец TDD» — самой крайней парадигмы тестирования — однажды сказал:

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

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

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

Комментарии (0)