Короткий ответ
assertEqual (как и обычный оператор ==) сравнивает структуры по значению рекурсивно, а не по идентичности объектов, поэтому два разных объекта dict/list с одинаковым содержимым считаются равными — это и есть правильное, ожидаемое поведение теста на корректность данных; проверка идентичности (assertIs / is) нужна только в особом случае, когда тест специально проверяет, что функция вернула именно тот же самый объект (например, из кэша), а не его копию.
Как это работает подробнее
== для list и dict определён так, что он рекурсивно сравнивает элементы/значения по их собственному ==, независимо от того, являются ли контейнеры одним и тем же объектом в памяти. assertEqual в unittest внутри вызывает именно ==, поэтому для словарей и списков он фактически проверяет совпадение содержимого, а не адреса в памяти.
Пример кода
def build_payload():
return {"ids": [1, 2, 3], "meta": {"source": "api"}}
result = build_payload()
expected = {"ids": [1, 2, 3], "meta": {"source": "api"}}
print(result == expected) # True — сравнение по значению, разные объекты
print(result is expected) # False — это действительно два разных объекта
cache = {}
def cached_build():
if "payload" not in cache:
cache["payload"] = build_payload()
return cache["payload"]
a = cached_build()
b = cached_build()
print(a is b) # True — здесь важно именно это: один и тот же объект из кэшаЧто использовать на практике
- Практика. Такое поведение — то, что обычно и нужно в тестах на бизнес-логику: "функция должна вернуть структуру данных с такими-то полями и значениями", а не "функция должна вернуть именно тот объект, что был создан где-то внутри". Проверка идентичности через
assertIs(a, b)оправдана, например, при тестировании кэширующего декоратора, где важно убедиться, что повторный вызов вернул именно закешированный объект, а не пересчитанную копию с таким же содержимым.
Подводные камни
- Риски. Ожидают от
==/assertEqualповедения идентичности и удивляются, что "тест не ловит баг", хотя баг на самом деле не в равенстве значений. Также путают это с проблемой мутируемости: если тест сравнивает объект с самим собой после его последующей мутации в другом месте теста, он может "пройти" не из-за корректности логики, а из-за того, что сравниваются два имени, указывающих на один и тот же изменённый объект.