- PVSM.RU - https://www.pvsm.ru -
Привет! Меня зовут Артём, и большую часть своего рабочего времени я пишу сложные автотесты на Selenium и Cucumber/Calabash. Честно говоря, довольно часто я оказываюсь перед непростым выбором: написать тест, который проверяет конкретную реализацию функциональности (потому что это проще) или тест, который проверяет функциональность (потому что это правильнее, но намного сложнее)? Недавно мне попалась неплохая статья о том, что тесты реализации – это «тавтологические» тесты. И, прочитав её, я уже почти неделю переписываю некоторые тесты в другом ключе. Надеюсь, вас она тоже подтолкнёт к размышлениям.
Все знают, что тесты крайне важны для быстрого создания качественного ПО. Но, как и всё остальное в нашей жизни, при неправильном использовании они могут принести больше вреда, чем пользы. Рассмотрим следующую несложную функцию и тест. В данном случае автор хочет защитить тесты от внешних зависимостей, поэтому используются заглушки.
import hashlib
from typing import List
from unittest.mock import patch
def get_key(key: str, values: List[str]) -> str:
md5_hash = hashlib.md5(key)
for value in values:
md5_hash.update(value)
return f'{key}:{md5_hash.hexdigest()}'
@patch('hashlib.md5')
def test_hash_values(mock_md5):
mock_md5.return_value.hexdigest.return_value = 'world'
assert get_key('hello', ['world']) == 'hello:world'
mock_md5.assert_called_once_with('hello')
mock_md5.return_value.update.assert_called_once_with('world')
mock_md5.return_value.hexdigest.assert_called()
Выглядит прекрасно! Четыре утверждения полностью протестированы, чтобы удостовериться, что код работает как ожидается. Тесты даже проходятся!
$ python3.6 -m pytest test_simple.py
========= test session starts =========
itemstest_simple.py .
======= 1 passed in 0.03 seconds ======
Конечно, проблема в том, что код ошибочен. md5 принимает только bytes
, а не str
(в этом посте [1] объясняется, как в Python 3 изменились bytes
и str
). Тестовый сценарий не играет большой роли; здесь протестировано лишь строковое форматирование, что даёт нам ложное ощущение безопасности: нам кажется, что код написано корректно, и мы это даже доказали с помощью тестовых сценариев!
К счастью, mypy [2] вылавливает эти проблемы:
$ mypy test_simple.py
test_simple.py:6: error: Argument 1 to “md5” has incompatible type “str”; expected “Union[bytes, bytearray, memoryview]”
test_simple.py:8: error: Argument 1 to “update” of “_Hash” has incompatible type “str”; expected “Union[bytes, bytearray, memoryview]”
Замечательно, мы исправили наш код, чтобы сначала перекодировать строки в байты:
def get_key(key: str, values: List[str]) -> str:
md5_hash = hashlib.md5(key.encode())
for value in values:
md5_hash.update(value.encode())
return f'{key}:{md5_hash.hexdigest()}'
Теперь код работает, но проблемы остались. Допустим, кто-то прошёлся по нашему коду и упростил его всего до нескольких строк:
def get_key(key: str, values: List[str]) -> str:
hash_value = hashlib.md5(f"{key}{''.join(values)}".encode()).hexdigest()
return f'{key}:{hash_value}'
Функционально получившееся идентично исходному коду. Для тех же входных данных он всегда будет возвращать такой же результат. Но даже в этом случае тестирование проходит с ошибкой:
E AssertionError: Expected call: md5(b’hello’)
E Actual call: md5(b’helloworld’)
Очевидно, что с этим простым тестом есть какая-то проблема. Здесь одновременно присутствуют ошибка первого рода [3] (тест падает, даже если код корректен) и ошибка второго рода [4] (тест не падает, когда код некорректен). В идеальном мире тесты будут падать, если (и только если) код содержит ошибку. А в ещё более идеальном мире при прохождении тестов можно быть полностью уверенным в корректности кода. И хотя оба идеала недостижимы, к ним стоит стремиться.
Тесты, описанные выше, я называю «тавтологическими». Они подтверждают корректность кода, гарантируя, что он выполняется так, как написан, что, конечно, предполагает, что он написан правильно.
Я считаю, что тавтологические тесты – несомненный минус для вашего кода. По нескольким причинам:
Иными словами, тавтологические тесты часто упускают реальные проблемы, стимулируя дурную привычку вслепую править тесты, и при этом польза от них не окупает усилия на их поддержку.
Давайте перепишем тест, чтобы проверять выходные данные:
def test_hash_values(mock_md5):
expected_value = 'hello:fc5e038d38a57032085441e7fe7010b0'
assert get_key('hello', ['world']) == expected_value
Теперь для теста не важны детали get_key
, он будет сбоить только в том случае, если get_key
вернёт неправильное значение. Я могу по своему желанию менять внутренности get_key
, не обновляя при этом тесты (пока не изменю публичное поведение). При этом тест получается кратким и лёгким для понимания.
Хотя это и надуманный пример, но в реальном коде легко можно найти места, где ради увеличения покрытия кода предполагается, что выходные данные внешних сервисов соответствуют ожиданиям реализации.
Код тестов невозможно редактировать без сверки с реализацией. В этом случае велик шанс, что вы получили тавтологический тест. В Testing on the Toilet: Don’t Overuse Mocks [7] вы найдёте очень знакомый пример. Вы можете воссоздать саму реализацию на основании этого теста:
public void testCreditCardIsCharged() {
paymentProcessor = new PaymentProcessor(mockCreditCardServer);
when(mockCreditCardServer.isServerAvailable()).thenReturn(true);
when(mockCreditCardServer.beginTransaction()).thenReturn(mockTransactionManager);
when(mockTransactionManager.getTransaction()).thenReturn(transaction);
when(mockCreditCardServer.pay(transaction, creditCard, 500)).thenReturn(mockPayment);
when(mockPayment.isOverMaxBalance()).thenReturn(false);
paymentProcessor.processPayment(creditCard, Money.dollars(500));
verify(mockCreditCardServer).pay(transaction, creditCard, 500);
}
Избегайте заглушек в находящихся в памяти объектах. Чтобы в качестве заглушек использовать зависимости, находящиеся целиком в памяти, нужны очень веские причины. Возможно, лежащая в основе функция является недетерминированной или исполняется слишком долго. Использование реальных объектов повышает ценность тестов за счёт проверки в рамках тестового сценария большего количества взаимодействий. Но даже в этом случае должны быть тесты, позволяющие удостовериться, что код правильно использует эти зависимости (вроде теста, проверяющего, что выходные данные находятся в ожидаемом диапазоне). Ниже приведён пример, в котором мы проверяем, что наш код работает в том случае, если randint
возвращает определённое значение, и что мы правильно вызываем randint
.
import random
from unittest.mock import patch
def get_thing():
return random.randint(0, 10)
@patch('random.randint')
def test_random_mock(mock_randint):
mock_randint.return_value = 3
assert get_thing() == 3
def test_random_real():
assert 0 <= get_thing() < 10
Лучше оставить строку кода непокрытой, чем создать иллюзию того, что она хорошо протестирована.
Также обращайте внимание на тавтологические тесты, проводя ревизию чужого кода. Спрашивайте себя, что на самом деле проверяет этот тест, а не просто покрывает ли он какие-то строки кода.
Помните, тавтологические тесты – плохие, потому что они не хорошие.
Автор: bbidox
Источник [12]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/python/262620
Ссылки в тексте:
[1] этом посте: http://eli.thegreenplace.net/2012/01/30/the-bytesstr-dichotomy-in-python-3
[2] mypy: http://mypy-lang.org/
[3] ошибка первого рода: https://www.khanacademy.org/math/statistics-probability/significance-tests-one-sample/idea-of-significance-tests/v/type-1-errors
[4] ошибка второго рода: https://ru.wikipedia.org/wiki/%D0%9E%D1%88%D0%B8%D0%B1%D0%BA%D0%B8_%D0%BF%D0%B5%D1%80%D0%B2%D0%BE%D0%B3%D0%BE_%D0%B8_%D0%B2%D1%82%D0%BE%D1%80%D0%BE%D0%B3%D0%BE_%D1%80%D0%BE%D0%B4%D0%B0
[5] Hack: http://hacklang.org/
[6] TypeScript: https://www.typescriptlang.org/
[7] Testing on the Toilet: Don’t Overuse Mocks: https://testing.googleblog.com/2013/05/testing-on-toilet-dont-overuse-mocks.html
[8] Sans-I/O: https://sans-io.readthedocs.io/how-to-sans-io.html
[9] Building Protocol Libraries The Right Way: https://www.youtube.com/watch?v=7cC3_jGwl_U
[10] Mocks Aren’t Stubs: https://martinfowler.com/articles/mocksArentStubs.html
[11] Testing Behavior vs. Testing Implementation: https://teamgaslight.com/blog/testing-behavior-vs-testing-implementation
[12] Источник: https://habrahabr.ru/post/336194/
Нажмите здесь для печати.