Всем привет! Работаю QA аналитиком на достаточно крупном проекте в области WEB и мобильных приложений. Программистом не являюсь, но периодически скрипты для себя пишу, поэтому за качество кода прошу строго не судить. Но сегодня хочется принести немного data science в жизнь тестировщиков и показать, что если не куплены средства автоматического анализа, то кое-что можно написать и самому.
Недавно столкнулся с задачей разбора багов прилетающих с прода. Не то, чтобы такого процесса не было заведено, но обычно внимание уделялось только самым приоритетным.
С точки зрения команды QA же было интересно узнать больше о нашем процессе регрессионного тестирования. Трудность проекта состоит в большом количестве подключаемых функций и кастомизаций. Количество Feature Flags подключающих те или иные опции стремится к 700. Регресс выполняется еженедельно, успеть протестировать всё возможности нет.
Отсюда интерес к тому, на что регулярно жалуются наши клиенты. Задача найти пробелы в тестировании через поиск "забаженных" областей приложений. Согласитесь, иногда большое количество мелких багов бесит не меньше серьезных просчётов.
Количество багов/вопросов от клиентов варьируется от 10 до 25 в месяц на команду. Команд - 15. Поставил задачу выделить группы проблем прилетевших в последний год для каждой из команд. Сразу скажу, что индексация по темам в ходе работы над проблемами имеется, но очень скромная. Можно сказать, что индексация проводится по желанию, каждая команда расставляет (или нет) лейблы по своему желанию. Отсюда проблема связи багов по функционалу. Более-менее понятно в Jira только то, к какой команде относится проблема.
Итак поехали. Что будем анализировать? В моем случае это заголовки (summary) багов из Jira. Стиль описания историй хромает, кто-то оформляет через картинки или видео, кто-то забывает описание и добавляет только acceptance criteria. Собрать воедино все это сложно. Комментариев к багам много, а название бага все оформляют на приемлемом уровне.
Импортируем требуемые библиотеки и вытащим из Jira ID багов и их названия
from nltk.stem import PorterStemmer
from nltk.tokenize import word_tokenize
import requests
import json
from pyvis.network import Network
def find_jira_issues (team, startAt, result ={}):
jql = (f'"Team[Dropdown]" = "{team}" AND "created >= -50w"') # можно задать любойо jql запрос валидный для Jira
headers = {
"Accept": "application/json",
"Content-Type": "application/json"
}
payload = json.dumps( {
"expand": [
"names"
],
"fields": [
"summary"
],
"fieldsByKeys": False,
"jql": jql,
"maxResults": 100,
"startAt": startAt
} )
response = requests.request("POST", url, data=payload, headers=headers,
auth=(jira_user, jira_pass)).json()
for issue in response["issues"]:
result.update({issue["key"]:issue["fields"]["summary"]})
if startAt + 100 < response["total"]:
startAt =+ 99
find_jira_escalated (team,startAt, result)
return (result)
На выходе получаем словарь багов для конкретной команды.
Далее придется очистить описание багов от названий клиентов, которые будут давать совпадения и вводить в заблуждение по отношению к сути проблем.
def remove_clients(str):
toremove = [] # здесь следует задать список клиентов, который можно вытащить из базы данных и немного откорректировать по неофициальным названиям
for i in toremove:
str = str.replace(i,"")
return(str)
Очищаем либо заменяем спецсимволы и, возможно, приводим к общему различные словосочетания
def remove_symbols (str):
toremove = ['!', '-', ';' , "'"]
toreplace = ['(', ')', '$', '@', '%', '_', '&', '/' , '|', '#', '*', '"', ',', ':']
toupdate = ['work order', 'work orders']
for i in toremove:
str = str.replace(i,"")
for i in toreplace:
str = str.replace(i," ")
for i in toupdate:
str = str.replace(i,"wo")
return (str)
Так же удаляем часто повторяющиеся слова и числа, которые не относятся к описанию сути, например issue , bug и т.д. На фоне небольших по размерам текстов в 3 - 10 слов любое совпадение будет сильно искажать связь проблем между собой.
def remove_words (str) :
todrop = ['error', 'issue', 'and', 'or', 'is',
'are', 'the', 'a', 'subid',
'to', 'for', 'on', 'sub', 'provider', 'subscriber',
'does' , 'not', 'proid', 'incorrect', 'id', 'via', 'when']
res = [i for i in str if (i not in todrop)and not(i.isnumeric())]
return (res)
Для окончательной подготовки текста остается привести различные формы одного и того же слова к единому варианту. Процесс известен как лемматизация. Подробнее тут.
Мы будем использовать стандартную готовую библиотеку. Наш основной скрипт будет выглядеть так
ps = PorterStemmer()
issues = find_jira_issues("team_name", 0)
new_list = dict(issues) # сохраню копию данных, ещё понадобится
for i in issues :
issues[i] = remove_clients(issues[i].lower()) # приводим к единому регистру и удаляем клиентов
issues[i] = remove_symbols(issues[i]).split() # удаляем символы и разбиваем на отдельные слова для последующей работы
issues[i] = remove_words(issues[i]) #удаляем лишние слова
for z, item in enumerate(issues[i]):
issues[i][z] = ps.stem(item) # каждое из оставшихся слов приводим к единой лемме
Теперь мы готовы к сравнению заготовленных текстов названий багов. Рекомендую ознакомиться со статьёй для общего представления о проблеме сравнения текстов. Здесь кратко скажу, что сам выбрал коэффициент Жаккара. Для каждой пары багов буду вычислять связь текстов. Опытным путем нашел, что меня интересует любая пара с коэффициентом выше 0.2. Если пара подходит по критерию, то сохраняю ID в массивы для последующего построения графа. Один массив содержит только уникальные ID вершин графов, второй для связей вершин между собой.
def jaccard_similarity(x,y):
""" returns the jaccard similarity between two lists """
intersection_cardinality = len(set.intersection(*[set(x), set(y)]))
union_cardinality = len(set.union(*[set(x), set(y)]))
return intersection_cardinality/float(union_cardinality)
nodes = set()
edges = []
for i in new_list:
for z in new_list:
if int(i[5:]) > int(z[5:]) and (
jaccard_similarity(issues[i],issues[z]) > 0.2) :
nodes.update({i,z})
edges.append((i,z))
linked_bugs = {}
for n in nodes:
linked_bugs[n] = new_list[n]
Далее построим граф и получим результат связей в виде HTML файла.
net = Network(notebook = True)
list_nodes = list(new_nodes.keys())
list_titles = list(new_nodes.values())
net.add_nodes(list_nodes, title=list_titles)
net.add_edges(edges)
net.show('file_name.html')
Результат будет выглядеть примерно так.
Дальше по этой подсказке можно почитать и прикинуть масштаб трагедий самостоятельно.