Автоматическое создание тестовой документации на базе автотестов с использованием Python и QASE.io

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

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

Статья является продолжением ранее опубликованной на Habr статьи Еще одна инструкция о том, как с нуля начать писать UI автотесты на Python + Pytest + Playwright + QASE.io.

Как и первая статья эта будет написана под MacOS и на python версии 3.11.1. В качестве IDE выступает PyCharm Community Edition.

Весь код из данной статьи будет доступен в репозитории на GitHub.

Каких результатов планируется достичь?

  1. Шаги в тест-кейсах на QASE.io создаются и актуализируются автоматически

Инструкция

Создаем новый репозиторий в GitHub для хранения кода, который будет написан в рамках этой статьи

В первой части мы создали аккаунт в GitHub и научились создавать репозиторий. Теперь нам понадобится еще один. Я назову его article-two.

Только что созданный репозиторий article-two
Только что созданный репозиторий article-two

Пока он пустой. Нам необходимо его склонировать на рабочий компьютер. Ранее я говорил, что все склонированные репозитории я храню в директории Code. Так как статей теперь две, то я создал дополнительную директорию articles. Именно туда я и буду клонировать только что созданный репозиторий.

В моем случае команда на клонирование выглядит следующим образом:

cd Code/articles
git clone git@github.com:stepushchenko/article_two.git

Первая команда позволяет мне перейти из директории пользователя в директорию articles.

Вторая команда клонирует репозиторий на мой компьютер.

Структура директорий на компьютере
Структура директорий на компьютере

Теперь мы можем приступать к созданию нового проекта в PyCharm.

Создаем новый проект в PyCharm и переносим в него код, написанный в первой статье

Создавать новый проект в PyCharm мы учились в первой статье. В этот раз все сделаем аналогично.

Новый проект в PyCharm, созданный на базе склонированного с GitHub пустого репозитория.
Новый проект в PyCharm, созданный на базе склонированного с GitHub пустого репозитория.

Теперь нам необходимо скопировать файлы, которые мы создавали в рамках первой статьи и положить их в папку article_two, то есть в папку текущего проекта.

Для этого я просто захожу в Finder, выделяю файлы в папке article_one, нажимаю Command + C, открываю папку article_two и нажимаю Command + V.

Но есть важный нюанс. В папке article_one есть скрытые директории и файлы. Видеть их в Finder мы уже умеем (надо нажать Command + .). Нам важно не копировать директории .git, .idea и venv. Директория .git хранит данные о репозитории, с которого была склонирована папка. В папке article_two у нас такая папка уже есть. Потому что там другой репозиторий. Аналогичная история с папкой venv. Там хранятся данные по виртуальному окружению проекта. И так как проекты разные, то у каждого из них будет своя уникальная папка venv. Поэтому копируем все, кроме этих трех директорий.

Результат копирования файлов из директории article_one в article_two
Результат копирования файлов из директории article_one в article_two
Результат копирования файлов из директории article_one в article_two
Результат копирования файлов из директории article_one в article_two

Теперь в новом проекте в PyCharm есть все файлы из предыдущего проекта.

Важно отметить, что теперь нам необходимо добавить эти файлы в GitHub и затем установить все зависимости из requirements.txt. Делать это мы учились в первой статье.

После добавления в GitHub PyCharm перестанет окрашивать файлы в красный цвет
После добавления в GitHub PyCharm перестанет окрашивать файлы в красный цвет
В GitHub теперь отображаются файлы проекта
В GitHub теперь отображаются файлы проекта

Остается убедиться, что мы ничего не поломали в процессе переноса файлов. Для этого в терминале PyCharm запустим команду pytest .

Автотест завершился успешно
Автотест завершился успешно

Знакомимся с сутью проекта

В первой статье мы создали аккаунт в QASE.io. Там же создали первый свит и первый кейс.

Свит и кейс в QASE.io
Свит и кейс в QASE.io

Откроем кейс.

Страница редактирования кейса в QASE.io
Страница редактирования кейса в QASE.io

Пока мы заполнили только наименование кейса. И сделали мы это сами, вручную. Теперь мы можем создать шаги внутри этого кейса. Повторим те шаги, которые уже реализовали в автотесте.

Создание шагов в QASE.io
Создание шагов в QASE.io

Сохраним изменения в кейсе.

Кейс после добавления шагов и сохранения
Кейс после добавления шагов и сохранения

Все операции по редактированию кейса, внесению в него шагов мы делали вручную. Это не было сложно. Но, когда кейсов в документации становится много (300-500 штук, а часто и 1000+) работа по написанию шагов в каждом кейсе становится большой работой. Да, это не так уж сложно. Но рабочее время тратится.

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

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

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

Формируем "техническое задание" на проект

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

Наша цель: автоматически обновлять шаги в тест-кейсах QASE.

Чтобы достичь цели мы должны создать фикстуру со скоупом function. То есть фикстуру, которая будет "что-то" делать перед запуском автотеста и "что-то" делать после завершения автотеста.

До запуска автотеста фикстура будет создавать json файл, где будут собираться все шаги текущего автотеста.

Чтобы собрать информацию о каждом шаге в json файле напишем декоратор и обернем в него каждый шаг автотестов. Декоратор будет запускаться перед каждым шагом автотеста и добавлять в json файл информацию о шаге. Таким образом пока будет прогоняться автотест мы будем собирать информацию о каждом шаге автотеста в json файле.

После завершения автотеста продолжит выполняться фикстура. Она переработает собранную в json файле информацию в такой вид, чтобы она была понятна API QASE. А следующим шагом фикстура инициирует запрос к API QASE, который и обновит шаги в кейсе в QASE.io.

Звучит, кажется, сложно. Но если смотреть на итоговый код, который мы получим к завершению статьи, то там все вполне прозрачно.

Готовимся к интеграции с QASE.io

Интеграцию с QASE.io мы будем делать через API. Всю информацию будем получать из официального сайта с документацией по API QASE.

Чтобы актуализировать шаги в кейсе, потребуется метод Update test case, вот ссылка на страницу с описанием.

Что нам потребуется, чтобы воспользоваться этим методом?

  1. Token

  2. Код проекта

  3. ID кейса

Чтобы получить Token наводим мышку на свою аватарку в правом верхнем углу сайта QASE.io. Откроется меню. В нем выбираем пункт API Tokens.

На открывшейся странице нажимаем кнопку Create new API token. Вводим наименование. Можете в качестве наименования использовать имя репозитория в GitHub, которое вы используете для работы над этой статьей. Затем остается нажать кнопку Create.

Откроется окно с токеном. Скопируйте его и сохраните в надежном месте. Я сохраню в 1Password, чтобы всегда иметь к нему доступ.

Теперь о Коде проекта. Чтобы узнать код проекта, достаточно открыть страницу самого проекта.

Код проекта вы сможете увидеть как в самом URL, так и в заголовке
Код проекта вы сможете увидеть как в самом URL, так и в заголовке

И остается найти ID кейса. Мы говорили об этом в первой статье. Напомню, что найти его можно практически на любой странице QASE.io

Указание на ID кейса
Указание на ID кейса

Итого. Мы нашли Token, код проекта и ID кейса.

Знакомимся с API QASE

Чтобы убедиться в том, что все подготовленные нами данные корректны, мы воспользуемся страницей с методом Update test case.

Открываем ее и находим интересующие нас поля. Заполняем их.

Поля, которые необходимо заполнить полученными ранее данными
Поля, которые необходимо заполнить полученными ранее данными

После заполнения полей нам остается найти поле Steps. В нем нажимаем на кнопку Add object. В открывшейся форме нас интересуют три поля. Action, Data, Expected_result. Эти поля мы сейчас заполним.

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

В поле action пишем Action. В поле expected_result пишем Expected result. В поле data пишем Data.

Заполненные поля
Заполненные поля

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

Смотрим блок Response.

Результат выполнения запроса
Результат выполнения запроса

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

Состояние кейса после выполнения API запроса.
Состояние кейса после выполнения API запроса.

Как видим, данные в кейсе обновились в соответствии с теми, которые мы подготовили перед нажатием кнопки Try it.

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

Присваиваем автотестам ID из QASE

Для того, чтобы присвоить каждому автотесту ID из QASE воспользуемся возможностями pytest. А именно '@pytest.mark.___'. Полностью код будет выглядеть следующим образом:

import pytest
import pages


class TestFooter:

    @pytest.mark.case_id(1)
    def test_user_should_be_able_to_open_popup_select_subscription_plan(self, page):
        pages.index_page.open_index_page(page)
        pages.index_page.press_link_english_lang(page)
        actual_result = pages.index_page.get_text_from_google_search_button(page)
        assert actual_result == 'Google Search', 'Google Search button text is not correct'

Нас интересует строка номер 7. В ней мы используем декоратор pytest.mark, с аттрибутом case_id (наименование атрибута мы придумываем сами, можно использовать любой). В виде параметра передаем ID кейса из QASE.

Теперь, в процессе выполнения автотеста, мы можем использовать его ID. То есть сможем отправить запрос к API QASЕ на обновление шагов кейса.

Готовим шаги автотестов к использованию в документации

Для того, чтобы отправить запрос в QASE на изменение шагов кейса, нам надо эти шаги собрать в процессе работы автотеста. Поэтому первым делом мы добавим к каждому шагу автотестов информацию о том, что они делают. Ровно также, как мы заполняли шаги в QASE. То есть нам надо для каждого шага автотестов подготовить action, data и expected_result.

Шаги автотестов хранятся в директории pages. Там есть модуль index_page.py. Откроем его. В нем три шага.

    def open_index_page(self, page: Page) -> None:  # noqa
        page.goto(config.url.DOMAIN)

    def press_link_english_lang(self, page: Page):
        page.locator(self._LINK_ENGLISH_LANG).click()

    def get_text_from_google_search_button(self, page: Page) -> None:
        return page.locator(self._BUTTON_GOOGLE_SEARCH).get_attribute('value')

Что делает первый шаг? Открывает Index page. В качестве data можно указать https://google.com. То есть адрес Index page. А expected_result представляет из себя успешно открывшуюся страницу.

Как добавить эту информацию к шагу?

Добавляем декоратор к шагу автотеста
Добавляем декоратор к шагу автотеста

Вносим правки в модуль index_page.py. На третьей стори импортировали qase. Пока PyCharm ругается на ошибку, что такого модуля нет. И это так. Мы его создадим совсем скоро.

В строке 10 мы указали на декоратор, который скоро опишем в модуле qase. Строки 11, 12, 13 - это параметры декоратора, которые содержат в себе те самые сведения о шаге, которые требуются QASE.

Аналогичным образом добавим сведения для всех остальных шагов.

Информация о каждом шаге
Информация о каждом шаге

Для всех трех имеющихся шагов мы добавили информацию, которая далее будет передаваться в QASE. Как можно заметить, у второго и третьего шага не указан параметр data. Только параметр action является обязательным. Остальные опциональные.

Теперь, когда мы прописали информацию о шагах, надо написать сам декоратор, который эту информацию будет обрабатывать.

Пишем декоратор для обработки информации о шагах

Первым делом в корневой директории проекта (у меня это директория article_two) добавим новую директорию qase. В ней будем собирать все модули, которые потребуются для интеграции автотестов с QASE.io.

В директории qase создадим новый модуль docs.py.

В модуле docs.py создадим декоратор step, который мы уже добавили ко всем шагам автотеста.

# decorator for updating QASE steps
def step(action: str, data: str = '', expected_result: str = ''):
    def decorator(func):
        def wrapper(*args, **kwargs):
            Docs().prepare_step_data(action, data, expected_result)  # save step data
            return func(*args, **kwargs)  # step execution
        return wrapper
    return decorator

Сейчас PyCharm ругается на строку 5. Потому что класс Docs мы еще не написали. Но с тем, как работает декоратор и класс Docs, мы разберемся чуть позже. А пока нам необходимо в директории qase создать модуль '__init__.py'. И в нем импортировать созданный декоратор.

from qase.docs import step

Открываем модуль index_page и проверяем, что PyCharm перестал ругаться на отсутствие модуля qase. Ошибки больше нет.

Но, как мы помним, у нас есть ошибка в модуле docs.py. Там мы используем класс Docs, который еще не создали. Займемся этой задачей.

Класс Docs призван обеспечить корректную работу декоратора step.

Поэтому класс Docs будет решать несколько задач:

  1. Создать временный файл json, куда мы складывать все шаги из запущенного автотеста.

  2. Удалять этот файл, когда исполнение автотеста завершено, а данные отправлены в QASE.

  3. Здесь же будет метод, который будет считывать значения параметров декоратора (action, data, expected_result) и сохранять эти значения во временный файл.

  4. Также в классе Docs разместим вспомогательные методы, которые позволят читать файл json и сохранять изменения в нем.

Целиком модуль docs.py вместе с декоратором step и классом Docs будет выглядеть следующим образом:

Код модуля docs.py
import json
import os

import config


# decorator for updating QASE steps
def step(action: str, data: str = '', expected_result: str = ''):
    def decorator(func):
        def wrapper(*args, **kwargs):
            Docs().prepare_step_data(action, data, expected_result)  # save step data
            return func(*args, **kwargs)  # step execution
        return wrapper
    return decorator


class Docs:
    def __init__(self, database_json: str = 'docs.json'):
        self.database_path = f'qase/{database_json}'

    def _prepare_case_data(self, request) -> dict:  # noqa: no-self-use
        result = {
            'steps': [],
            'automation': 2
        }
        for attr in config.qase.CASE_PARAMS:
            if hasattr(request.node.get_closest_marker(attr), 'args'):
                result[attr] = request.node.get_closest_marker(attr).args[0]
        return result

    def create_database(self, request) -> bool:
        if os.path.exists(self.database_path):
            self.delete_database()
        with open(self.database_path, 'w') as database:
            database.write(json.dumps(self._prepare_case_data(request)))
        return os.path.exists(self.database_path)

    def prepare_step_data(self, action: str, data: str = '', expected_result: str = ''):
        step_value = {
            'action': action,
            'data': data,
            'expected_result': expected_result
        }
        if os.path.exists(self.database_path):
            database: dict = self.read()
            database['steps'].append(step_value)
            self.save(database)

    def read(self) -> dict:
        with open(self.database_path) as database:
            return json.load(database)

    def save(self, updated_database: dict) -> bool:
        with open(self.database_path, 'w') as database:
            json.dump(updated_database, database)
        return self.read() == updated_database

    def delete_database(self) -> bool:
        os.remove(self.database_path)
        return not os.path.exists(self.database_path)

После того, как мы добавили класс Docs проверяем, что PyCharm перестал ругаться на ошибку отсутствия класса Docs в декораторе step. Да, ошибки больше нет.

Методы по созданию и удалению временного файла json, которые мы реализовали в классе Docs, нам необходимо будет использовать в фикстуре pytest, которая будет отвечать за сбор, подготовку и отправку данных в QASE. Для удобного использования этих методов в других модулях нашего проекта вернемся в модуль '__init__.py' в директории qase. Импортируем и инициализируем класс Docs.

Инициализация класса Docs в модуле __init__.py
Инициализация класса Docs в модуле __init__.py

Предлагаю добавить изменения в GitHub и переходить к работе над первой фикстурой.

Пишем фикстуру update_docs()

Первым делом в директории fixtures создадим новый модуль qase_fixtures.py.

В модуль qase_fixtures.py добавляем следующий код:

import pytest

import config
import qase
import api


@pytest.fixture(autouse=config.qase.IS_DOC_UPDATE_NEEDED)
def update_docs(request) -> None:
    case_id_marker = request.node.get_closest_marker('case_id')
    if config.qase.TOKEN and len(case_id_marker.args) > 0:
        qase.docs.create_database(request)
    yield
    if config.qase.TOKEN and len(case_id_marker.args) > 0:
        api.qase.update_case(case_id_marker.args[0], qase.docs.read())
        qase.docs.delete_database()

Важно отметить, что после вставки кода в модуль PyCharm покажет ошибки о том, что мы еще не написали модули config.qase и qase.api. Так и должно быть. Мы их скоро напишем. А пока посмотрим на то, что делает код фикстуры.

Строка 8. Обозначаем, что это фикстура. И используем параметр autouse. Он может принимать значения True/False. Так как нам не нужно будет при каждом запуске pytest генерить документацию, то мы настроим config.qase так, чтобы он мог принимать из команды в терминале значение переменной IS_DOC_UPDATE_NEEDED. Таким образом, задавая в терминале значение этой переменной как True или False мы сможем управлять поведением документации (генерим ее или нет).

Строка 9. В качестве параметра функции передаем request. Он позволит нам получать доступ к значениям в атрибуте '@pytest.marl.case_id(1)', который мы добавляем к каждому автотесту.

Строка 10. Получение значения параметра из аттрибута case_id.

Строка 11 и 14. Для отправки запроса в API QASE потребуется Token, который мы ранее получили на сайте QASE.io. Но Token надо хранить в безопасности. Хранить его в коде проекта мы не будем. Поэтому Token будет работать аналогично с переменной IS_DOC_UPDATE_NEEDED и его значение мы будем получать из команды в терминале. На следующем шаге мы создадим модуль config.qase, где опишем поведение всех этих переменных.

Строка 12. Используем метод create_database, который создает файл json для хранения информации о шагах.

Строка 13. Сам по себе yield несет в себе два важных момента. Во-первых, он может работать как return, если после yield указать что-либо. Во-вторых он является разделителем. То, что написано до yield будет выполнено перед автотестом. То, что написано после yield будет выполнено после автотеста. Поэтому перед yield у нас идет создание файла, а после него отправка значений в QASE.io и удаление файла.

Строка 15. Запрос к API QASE. В этом запросе мы обновим шаги в кейсе. Запрос к API QASE создадим совсем скоро.

Чтобы в будущем pytest мог найти эту фикстуру нам следует добавить ее в файл conftest.py:

pytest_plugins = [
    'fixtures.page',
    'fixtures.qase_fixtures'
]

Таким образом сейчас необходимо создать config.qase и api.qase_api. Займемся этим.

Создаем модуль qase в директории config

В директории config создаем новый модуль qase.py. В созданный модуль добавляем следующий код:

import os


class Qase:
    TOKEN = os.getenv('QASE_TOKEN') if os.getenv('QASE_TOKEN') is not None else False
    IS_DOC_UPDATE_NEEDED = True if os.getenv('QASE_DOC') == 'True' else False

    API_DOMAIN = 'https://api.qase.io'
    FRONTEND_DOMAIN = 'https://app.qase.io'
    API_VERSION = 'v1'
    PROJECT = 'GS'

Строка 5. В модуле qase_fixtures.py мы использовали конструкцию if config.qase.TOKEN, которая либо позволит выполниться коду после нее, если значение config.qase.TOKEN не будет равно False. Так вот в строке 5 модуля qase.py мы считываем значение переменной QASE_TOKEN из команды в терминале и если она не равна None (то есть если такая переменная была передана в команде), то сохраняем в эту переменную токен. Если же такой переменной не было в команде в терминале, то присваиваем TOKEN значение False, что не позволит выполняться коду, связанному с интеграцией с QASE.io.

Строка 6. Работает по аналогии со строкой 5.

Строки 8, 9, 10 хранят в себе значения, необходимые для отправки запроса к API QASE. Эти данные нам совсем скоро пригодятся.

Строка 11 хранит код проекта из QASE, который нужен для отправки запросов к API QASE.

Не забудем импортировать и инициализировать модуль qase.py в модуле '__init__.py' директории config.

Текущее состояние модуля __init__.py
Текущее состояние модуля __init__.py

Сохраним изменения в GitHub и переходим к задаче с отправкой запроса к API QASE.

Создаем модуль qase_api, чтобы отправлять запросы к API QASE.io

В корневой директории проекта создадим директорию api. В ней мы будем собирать все модули, которые связаны с API. Первым таким модулем станет qase_api.py. Создадим модуль.

В модуль qase_api.py добавим код, в котором создадим класс и определим необходимые переменные.

import config


class QaseApi:
    HEADERS = {
            'accept': 'application/json',
            'content-type': 'application/json',
            'Token': config.qase.TOKEN
        }
    DOMAIN = f'{config.qase.API_DOMAIN}/{config.qase.API_VERSION}'
    PROJECT = config.qase.PROJECT

Строка 6. Заголовки потребуются для отправки запроса к API QASE.

Строка 11 и 12. Значения переменных DOMAIN и PROJECT будут необходимы для составления URL в запросе к API.

Теперь добавим сюда сам метод запроса к API QASE и проверку ответа.

    @staticmethod
    def check_response(response) -> bool:
        json_response = response.json()
        if response.status_code != 200 or not json_response['status']:
            return False
        else:
            return True

    def update_case(self, case_id: int, payload: dict) -> bool:
        url = f'{self.DOMAIN}/case/{self.PROJECT}/{case_id}'
        response = requests.patch(url, json=payload, headers=self.HEADERS)
        return self.check_response(response)

Итого полностью модуль qase_api.py выглядит следующим образом:

Итоговый вид модуля qase_api.py
Итоговый вид модуля qase_api.py

Теперь остается создать модуль '__init__.py' в директории api и добавить в него следующий код, который импортирует и инициализирует класс QaseAPI:

from api.qase_api import QaseApi

qase = QaseApi()

Остается зайти в модуль qase_fixtures.py и убедиться, что там нет ошибок.

Все наработки зальем в GitHub.

Проверяем работоспособность проекта

Если смотреть на ТЗ, то мы сделали все, что планировали. Но сейчас надо убедиться, что все работает корректно.

Ранее мы запускали автотесты командой pytest .

Сейчас перед самой командой нам надо добавить переменные, от которых зависит запуск интеграции с QASE. Нам необходимо добавить следующие переменные:

  1. QASE_DOC=True

  2. QASE_TOKEN=Значение токена, который получен из QASE.io

В целом команда должна выглядеть следующим образом:

QASE_DOC=True QASE_TOKEN=XXXXXXX pytest .

После запуска команды видим, что автотест прошел успешно. Но появилось сообщение о том, что используется не зарегистрированная маркировка case_id.

Сообщение об отсутствии регистрации маркировки
Сообщение об отсутствии регистрации маркировки

Чтобы исправить этот момент достаточно в корневой директории добавить файл pytest.ini со следующим кодом:

[pytest]

markers =
    case_id(id):

Таким образом мы можем сообщить pytest, что такая маркировка нами предусмотрена.

После перезапуска тестов получаем результат уже без сообщений о маркировке:

Сообщения о маркировке нет
Сообщения о маркировке нет

Чтобы увидеть, какие фикстуры в каком порядке выполнялись при прогоне, можно в конец команды добавить --setup-show.

Команда получится такой:

QASE_DOC=True QASE_TOKEN=XXXXXXX pytest . --setup-show

А результат выполнения команды теперь содержит наименования фикстур:

Фикстуры в результате выполнения команды
Фикстуры в результате выполнения команды

Остается проверить, что шаги в QASE обновились. Для этого открываем кейс:

Шаги в кейсе сформированы на основе шагов в автотестах
Шаги в кейсе сформированы на основе шагов в автотестах

Теперь, независимо от того, 1 у нас кейс в документации или 3000, шаги в кейсах будут актуальными и тратить дополнительное время на актуализацию документации не нужно.

Источник: https://habr.com/ru/post/715732/


Интересные статьи

Интересные статьи

Технология WebAssembly появилась относительно недавно (в 2015 году) и позиционировалась как альтернатива JavaScript для выполнения в среде браузера с максимально достижимой производительностью. Прилож...
Всем привет, я - Денис, Product Manager в банке, а до этого перезапускал вебинарную платформу VirtualRoom.Сегодня я расскажу про свой скрипт, который отмечал меня на лекциях в аспирантуре мехмата МГУ....
В уже  далеком 2019 Telegram объявил конкурс на создание веб-версии своего мессенджера, в котором мне удалось поучаствовать. По итогу у меня осталась библиотека, которая может работать с API Tele...
dotMemory — это профилировщик памяти для .NET от компании JetBrains. А меня зовут Илья, и я из команды разработки этого инструмента.Хочу поделиться историей классического догфудинга: как мы оптимизиро...
В ноябре 2019 года я уволился с работы и решил посвятить несколько месяцев изучению нового навыка, которому я уже давно хотел научиться. В то время я работал веб-разработчиком. До этого я изуча...