Разделы портала

Онлайн-тренинги

.
Как QA-инженер может влиять на unit-тесты
08.11.2022 00:00

Привет! Меня зовут Алёна Луцик, я QA-инженер в команде Авито. За время работы я много раз убеждалась, что разработчик и тестировщик смотрят на код по-разному. 

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

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

Примеры будут приведены в рамках микросервисной архитектуры на языке Golang.

Что поможет QA-инженеру договориться с разработчиками

Одна из целей QA-инженера — обеспечить высокое качество продукта без избыточного тестирования. Это не значит, что придётся писать все автотесты своими руками — важно понимать, какая функциональность уже проверяется ими на базовом уровне. Для этого нужно правильно выстроить коммуникацию с разработчиками. На примере покажу, как это сделать.

Предположим, вы QA-инженер. У вас есть задача: нужно продумать покрытие новой фичи автотестами. Вы понимаете, какие тесты вам надо покрыть на уровне e2e-тестов, а какие сценарии покрыть компонентными и интеграционными. 

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

Шапка QA-инженера


Шапка QA-инженера

Теперь посмотрим на ситуацию со стороны разработчика. Он спокойно делает свою работу: пишет код, покрывает его unit-тестами. И тут к нему приходит ваш коллега-тестировщик с вопросами. Разработчик не всегда понимает, зачем тратить время на объяснения — всё же и так хорошо работает. 

Шапка разработчика


Шапка разработчика

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

Шаг 1: проговаривать мотивацию

Мотивация QA-инженера — понять, что находится в основании пирамиды. Но для других членов команды это не очевидно. Поэтому нужно объяснить им, что вы хотите от unit-тестов и зачем.

Что проговорить коллеге-разработчику:

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

Что проговорить команде:

Я трачу время на понимание, чтобы не тратить его на написание избыточных тестов.

Шаг 2: научиться читать код 

Это важный навык для QA-инженера. Можно пройти курсы по языку программирования, который используют в команде. Кроме этого, нужно разбираться, что делает конкретный участок кода. 

На курсах разработки задачи обычно сформулированы прозрачно и код решения кажется понятным. Но в реальных проектах всё сложнее. Обсудите с коллегой из разработки, что делает каждая функция и unit-тест для неё. Тогда станет понятнее, какие тесты можно улучшить с точки зрения бизнес-процесса.

Это самый долгий и сложный этап. Его результатом должен быть ответ на вопрос:

«Что мы можем покрыть на unit-тестах?»

Шаг 3: добавить обсуждение unit-тестов в процесс

Тестировщик смотрит на функцию как на часть бизнес-процесса, а разработчик отвечает за внутреннюю логику её работы. На стыке этих задач появляются полезные unit-тесты. Поэтому обсуждать их лучше на этапе, когда создаётся функциональность или формулируется задача. Тогда QA-инженеру становится понятнее, что можно покрыть на уровне unit-тестов, а что нужно проверять на следующих этапах.

При тестировании иногда полезно «двигаться влево» — подключаться к разработке как можно раньше

При тестировании иногда полезно «двигаться влево» — подключаться к разработке как можно раньше

Не важно, используете ли вы TDD или нет, важно, что вы поговорили об этом до начала работы.

Каких результатов добилась моя команда

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

Увеличилось покрытие негативных сценариев 

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

До: 2 позитивных теста, 2 негативных теста, покрытие 73.3%. 
После: 2 позитивных теста, 9 негативных тестов, покрытие 95.0%

До: 2 позитивных теста, 2 негативных теста, покрытие 73.3%. После: 2 позитивных теста, 9 негативных тестов, покрытие 95.0%

Вам не обязательно читать код в примере — достаточно видеть цветовую дифференциацию. Зелёным обозначен код, который был вызван тестами, красным — пропущенный.

При этом для QA-инженера важно узнать, что именно не работает. Поэтому нужно участвовать в код-ревью и менять unit-тесты, если требуется.

Увеличилось покрытие dataset

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

Данные для тестирования функции Filter_Int

Данные для тестирования функции Filter_Int

Улучшилось нахождение сценариев для деградации

Чтобы понять, как найти сценарии деградации, возьмём гипотетические «Наш сервис» и «Другой сервис» — они взаимодействуют друг с другом и обмениваются данными. Причём «Другой сервис» — это фронт-слой, его видят пользователи. «Наш сервис» — промежуточный, он подключается к базе данных, ещё одному нашему сервису и соседнему. Затем собирает их ответы и передаёт на фронт-слой.

На схеме мы любую ошибку передаём выше в чистом виде с кодом 500


На схеме мы любую ошибку передаём выше в чистом виде с кодом 500

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

На этой схеме мы уже задеградировали два сценария

На этой схеме мы уже задеградировали два сценария

От базы данных «Наш сервис» получает информацию, без которой невозможно продолжить пользовательский сценарий. Он передаёт «Другому сервису» ошибку. А данные от ещё одного нашего сервиса не так важны для сценария. «Наш сервис» возвращает «Другому сервису» пустой массив и идёт дальше. Это считается деградацией сценария.

Пример из жизни Авито:

Когда создаётся объявление, на экране появляется аватарка пользователя, который его размещает. Если эта аватарка по какой-то причине не прогружается, нет смысла останавливать прорисовку экрана и передавать на фронт ошибку. Для пользовательского сценария полезнее продолжить работу без аватарки. 

QA-инженер может заметить подобные сценарии на этапе unit-тестирования. Если в автотесте у разработчика функция возвращает ошибку и останавливается, стоит уточнить у него, почему так происходит. Возможно, с точки зрения бизнес-логики лучше обработать ошибку как-то иначе или деградировать сценарий. 

Как я отслеживаю unit-тесты у себя в команде

Кроме общения с разработчиками, QA-инженеру было бы хорошо посчитать общее число unit-тестов. В нашей компании есть готовое решение по выгрузке автотестов из кода в тестохранилку, где реализована тестовая модель с фичами, к которым крепятся автотесты. Подробнее про механизм выгрузки автотестов из кода в хранилище можно посмотреть в докладе моего коллеги. 

Все unit-тесты в нашей команде отмечаются комментарием и tagID. В комментарии понятно и кратко описано, что именно проверяет этот тест, а tagID показывает, к какой фиче будет прикреплён тест. 

Пример позитивного unit-теста для SomeFunc:

// tagID 4637 Параметры. Позитивный тест получения параметров
func Test_SomeFunc_Positive(t *testing.T) {
    contrl := gomock.NewController (t)
    defer ctrl.Finish ()
    categoryID := map[string]interface{}{"first": 123}

    composition := NewMockComposition(ctrl)

    result, err := composition.GetRequestFormByParams(context.Background(), categoryID)

    assert.Nil(t, err)
    assert. Equal(t, expexted: 123, result)
}

Тесты выгружаются во внутреннее хранилище, которое автоматически считает их число. В нём учитываются unit-тесты, интеграционные, компонентные и UI-тесты. Здесь же можно посмотреть сгенерированную пирамиду тестирования: сколько проверок на каждом уровне у выбранной фичи и какое общее тестовое покрытие.

Зелёные блоки показывают процент покрытого автотестами кода, красные — непокрытого


Зелёные блоки показывают процент покрытого автотестами кода, красные — непокрытого

Вывод

Мой подход к обсуждению помог повысить понимание unit-тестов, поэтому я советую QA-инженерам попробовать его в своей работе. Напомню, что нужно делать: 

  • Объяснять коллегам из разработки, почему для вас важно разбираться в unit-тестах.

  • Учиться читать код и стараться понять, какой именно кейс покрыт.

  • Заранее проговаривать, какую функциональность можно протестировать на уровне unit.

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

Обсудить в форуме