Как стать автором
Обновить

Типичные ошибки при написании юнит-тестов. Лекция Яндекса

Время на прочтение10 мин
Количество просмотров24K
Всего голосов 42: ↑36 и ↓6+30
Комментарии32

Комментарии 32

Мельком пролистывая, подумал: «А Бутрутдинов то что здесь делает»

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


Мое мнение — прошу не ассоциировать его со мнением моей команды, — что тесты нам помогают.

Т.е. не все в команде согласны, что тесты это хорошо? )


long_name
multiple_asserts

Не совсем согласен. Иногда бывает так, что тестируется какой-то особо хитрый corner case, который возникает после какой-то определенной последовательности действий. Причем добавлять ассерты в промежуточные моменты также бывает полезным, чтобы точно знать, что все действительно идет так, как ожидается. Понятно, что если такая ситуация возникает, то это сигнал к тому, что тестируемый объект вероятно надо как-то распиливать, но далеко не всегда это возможно сделать быстро, особенно если это еще и legacy.


При этом ассерты могут быть довольно сложные.

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


random

Плюс много. Еще могу добавить к перечисленному списку источников рандома — передача get_current_time в тестируемую функцию, которая ожидает время.
Правда есть отдельный случай, когда рандом имхо оправдан — это property based testing, но там во всех нормальных фреймворках всегда есть возможность воспроизводимости, как правило через seed.


thread_sleep

О да. И еще всякие while current_time < last_time + timeout в разных вариациях. Причем часто интерфейс можно очень легко сделать так, чтобы таймстемпы можно было инжектить, но к сожалению все равно находятся товарищи, которые говорят "фуу, только ради инжекта таймстемпов менять интерфейс".


@VisibleForTesting

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

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

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

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

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

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

Фикстуры — да, полезны когда какое-то конкретное состояние системы может использоваться в куче разных тестов. Но я говорил про другое — когда есть объект какого-то класса, у него например вызывается два раза метод a(), потом три раза метод b(), а потом снова a() и тут его раскорячивает. Как по мне — вполне нормальный кейс для отдельного изолированного теста, воспроизводящего конкретный баг (при том, что есть также отдельные тесты на более простые ситуации).


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

Несколько раз перечитал эту фразу, и не понял. Можете пояснить (желательно на примере)?


Это тоже test smell, когда методы тестируются не изолированно друг от друга.

Да, отстутвие изоляции между тестами — это фуфуфу. Но я предлагал совсем не это.


Хороший модульный тест своим фейлом говорит, что «вот этот конкретный метод не работает так, как задуман»

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


x.setA(10)
assert x.a() == 10

это проверка сразу пары методов класса.

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

Основные причины — легаси код; свой фреймворк, быстрый но сильно связанный внутри; бизнес логика, на 99% завязанная на работу с базами данных.

Подозреваю, что я что-то делаю не так, и каждый раз читаю такие статьи в надежде найти в них ответы — и не нахожу. Например, как можно протестировать что при редактировании определённой сущности, в базе данных появляется N связанных сущностей? А если их появление должно зависеть от N переменных и\или переключателей в данных, поступивших с фронта? По идее, для этого нужно полностью переписывать классы работы с базой, чтобы обеспечить возможность мокать их. Или поднимать отдельную тестовую базу, которая будет заполняться тестовыми данными и очищаться после прогона тестов? Ситуация осложняется тем, что местами код работает с ORM, а местами для скорости используются прямые запросы к БД. В проекте используются четыре разные базы данных — MariaDB, ClickHouse, CouchBase и Redis — у всех свои задачи, но проблему тестирования взаимодействия всего этого зоопарка это только усугубляет.

Если кто-то в меня кинет статьёй о том, как правильно тестировать бизнес логику, завязанную на работу с БД — буду очень признателен…
Иногда пытаюсь подступиться к этой задаче, но каждый раз ломаю зубы и откатываюсь.

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

Дальнейшее движение по более сложным классам вскроет довольно много интересного в используемой архитектуре, когда вы чуть ли не каждый метод каждого класса будете распознавать как for single use only. Т.е. из-за сильной связанности данный метод применим (полезен) только в данной конкретной ситуации, и развязать весь контекст, чтобы изолированно протестировать этот метод, не так уж и просто.

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

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

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

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

Нельзя сказать, что это всегда плохо. Писать тесты на маленькие классы, которые завтра удалят в результате рефакторинга это расточительно. Я чаще вижу такое — тестировать хочется то, в чем не уверен, эта часть системы чаще подвергается рефакторингу, чаще меняются ее внутренности. Юниты в таких случаях становятся обузой их надо постоянно переписывать. Проще проверить требуемое поведение всего куска и не лезть в детали того, как оно там разбито на классы.
Да, постоянное мутирование системы тоже вносит свою боль. Места, которые не меняются — уже протестированы руками, и там ничего не ломается, хотя в случае каких либо право и придётся провести регрессионные тесты, и это будет муторно делать руками. Намного чаще баги находятся в местах с экспериментальным функционалом, к которому тз менялось уже несколько раз за полгода, и там постоянно всё переписывается.
Но проблема же именно в связанности контекстов, которые по большей части лежат в плоскости БД, или вообще в сторонних сервисах. По сути, для того чтобы нормально проверять классы, надо инвертировать зависимости и, например, отказываться от применения орм, поскольку он слабо тестируем. И естественно, выпиливать работу с бд напрямую. Но дополнительные уровни абстракции не только дадут возможность тестирования — они так же принесут свой оверхед, что недопустимо. На данный момент проект держит до 3к рпс, и упирается в коуч по производительности, один из параллельных проектов запущенный на laravel в порядке эксперимента — показал крайне неудовлетворительные результаты — на сравнимой нагрузке падал уже php(даже при полном отключении работы с бд — просто вызов апи). Плюс, по большей части баги находятся на границе бэка и фронта — и нужно по сути проверить что при наличии определённого набора данных в базе(включающих конкретного авторизованного пользователя и набор сущностей привязанных к нему), на фронт будет отдаваться определённый набор данных. Но это, по идее, уже надо интеграционными тестами делать.
Но проблема же именно в связанности контекстов, которые по большей части лежат в плоскости БД, или вообще в сторонних сервисах. По сути, для того чтобы нормально проверять классы, надо инвертировать зависимости и, например, отказываться от применения орм, поскольку он слабо тестируем. И естественно, выпиливать работу с бд напрямую.

Вопрос в том, какую конкретно задачу вы хотите решить тестами?

Если проверять, что методы правильно раскладывают данные в базе данных, то надо делать тестовую бд, in-memory либо ещё как-то.

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

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

Начните с интеграционных или e2e тестов для начала. Запустили всю систему, дернули апи и проверили результат, фактически автоматизируете работу тестеров. Таких тестов много не надо, но они ловят очень много багов. Когда есть хоть какая-то уверенность в том, что «тесты прошли, значит система работает», можно уже рефактоить. Если получится сделать хорошую обвязку для интеграционных тестов: in-memory db, фейковые очереди, фейковые платежные системы и email сервисы, то писать юниты может и не нужно совсем, ваши интеграционные тесты будут проверять 90% системы с 10% усилий на написание и поддержку тестов.


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

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

Если у вас логика в БД (например, хранимые процедуры), можно проводить unit-тесты именно реализации логики в БД.

Конкретных советов давать не буду, я не знаю вашей системы. А если в общем, то раз ваша система про БД, то и тестировать надо как она работает с БД. То есть проверять, в первую очередь, что система свою работу делает. А дальше уже проверять, если надо, какие-то нюансы.

Мне кажется деление на «юнит» и «не юнит» тесты несколько искусственное. У теста есть характеристики, вроде скорости работы, воспроизводимости, сложности написания, покрытия и т.п. Обычно хочется максимальное покрытие, минимальным количеством, воспроизводимыми и быстрыми тестами и обычно так не выходит. Искусство заключается в выборе удобного компромисса и поддержании его на протяжении жизни системы.
Может сложновато будет на русском :) но все же посмотри тут infostart.ru/public/544782, 1С это как раз про БД.
Вот только на днях пролистывал статью и во многом согласен с автором — моульные тесты стали карго-культом, их просто надо делать, а кто его знает зачем?
Для рефакторинга? Да где вы видели рефакторинг который не затрагивает интерфейсов модулей? Главные проблемы-то с архитектурой, она рефакторится обычно и тогда все эти модельные тесты идут на помойку.
Да и тут пишут:
В тот момент я понял, что unit-тесты не гарантируют хороший дизайн и работающий функционал. Перестаньте считать процент покрытия кода тестами. Эта метрика ничего не показывает.

смотрю зарубежные авторы более взвешенно относятся к юнит-тестам, может о нас ещё не дошли эти практики.
Стоит следить за процентом _не покрытия_ условий тестами, Сонар умеет показывать эту метрику.
Она не должна быть целью, но хорошо показывает, что происходит с проектом на больших дистанциях
Например, у нас на проекте процент непокрытых условий в 2016 году был 71%, а сейчас — 42%, и монотонно убывает. Надо заметить, что мы смотрим на общее покрытие от юнит, интеграционных и end-to-end тестов.
Польза конечно от этого есть, но после достижения чего-то близкого к 100% надо будет искать другие метрики.
О, здравые мысли в комментариях! Хоть кто-то включает голову, а не дрочит на покрытие и пустые зеленые галочки. Статья Jim Coplien'a просто обязательна к прочтению. Вот она же в pdf: rbcs-us.com/documents/Why-Most-Unit-Testing-is-Waste.pdf. Тут на нее ссылается David Heinemeier Hansson: dhh.dk/2014/tdd-is-dead-long-live-testing.html. Немного о другом, но Dan Abramov про TDD: twitter.com/dan_abramov/status/1086418722124906497. Все это не последние люди в программировании, которым есть что сказать. Но это уже следующий уровень понимания, мерзкий голос рассудка, который тихо подкрадывается и обзывает тебя клоуном, каждый раз когда ты пишешь очередную пачку идиотских тестов вокруг «1+1 = 2», чтобы не просело покрытие.
Я новичок в тестировании. Тема меня эта интересует: стараюсь изучать соответствующие материалы. Но пока мне не везет, и я не могу получить ответы на свои вопросы.

Я надеюсь, хабро-сообщество не будет против, если я озвучу здесь эти вопросы?
Всё-таки Хабр — это соц.сеть, где программисты делятся опытом.

Ремарка: понимаю, что не все тесты озвученные вопросы ниже относятся к unit-тестированию.
Ремарка2: некоторые вопросы я озвучивал на Тостере. Но качественных ответов так и не получил.

1. Нужно протестировать распределение hashCode у своего класса. Вам нужно сделать оценку кэш-промахов.

2. У вас функция шифрует данные (пусть алгоритмом AES).
Ваша задача убедиться, что вы правильно шифруете (правильно вызвали библиотечный код — там много подводных камней), и удаленный получатель сможет их расшифровать.
Проблема: для одной и той же входной последовательности шифр всегда разный.

3. Ваш класс генерирует параметризируемый mesh 3D-объекта. Пусть это будет куб с закругленными краями. Одна функция — генерирует массив точек, другая — массив индексов точек, связывающих их в полигоны, третья — нормали к вершинам, четвертая — текстурные координаты. Все функции — независимы друг от друга (вообще это чистые функции). Их можно хоть вынести в другие классы. Как протестировать их работу? А именно:
1) что куб — вообще на куб похож
2) что все полигоны обращены к наблюдателю, как ожидается
3) не перепутаны индексы полигонов или текстурных координат

4. Ваша функция увеличивает среднюю громкость аудиопотока. Как написать тест на это?

5. Вы реализовали генератор случайных чисел. Как его протестировать?

6. Функция получает на вход дату рождения человека и возвращает его возраст. Как написать тест на такую функцию?

7. Есть некий класс reader с двумя функциями: readChar(), isAvailable()
Первая функция — читает из буфера символ, вторая — проверяет, что буфер не исчерпан.
(Пусть буфер — это просто массив символов)
Как написать правильный тест на isAvailable? Функция readChar() влияет на результат работы isAvailable().

8. Функция получает сложный объект (например, большую json-структуру) и вытягивает нужные ей значения, например так: json.getDeclarant().getPersonalInfo().getIdentityDocument().getIssueDate().
Как правильно сделать mock такого объекта?

Благодарю всех, кто даст конструктивные советы.
У вас функция шифрует данные (пусть алгоритмом AES).

assert decrypt(encrypt(data)) == data и проверяете на самых разных data


Ваш класс генерирует параметризируемый mesh 3D-объекта. Пусть это будет куб с закругленными краями.
1) что куб — вообще на куб похож.

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


2) что все полигоны обращены к наблюдателю, как ожидается

Например взять центральную точку куба, и для каждого треугольника assert dot(((a+b+c)/3 - center), cross(b-a, c-a)) >= 0.


3) не перепутаны индексы полигонов или текстурных координат

Топологические проверки очень неплохо делать с помощью half-edge структуры данных. Как минимум — если у вас в принципе по набору треугольников получилось построить half-edge граф, то сетка является 2-manifold (а в большинсте подобных случаем именно такая проверка и требуется).


Ваша функция увеличивает среднюю громкость аудиопотока. Как написать тест на это?

assert stddev(output) > stddev(input), где stddev — функция рассчета среднеквадратичного отклонения.


Вы реализовали генератор случайных чисел. Как его протестировать?

Если не считать очевидных ассертов на вхождение вывода генератора в некий диапазон, если это применимо, то как минимум можно посчитать среднее и СКО, причем лучше делать это примерно так:


for _ in range(min_iters):
    stats.add(gen_under_test())
for _ in range(max_iters - min_iters):
    if abs(stats.mean - expected_mean) < tolerance:
        return
    stats.add(gen_under_test())
assert False, "after maximum iterations mean {} was still far to off from expected {}".format(stats.mean, expected_mean)

где stats — некий статистический аккумулятор. Смысл — с одной стороны как можно раньше закончить тест, с другой — если прям сильно неудачные значения из генератора выскакивают, то можно было подождать подольше. Хотя в целом имеет право на существование и более простой вариант assert abs(mean(gen_under_test() for _ in range(iter_count)) - expected_mean) < tolerance. Аналогично можно проверять гистограмму на соответствие функции плотности вероятности.


Функция получает на вход дату рождения человека и возвращает его возраст. Как написать тест на такую функцию?

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


Есть некий класс reader с двумя функциями: readChar(), isAvailable()

Если длина буфера заранее известна, то первое, что приходит в голову:


for _ in range(buf_size):
    assert isAvailable()
    readChar()
assert not isAvailable()

Но в идеале конечно лучше несколько раздельных тестов.

Спасибо, за ответы!

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

Да, так и есть:
private function getYearsOld( Date birthDate )
{
   Date currentDate = new Date(); // 
   ...
}

Наверное, Вы правы. Стоит передавать текущую дату в качестве второго параметра, превратив функцию getYearsOld в чистую (без явных и скрытых зависимостей). Тогда можно её вынести вообще в отдельный класс (сейчас она private), сделав static public.

Что касается тестирования reader. В принципе, я так и делал. Но не могу отделаться от ощущения, что это smell-test.

А кто, что посоветует для 8-го вопроса?
assert decrypt(encrypt(data)) == data и проверяете на самых разных data

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


buffer encrypt(buffer unecrypted) {
  return unencrypted;
}

buffer decrypt (buffer encrypted) {
  return encrypted;
}

Что, разумеется, полная чушь.


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

Это уже не юнит тест

Вы правы. И я писал, что «не все озвученные вопросы ниже относятся к unit-тестированию. Но, меня, как практика, интересует вопрос — как проверить, что написанный мною код работает „вообще“. А какой это тест: модульный, функциональный или интеграционный — дело десятое.

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

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

Похоже, что методика:
assert decrypt(encrypt(data)) == data // проверяете на самых разных data

пока без вариантов
Это уже не юнит тест, вы тестируете связку двух методов.

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


К тому же ваш подход даст зеленый свет на таком коде

Возможно мне следовало уточнить, что предложенный мной тест не является исчерпывающим. Разумеется, необходимо проверять и другие вещи — как минимум неравенство encrypted != decrypted, невозможность расшифровать "чужим" ключом, и т.п. И да, я полностью согласен, что должна быть возможность инжектить nonce, чтобы можно было написать тест-кейсы на конкретные примеры.

2. У вас функция шифрует данные (пусть алгоритмом AES).
Ваша задача убедиться, что вы правильно шифруете (правильно вызвали библиотечный код — там много подводных камней), и удаленный получатель сможет их расшифровать.
Проблема: для одной и той же входной последовательности шифр всегда разный.

Какой контракт мы хотим подтвердить своим тестом? Что клиент расшифрует данные, которые мы ему отправим? Т.е. мы проверяем то, что данные, которым мы отправим, могут быть расшифрованы.
Тест тут прост и ясен:
encryptedData = encrypt(expectedData)
assertDecrypt(expectedData, encryptedData);

В assertDecrypt мы прячем реализацию decrypt, которую рассматриваем как эталонную: наш контракт таков, что зашифрованные данные будут расшифрованы данной реализацией. Используйте её или совместимую. Мы проверяем, что наша функция encrypt возвращает данные, которые могут быть расшифрованы этой эталонной реализацией.

Если мы хотим проверить, что мы можем расшифровать некие зашифрованные данные, то подход тот же:
decryptedData = decrypt(encryptedData);
assertEqual(expectedData, decryptedData);

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

Но вот чего точно нельзя делать, так это взаимно использовать decrypt <-> encrypt в рамках одного теста, так как вместо валидации контракта, мы валидируем обратимость функций, о чём тут уже упомянули.
В assertDecrypt мы прячем реализацию decrypt, которую рассматриваем как эталонную: наш контракт таков, что зашифрованные данные будут расшифрованы данной реализацией.

А потом, чтобы протестировать decrypt вы будете писать аналогичный assertEncrypt, в который спрячете "эталонный" encrypt? Но по факту это же будет два одинаковых теста?


вместо валидации контракта, мы валидируем обратимость функций

А разве контракт не подразумевает, что функция шифрования должна быть обратимой? В предложенном мной тесте именно эта часть контракта и валидируется, и если я вас правильно понял — в вашем варианте проверяется ровно та же часть контракта.

Вопрос в том, что вы хотите подтвердить тестом, который работает с ответом, полученным от функции encrypt? То, что в вашем проекте есть функция (например, decrypt), которая выдаст ожидаемый результат? Или то, что некая внешняя библиотека может расшифровать эти данные?

Те же вопросы и для функции decrypt? Вы хотите подтвердить, что эта функция может расшифровать ответ от вашей функции encrypt? Или расшифровать сообщение, зашифрованное какой-то сторонней библиотекой?
Проверка
assertThat(decrypt(encrypt(data)), is(data))
проверяет только, что последовательно вызванные методы вернут исходные данные. Правильно ли происходит шифрование, такой подход не проверит.
Проверять стоит на референсных тестовых data и encryptedData, которые получены не вашим кодом, а какими-то сторонними утилитами.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий