Slide 1

Slide 1 text

Анализируем исходный код с пользой или как облегчить работу ревьюерам Сосновских Кирилл

Slide 2

Slide 2 text

• Работаю в YADRO автоматизатором тестирования в отделе TATLIN.OBJECT • Пишу на Python, знаю что такое AST, вкусный мармелад и как написать свой линтер Кирилл Сосновских
 Automation QA


Slide 3

Slide 3 text

3 О какой рутине речь? С этим сталкивался каждый… Review Merge Code

Slide 4

Slide 4 text

4 О какой рутине речь? Review Merge Но у меня немного иначе Code Test Report Test Storag e

Slide 5

Slide 5 text

5 @allure.title("Create bucket with valid name length") @pytest.mark.parametrize("length", [3, 32, 63]) def test_s3_create_bucket_with_valid_length( self, s3_client: S3ClientWrapper, length: int, ): bucket_name = string_utils.random_string( length, VALID_SYMBOLS_WITHOUT_DOT, ) with allure.step("Create bucket with valid name length"): s3_client.create_bucket(bucket_name) with allure.step("Check bucket name in buckets"): assert bucket_name in s3_client.list_buckets() Принятые в команде конвенции Рассмотрим на примере

Slide 6

Slide 6 text

6 @allure.title("Create bucket with valid name length") @pytest.mark.parametrize("length", [3, 32, 63]) def test_s3_create_bucket_with_valid_length( self, s3_client: S3ClientWrapper, length: int, ): bucket_name = string_utils.random_string( length, VALID_SYMBOLS_WITHOUT_DOT, ) with allure.step("Create bucket with valid name length"): s3_client.create_bucket(bucket_name) with allure.step("Check bucket name in buckets"): assert bucket_name in s3_client.list_buckets() Принятые в команде конвенции Заголовок всему голова

Slide 7

Slide 7 text

7 @allure.title("Create bucket with valid name length") @pytest.mark.parametrize("length", [3, 32, 63]) def test_s3_create_bucket_with_valid_length( self, s3_client: S3ClientWrapper, length: int, ): bucket_name = string_utils.random_string( length, VALID_SYMBOLS_WITHOUT_DOT, ) with allure.step("Create bucket with valid name length"): s3_client.create_bucket(bucket_name) with allure.step("Check bucket name in buckets"): assert bucket_name in s3_client.list_buckets() Принятые в команде конвенции Был один – стало три

Slide 8

Slide 8 text

8 @allure.title("Create bucket with valid name length") @pytest.mark.parametrize("length", [3, 32, 63]) def test_s3_create_bucket_with_valid_length( self, s3_client: S3ClientWrapper, length: int, ): bucket_name = string_utils.random_string( length, VALID_SYMBOLS_WITHOUT_DOT, ) with allure.step("Create bucket with valid name length"): s3_client.create_bucket(bucket_name) with allure.step("Check bucket name in buckets"): assert bucket_name in s3_client.list_buckets() Принятые в команде конвенции Пора программировать

Slide 9

Slide 9 text

9 @allure.title("Create bucket with valid name length") @pytest.mark.parametrize("length", [3, 32, 63]) def test_s3_create_bucket_with_valid_length( self, s3_client: S3ClientWrapper, length: int, ): bucket_name = string_utils.random_string( length, VALID_SYMBOLS_WITHOUT_DOT, ) with allure.step("Create bucket with valid name length"): s3_client.create_bucket(bucket_name) with allure.step("Check bucket name in buckets"): assert bucket_name in s3_client.list_buckets() Принятые в команде конвенции По шажку

Slide 10

Slide 10 text

10 @allure.title("Create bucket with valid name length") @pytest.mark.parametrize("length", [3, 32, 63]) def test_s3_create_bucket_with_valid_length( self, s3_client: S3ClientWrapper, length: int, ): bucket_name = string_utils.random_string( length, VALID_SYMBOLS_WITHOUT_DOT, ) with allure.step("Create bucket with valid name length"): s3_client.create_bucket(bucket_name) with allure.step("Check bucket name in buckets"): assert bucket_name in s3_client.list_buckets() Представим, что мы разработчик Это я написал!

Slide 11

Slide 11 text

11 Allure Report: test_s3_create_bucket_with_valid_length Фиксируем результат А почему их шесть?

Slide 12

Slide 12 text

12 Фиксируем результат Хранилище тестов Allure Report TestY TMS Test Managmen t

Slide 13

Slide 13 text

13 Фиксируем результат Кажется что-то не так… TestY TMS: test_s3_create_bucket_with_valid_length

Slide 14

Slide 14 text

14 @allure.title( "Create bucket with valid name length (s3_client={s3_client}, length={length})" ) @pytest.mark.parametrize("length", [3, 32, 62]) def test_s3_create_bucket_with_valid_length( self, s3_client: S3ClientWrapper, length: int, ): bucket_name = string_utils.random_string( length, VALID_SYMBOLS_WITHOUT_DOT, ) with allure.step("Create bucket with valid name length"): s3_client.create_bucket(bucket_name) with allure.step("Check bucket name in buckets"): assert bucket_name in s3_client.list_buckets() Отчаянно исправляем Ну сейчас то точно все будет хорошо!

Slide 15

Slide 15 text

15 Фиксируем результат 2 Черное лучше серого! Allure Report: test_s3_create_bucket_with_valid_length

Slide 16

Slide 16 text

16 Фиксируем результат 2 Скажем нет дублированию! TestY TMS: test_s3_create_bucket_with_valid_length

Slide 17

Slide 17 text

17 А кто виноват? Проблема в ревью-процессе? Расследование…

Slide 18

Slide 18 text

18 Ревьюер? Проблема в ревью-процессе? Версия номер 1

Slide 19

Slide 19 text

19 Разработчик? Проблема в ревью-процессе? Версия номер 2

Slide 20

Slide 20 text

20 Люди – не роботы, человеческий фактор неизбежен Проблема в ревью-процессе? Висяк!

Slide 21

Slide 21 text

21 Пусть рутиной занимается робот! Разве может робот написать симфонию… Его обязанности: • Анализировать исходный код • Находить проблемные по нашему мнению места в коде • Бить разработчика по рукам • Не просить зарплату

Slide 22

Slide 22 text

22 @allure.title( "Create bucket with valid name length " "(s3_client={s3_client}, length={length})" ) Код как данные Знакомый нам заголовок

Slide 23

Slide 23 text

23 @allure.title( "Create bucket with valid name length " "(s3_client={s3_client}, length={length})" ) Код как данные Обобщим! @allure.title( "Заголовок описывающий тест (p1={p1}, p2={p2}, ...)" )

Slide 24

Slide 24 text

24 @pytest.mark.parametrize("length", [3, 32, 63]) Код как данные Знакомая нам параметризация

Slide 25

Slide 25 text

25 @pytest.mark.parametrize("length", [3, 32, 63]) Код как данные Обобщим и её! @pytest.mark.parametrize("p1, p2, ...", [V1, V2, ...])

Slide 26

Slide 26 text

26 def length_parametrizer() -> list[int]: # Очень сложный код return lengths @pytest.mark.parametrize("length", length_parametrizer()) def test_s3_create_bucket_with_valid_length(...): ... Как еще может выглядеть параметризация? Это стоит учесть

Slide 27

Slide 27 text

27 def pytest_generate_tests(metafunc: pytest.Metafunc): if "length" in metafunc.fixturenames: metafunc.parametrize("length", [3, 32, 63]) def test_s3_create_bucket_with_valid_length( self, s3_client: S3ClientWrapper, length: int, ): ... Как еще может выглядеть параметризация? Вариантов много!

Slide 28

Slide 28 text

28 def pytest_generate_tests(metafunc: pytest.Metafunc): # Очень сложный код if "length" in metafunc.fixturenames: metafunc.parametrize("length", lengths) def test_s3_create_bucket_with_valid_length( self, s3_client: S3ClientWrapper, length: int, ): ... Как еще может выглядеть параметризация? Совместим оба кейса!

Slide 29

Slide 29 text

29 @allure.title( "Create bucket with valid name length (s3_client={s3_client}, length={length})" ) @pytest.mark.parametrize("length", [3, 32, 63]) def test_s3_create_bucket_with_valid_length( self, s3_client: S3ClientWrapper, length: int, ): bucket_name = string_utils.random_string( length, VALID_SYMBOLS_WITHOUT_DOT, ) with allure.step("Create bucket with valid name length"): s3_client.create_bucket(bucket_name) with allure.step("Check bucket name in buckets"): assert bucket_name in s3_client.list_buckets() Код как данные Сложновато!

Slide 30

Slide 30 text

30 @allure.title( "Create bucket with valid name length (s3_client={s3_client}, length={length})" ) @pytest.mark.parametrize("length", [3, 32, 63]) def test_s3_create_bucket_with_valid_length( self, s3_client: S3ClientWrapper, length: int, ): bucket_name = string_utils.random_string( length, VALID_SYMBOLS_WITHOUT_DOT, ) with allure.step("Create bucket with valid name length"): s3_client.create_bucket(bucket_name) with allure.step("Check bucket name in buckets"): assert bucket_name in s3_client.list_buckets() Заголовок наш выбор! И все же заголовок всему голова!

Slide 31

Slide 31 text

31 @allure.title( "Create bucket with valid name length (s3_client={s3_client}, length={length})" ) @pytest.mark.parametrize("length", [3, 32, 63]) def test_s3_create_bucket_with_valid_length( self, s3_client: S3ClientWrapper, length: int, ): bucket_name = string_utils.random_string( length, VALID_SYMBOLS_WITHOUT_DOT, ) with allure.step("Create bucket with valid name length"): s3_client.create_bucket(bucket_name) with allure.step("Check bucket name in buckets"): assert bucket_name in s3_client.list_buckets() Что за параметр такой? Опять что-то упустили…

Slide 32

Slide 32 text

32 @allure.title("[Session]: Create S3 client") @pytest.fixture( scope="session", params=[ pytest.param(AwsCliClient, marks=...), pytest.param(Boto3ClientWrapper, marks=...), ], ) def s3_client(...) -> S3ClientWrapper: # Создание и подготовка S3 клиента return client Немного о фикстурах Что-то новенькое!

Slide 33

Slide 33 text

33 @allure.title("[Session]: Create S3 client") @pytest.fixture( scope="session", params=[ pytest.param(AwsCliClient, marks=...), pytest.param(Boto3ClientWrapper, marks=...), ], ) def s3_client(...) -> S3ClientWrapper: # Создание и подготовка S3 клиента return client Немного о фикстурах Снова параметризация?

Slide 34

Slide 34 text

34 def fixture_parametrizer() -> list: # Очень сложный код return data @allure.title("[Session]: Create S3 client") @pytest.fixture( scope="session", params=fixture_parametrizer(), ) def s3_client(...) -> S3ClientWrapper: # Создание и подготовка S3 клиента return client Немного о фикстурах Знакомое явление…

Slide 35

Slide 35 text

35 Abstract Syntax Tree Тут и думать нечего – будет 2! r = (6 + 4) // 5

Slide 36

Slide 36 text

36 Abstract Syntax Tree Какое ж это дерево без корня? root r = (6 + 4) // 5

Slide 37

Slide 37 text

37 Abstract Syntax Tree root = r (6 + 4) // 5 Все как в математике

Slide 38

Slide 38 text

38 Abstract Syntax Tree root = // r 5 6 + 4 Соблюдаем приоритетность операций!

Slide 39

Slide 39 text

39 Abstract Syntax Tree Листья распустились root = // r + 5 6 4

Slide 40

Slide 40 text

40 import ast 
 source_code = """ @allure.title(...) @pytest.mark.parametrize("length", [3, 32, 63]) def test_s3_create_bucket_with_valid_length( self, s3_client: S3ClientWrapper, length: int ): ... """ 
 module = ast.parse(source_code) print(ast.dump(module, indent=4)) От слов к действиям Наконец-то!

Slide 41

Slide 41 text

41 Реальная структура AST Не корень, а модуль! Module body ... ... ...

Slide 42

Slide 42 text

42 Реальная структура AST Не корень, а модуль! Module body FunctionDef

Slide 43

Slide 43 text

43 Декомпозируем пример Еще немного теории… FunctionDef name args body decorator_list ... ... ... ... ... ... ... ... ...

Slide 44

Slide 44 text

44 FunctionDef( name='test_s3_create_bucket_with_valid_length', args=arguments( posonlyargs=[], args=[ arg(arg='self'), arg(arg='s3_client', annotation=Name(id='S3ClientWrapper', ctx=...)), arg(arg='length', annotation=Name(id='int', ctx=...)) ], kwonlyargs=[], kw_defaults=[], defaults=[] ), body=[...], decorator_list=[...], type_params=[] ) Тест как объект И это только один тест…

Slide 45

Slide 45 text

45 FunctionDef( name='test_s3_create_bucket_with_valid_length', args=arguments( posonlyargs=[], args=[ arg(arg='self'), arg(arg='s3_client', annotation=Name(id='S3ClientWrapper', ctx=...)), arg(arg='length', annotation=Name(id='int', ctx=...)) ], kwonlyargs=[], kw_defaults=[], defaults=[] ), body=[...], decorator_list=[...], type_params=[] ) Тест как объект Имя теста

Slide 46

Slide 46 text

46 FunctionDef( name='test_s3_create_bucket_with_valid_length', args=arguments( posonlyargs=[], args=[ arg(arg='self'), arg(arg='s3_client', annotation=Name(id='S3ClientWrapper', ctx=...)), arg(arg='length', annotation=Name(id='int', ctx=...)) ], kwonlyargs=[], kw_defaults=[], defaults=[] ), body=[...], decorator_list=[...], type_params=[] ) Тест как объект Аргументы теста

Slide 47

Slide 47 text

47 FunctionDef( name='test_s3_create_bucket_with_valid_length', args=arguments( posonlyargs=[], args=[ arg(arg='self'), arg(arg='s3_client', annotation=Name(id='S3ClientWrapper', ctx=...)), arg(arg='length', annotation=Name(id='int', ctx=...)) ], kwonlyargs=[], kw_defaults=[], defaults=[] ), body=[...], decorator_list=[...], type_params=[] ) Тест как объект Список декораторов

Slide 48

Slide 48 text

48 decorator_list=[ Call(...), # allure.title(...) Call( func=Attribute( value=Attribute( value=Name(id='pytest', ctx=Load()), attr='mark', ctx=Load() ), attr='parametrize', ctx=Load() ), args=[ Constant(value='length'), List( elts=[Constant(value=3), Constant(value=32), Constant(value=63)], ctx=Load() ) ], keywords=[] ) ], Декораторы теста Немного углубимся

Slide 49

Slide 49 text

49 Call( func=Attribute( value=Attribute( value=Name(id='pytest', ctx=Load()), attr='mark', ctx=Load() ), attr='parametrize', ctx=Load() ), args=[ Constant(value='length'), List( elts=[Constant(value=3), Constant(value=32), Constant(value=63)], ctx=Load() ) ], keywords=[] ) Декомпозируем декоратор Имя декоратора

Slide 50

Slide 50 text

50 Call( func=Attribute( value=Attribute( value=Name(id='pytest', ctx=Load()), attr='mark', ctx=Load() ), attr='parametrize', ctx=Load() ), args=[ Constant(value='length'), List( elts=[Constant(value=3), Constant(value=32), Constant(value=63)], ctx=Load() ) ], keywords=[] ) Декомпозируем декоратор Параметр и его значения

Slide 51

Slide 51 text

51 Что делать с хуками? Как же много всего нужно учесть! Все просто, но требуются допущения: • Будем анализировать только pytest_generate_tests хуки • Не будем анализировать логику самого хука • Будем учитывать все параметры независимо от их использования в тестах • Перечисленные в хуке параметры будем считать фикстурами

Slide 52

Slide 52 text

52 А с фикстурами что? Кажется все не так просто… Аналогично тестам, но есть нюансы: • Фикстуры могут быть где угодно • Фикстуры могут переопределять друг друга • Фикстуры могут быть зависимы от других фикстур • Фикстуры могут быть определены во внешних плагинах

Slide 53

Slide 53 text

53 Этапы работы линтера Что анализируем? Сбор исходных файлов Сбор AST объектов Определени е связей Валидация

Slide 54

Slide 54 text

54 Этапы работы линтера Что анализируем? Сбор исходных файлов Сбор AST объектов Определени е связей Валидация • Путей к файлам с тестами • Путей к файлам с фикстурами • Путей к файлам плагинов

Slide 55

Slide 55 text

55 Этапы работы линтера Формируем данные удобным для нас образом Сбор исходных файлов Сбор AST объектов Определени е связей Валидация • Представляющих тесты • Представляющих фикстуры • Представляющих pytest-хуки

Slide 56

Slide 56 text

56 Этапы работы линтера Получаем единый контекст Сбор исходных файлов Сбор AST объектов Определени е связей Валидация • Для зависимых друг от друга фикстур • Для тестов с их параметрами и фикстурами, в том числе динамическими

Slide 57

Slide 57 text

57 Этапы работы линтера Находим проблемы Сбор исходных файлов Сбор AST объектов Определени е связей Валидация • Уникальности заголовков • Заполненности заголовков • Наличия или отсутствия в заголовках необходимых параметров

Slide 58

Slide 58 text

58 Смотрим что получилось Как запустить? allure-validator <путь к тестам> [--plugins, --files]

Slide 59

Slide 59 text

59 Смотрим что получилось Даже запускаться само будет! allure-validator <путь к тестам> [--plugins, --files] - repo: https://git.frostfs.info/TrueCloudLab/allure-validator rev: <ревизия> hooks: - id: allure-validator args: [ "<путь к тестам>", ["--plugins ...", "--files ..."] ] или

Slide 60

Slide 60 text

60 Смотрим что получилось Опциональные параметры allure-validator <путь к тестам> [--plugins, --files] - repo: https://git.frostfs.info/TrueCloudLab/allure-validator rev: <ревизия> hooks: - id: allure-validator args: [ "<путь к тестам>", ["--plugins ...", "--files ..."] ] или

Slide 61

Slide 61 text

61 Смотрим что получилось Уникальные и непустые заголовки NOT UNIQUE TITLE: Title for two tests In the following tests: tests/test_validator.py:127: title is not unique by test_not_unique_2 tests/test_validator.py:122: title is not unique by test_not_unique_1 tests/test_validator.py:104: EMPTY TITLE: Missing title in test_empty_title_1 tests/test_validator.py:109: EMPTY TITLE: Missing title in test_empty_title_2

Slide 62

Slide 62 text

62 Смотрим что получилось Неуникальные и пустые заголовки NOT UNIQUE TITLE: Title for two tests In the following tests: tests/test_validator.py:127: title is not unique by test_not_unique_2 tests/test_validator.py:122: title is not unique by test_not_unique_1 tests/test_validator.py:104: EMPTY TITLE: Missing title in test_empty_title_1 tests/test_validator.py:109: EMPTY TITLE: Missing title in test_empty_title_2

Slide 63

Slide 63 text

63 Смотрим что получилось Неуникальные и пустые заголовки NOT UNIQUE TITLE: Title for two tests In the following tests: tests/test_validator.py:127: title is not unique by test_not_unique_2 tests/test_validator.py:122: title is not unique by test_not_unique_1 tests/test_validator.py:104: EMPTY TITLE: Missing title in test_empty_title_1 tests/test_validator.py:109: EMPTY TITLE: Missing title in test_empty_title_2

Slide 64

Slide 64 text

64 Смотрим что получилось Неуникальные и пустые заголовки NOT UNIQUE TITLE: Title for two tests In the following tests: tests/test_validator.py:127: title is not unique by test_not_unique_2 tests/test_validator.py:122: title is not unique by test_not_unique_1 tests/test_validator.py:104: EMPTY TITLE: Missing title in test_empty_title_1 tests/test_validator.py:109: EMPTY TITLE: Missing title in test_empty_title_2

Slide 65

Slide 65 text

65 Смотрим что получилось Пропущенные параметры tests/test_validator.py:53: MISSING PARAMS: Parameters are missing from title: required_runtime_fixture tests/test_validator.py:127: MISSING PARAMS: Parameters are missing from title: object_size tests/test_validator.py:152: MISSING PARAMS: Parameters are missing from title: p1 OR p2 OR p3

Slide 66

Slide 66 text

66 Смотрим что получилось Пропущенные параметры tests/test_validator.py:53: MISSING PARAMS: Parameters are missing from title: required_runtime_fixture tests/test_validator.py:127: MISSING PARAMS: Parameters are missing from title: object_size tests/test_validator.py:152: MISSING PARAMS: Parameters are missing from title: p1 OR p2 OR p3

Slide 67

Slide 67 text

67 Смотрим что получилось Пропущенные параметры tests/test_validator.py:53: MISSING PARAMS: Parameters are missing from title: required_runtime_fixture tests/test_validator.py:127: MISSING PARAMS: Parameters are missing from title: object_size tests/test_validator.py:152: MISSING PARAMS: Parameters are missing from title: p1 OR p2 OR p3

Slide 68

Slide 68 text

68 А если что-то сломается? И такое учли! def test_something(...): # noqa: allure-validator ... def some_fixture(): # noqa: allure-validator ... class TestS3GateBucket: # noqa: allure-validator ...

Slide 69

Slide 69 text

69 • Найдено и исправлено около 50 проблемных тестов • Используется в двух объемных тестовых репозиториях • Интегрирован как хук в pre-commit • Можно запускать руками • Практически нет накладных расходов Интеграция в команду

Slide 70

Slide 70 text

70 • Ревью стало проще и быстрее • Соблюдение стандартов команды • Улучшение качества тестовой отчетности и опыта работа с TestY Что мы получили?

Slide 71

Slide 71 text

71 • Время на поддержку дополнительного инструмента • Периодические поломки и частичные блокировки в работе Что мы отдаем взамен?

Slide 72

Slide 72 text

72 Посмотреть, послушать и попробовать TestY Repo TestY YouTube Linter Repo