1. Робити мок того що тобі не належить, наприклад http-клієнта
Коли ви мокаєте зовнішній компонент, ви замінюєте його спрощеною версією, яка може не відображати його реальну поведінку. Це призведе до того, що коли у реальному середовищі виникнуть проблеми - ваші тести всеодно будуть зеленими.
2. Мокати через звичайний Mock() а не через create_autospec та альтернативи
Звичайний Mock() не відтворює інтерфейс того що ви мокаєте. Це означає, що ви можете викликати методи, які не існують у реальному об'єкті, або передати неправильну кількість аргументів у функцію чи метод. Тобто, якщо у замоканій функції зміняться аргументи, ваші тести продовжать бути зеленими, хоча реальний код буде падати з помилкою. Якщо мокаєте щось то робіть це зі спеціфікацією
3. Не використовувати фабрику для створення обʼєктів, сервісів, тощо
У кожному тесті створювати обʼєкт напряму через його конструктор, ще й кожного разу окремо мокати його залежності. Натомість краще зробити окрему функцію що буде інстанціювати обʼєкт, або скористатись паттерном Object Mother
4. Вказувати у сетапі неважливі для цього тесту аргументи
Це є наслідком попереднього пункту, коли ви інстанціюєте обʼєкт без фабрики у котрої вже задані дефолтні аргументи то не завжди зрозуміло які саме дані важливі для цього тесту. Ось наприклад:
def test_display_track_author_unknown_if_none():
sut = Track(
name="Icky Thump",
year=2007,
album="Icky Thump",
artist=None
)
result = sut.for_display() # Icky Thump - Icky Thump (2007) - Unknown Artist
assert result.endswith(" - Unknown Artist")
У Track усі аргументи є обовʼязковими, тож ми не можемо інстанціювати його лише із важлими для цього тесту аргументами. І це лише синтетичний приклад, у реальних проєктах аргументів може бути значно більше, до того ж частина з них може бути іншими сервісами котрі також треба інстанціювати. Було б краще якби ми таки мали фабріку для цього:
def make_track(
name: str = "Icky Thump",
year: int = 2007,
album: str = "Icky Thump",
artist: str | None = None,
) -> Track:
return Track(name=name, year=year, album=album, artist=artist)
def test_display_track_author_unknown_if_none():
sut = make_track(artist=None)
result = sut.for_display()
assert result.endswith(" - Unknown Artist")
У такому разі сетап тесту став значно простіше і ми інстанціюємо обʼєкт лише з важливими даними для нашого тесту
5. Не вказувати у сетапи важливі для цього тесту аргументи
Це якби ми у минулому прикладі замість sut = make_track(artist=None) написали sut = make_track(), адже всеодно у фабриці artist й так по-замовчуванню None. Так робити не треба бо знову ж таки не ясно які дані для цього тесту важливі, а які ні. Вказуючи artist=None явно ми показуємо від яких даних залежить результат
6. Відзеркалювати структуру проєкта у структурі тестів
Дуже часто бачу що струкруту проєкта відзеркалюють у тестах, тобто якщо те що ви тестуєте має шлях app.music_library.services.tracks.suggestions.SuggestionService то класти тест для цього тесту по такому самому шляху у модулі tests не треба. Це а) не зручно, б) якщо ви будете змінювати структуру діректорій проєкта, вам доведеться те саме зробити і з модулем тестів (але звісно ви про це забудете і все стане ще гірше)
Гарний приклад того як треба робити це Django - у них пласка структура модуля tests у якій зручно орієнтуватись
7. Назви тестів які не дають представлення про те що ми тестуємо
Ну тут все зрозуміло. Це якби у прикладі із автором треку тест би називався test_unknown_artist, або взагалі по назві методу test_for_display
Лінки:
- https://docs.python.org/3/library/unittest.mock.html#unittest.mock.create_autospec
- https://martinfowler.com/bliki/ObjectMother.html
- https://github.com/django/django/tree/main/tests