TGViewer
Channel Public Channel
Валерий | AQA Engineer | Автотестирование на Python | REST, gRPC, GraphQL

Валерий | AQA Engineer | Автотестирование на Python | REST, gRPC, GraphQL

@aqa_engineer

Сделаю из тебя крутого AQA инженера на Python.

• Преподаю лучшие тренинги по автоматизации тестирования API
• Senior Python developer | AQA lead, 7 лет в IT
Subscribers
1.52K
Photos
167
Videos
151
Links
179

Showing posts older than #356 · Back to latest

Older Posts 20 shown
Post #355 973
Fuzzing тестирование API

В этом канале мы с вами рассматриваем преимущественно темы автоматизации тестирования API.

У меня все никак не доходили руки рассказать про инструмент, который вы возможно даже использовали и который занимается fuzzing тестированием.

🔍 Что такое фаззинг-тестирование?

Фаззинг (fuzzing) — это метод автоматического тестирования, при котором программа проверяется на уязвимости и ошибки с помощью передачи случайных или полу-случайных данных на вход. Цель — найти краш, утечку памяти, неожиданное поведение или уязвимости безопасности.

🧩 Виды фаззинга:

- Мутационный фаззинг — изменяет существующие входные данные (например, искажая корректный JSON).

- Генеративный фаззинг —
создает данные с нуля на основе спецификации (например, OpenAPI/Swagger).


А теперь собственно про сам инструмент.

⚡️
Schemathesis — фаззер для API

Schemathesis —
это инструмент для тестирования API, основанный на OpenAPI/Swagger-схеме сервиса. Он автоматически генерирует тестовые сценарии и "обстреливает" ваш API некорректными или неожиданными данными, чтобы выявить:

- Ошибки сервера (500 Internal Server Error)
- Уязвимости (SQL-инъекции, XSS)
- Проблемы валидации
- Неожиданное поведение

🚀 Как работает?

1. Берет схему OpenAPI/Swagger.

2. Генерирует корректные и некорректные запросы на основе property based testing.

3. Отправляет их в API и анализирует ответы.


🔧 Пример использования:

pip install schemathesis  
schemathesis run https://example.com/openapi.json


Или с кастомными параметрами:
schemathesis run --checks all --max-response-time 5000 http://api.example.com/schema.json


💡 Зачем это нужно?


- Находить баги до продакшена.
- Улучшать отказоустойчивость API.
- Автоматизировать тестирование безопасности.

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

Инструмент безумно простой в использовании и очень эффективный, в целом можно внедрить отдельной не блокирующей гитлаб джобой с заделом на будущее. Кстати недавно обнаружил, что он еще умеет в GraphQL

Думаю добавлю урок с этим инструментом в курс Advanced, лишним не будет)

——————————-

📱 TG-сообщество

📱 Обучение

📱 Отзывы
  • 💩 53
  • 🔥 14
  • 👍 9
Post #354 1.32K
“Ты не хуже, просто у тебя не было нужного тренера.”

Недавно мой ученик написал:

«К нам пришёл человек с 4 годами опыта в автотестах — и он знает ровно столько же, сколько я.»

Он добавил:

«я хотел сказал, что с таким учителем как ты я развил потенциал и опыт меньше чем за год.»


Это не про меня. Это про силу правильного подхода.
Про то, как практика, структура, и обучение дают больше, чем 4 года «просто работы».


Что даёт результат быстрее опыта в вакууме?
• Фокус не на инструментах, а на понимании архитектуры
• Практика, приближённая к реальным задачам на проекте
• Преподаватель, который объясняет


Если ты:
• Хочешь не просто «писать тесты», а строить фреймворки
• Чувствуешь, что время идёт, а прогресса нет
• Или уже в тестировании, но хочешь расти как инженер —
присоединяйся.

Ты не отстаёшь. У тебя просто ещё не было правильной траектории.

——————————-

📱 TG-сообщество

📱 Обучение

📱 Отзывы
  • 💩 63
  • 🔥 7
  • ❤ 5
  • 💯 1
Post #353 979
Отзыв на курс Advanced, запоздалый немного, но вот только созрел)


Когда отзыв приходит не сразу — это тоже хорошо.
Значит, человек не просто посмотрел курс, а пожил с ним.

Вот кусочек одного такого отзыва на курс Advanced:

«Сейчас пробую свои собственные проекты писать по этой же структуре и все реально легко работает, прописал основные моменты и дальше остается только описывать апишку и шаги тестов, все работает красиво с первых дней)».


✔️ Вот за это я и люблю поздние фидбэки.

Курс отлежался в голове, прошёл через пару шишек, и вдруг всё встало на свои места.

Читаемость, структура, простота — это не случайно.

🔥Вот ради этого и делался Advanced.
Весь курс построен так, чтобы после него можно было не “переизобретать”, а спокойно писать API-тесты на понятной архитектуре, без боли и гадания, «как бы тут лучше».

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

Спасибо за отзыв — и жду на 🏆 про-уровне, когда придёт время.

Полный отзыв можно прочитать тут.

——————————-

📱 TG-сообщество

📱 Обучение

📱 Отзывы
  • 💩 57
  • 🔥 11
  • ❤ 1
Post #348 1.34K
🚀 Открыл набор на потоки REST API Advanced и Professional!

Если ты автоматизатор, и до сих пор не с этими навыками — ты просто теряешь в деньгах.

Выкладываю один из самых интересных моментов прошедшего вебинара, где менее чем за минуту ⏱️ мы разворачиваем целый фреймворк!

📊 И немного про ступени.

🎓 Advanced — это не “чуть сложнее”, это переход на уровень, где:

- ты больше не “дописываешь тесты” — ты строишь фреймворк, и делаешь это правильно
- ты шаришь в микросервисной архитектуре, авторизации, логах, отчётах и Telegram-нотификациях
- ты понимаешь, как устроен CI/CD и как тесты встраиваются в процесс поставки продукта.
- можешь дать оценку покрытия своего сервиса автотестами.



🏆 Professional — для тех, кто хочет делать так, как делают в top-компаниях:

- асинхронность, CLIs, генерация кода и фикстур, тестов, проектные шаблоны, управление зависимостями и качество кода на уровне команды
- ты не просто пишешь код — ты делаешь инструменты, которые экономят часы другим
- после этого курса ты больше не “один из”, ты становишься носителем экспертизы.

Как вы знаете я работаю и с юридическими лицами.

Почему стоит согласовать обучение с работодателем прямо сейчас:
✅ Это не теория — ты сразу начнёшь внедрять полезные штуки в проект.
✅ Можно частично или полностью компенсировать курс через обучение от компании.
✅ Это прямой вклад в эффективность команды, стабильность автотестов и ускорение поставок.

Почему надо шевелиться:
🔹 Поток стартует скоро — 12 мая.
🔹 Кол-во мест ограничено, чтобы каждому было внимание.
🔹 Если ты от компании, нужно успеть согласовать нужные документы.
🔹 В следующем квартале ты или уже покажешь рост, или опять скажешь “вот в следующем точно пройду”.

Готов? Тогда выбирай курс и вписывайся:
• 🎓 Advanced
• 🏆 Professional

И если сомневаешься, с какого лучше начать — напиши, помогу определиться. 💬
  • 💩 84
  • 🔥 5
Post #341 1.36K
Привет, друзья!

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

Я не люблю воду на вебинарах, поэтому сделал его максимально прикладным и техническим – только практика, только хардкор.

Что выяснили:

- На написание одного хорошего автотеста (с учетом опыта) уходит ~1,5 часа

- Но это без учета сопровождения – а там могут быть бесконечные правки и доработки

- С правильными инструментами тот же тест пишется за 5 минут (да, в 18 раз быстрее!)


90 минут против 5 – и это без учета времени на настройку линтеров, CI/CD и прочей инфраструктуры.

Это не просто экономия времени – это возможность:
✅ Развиваться вместо бесконечного фикса багов
✅ Делать сложные задачи легко
✅ Стать настоящим Senior’ом, а не просто "поставить галочку" в резюме

Пока другие крутят "опыт" и продают вымышленные успехи, вы сможете:
🔹 Писать чисто, быстро и профессионально
🔹 Работать с реальной технической базой (а не поверхностными знаниями)
🔹 Сократить не только свои трудозатраты, но и бюджет компании

Мои тренинги я считаю лучшими на рынке – и это не просто слова. Многие студенты после них растут в 2-3 раза быстрее коллег.

Хочешь так же? Присоединяйся – научу!

Старт уже в понедельник 7 апреля!

А если пропустил вебинар – обязательно посмотри запись, пока она доступна. Это сильный рывок даже для тех, кто уже в теме.

P.S. Разница в 18 раз – это не магия. Это знание того, как делать правильно. Если хочешь таких же результатов – пиши, расскажу подробнее. 💪

Описание всех тренингов можно найти тут
  • 💩 59
Post #340 2.54K
Как развернуть автоматизацию тестирования API за неделю?

Сейчас расскажу, как моя жена развернула автоматизацию на проекте за неделю.

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

Я не люблю помогать)))
Это высказывание не совсем вяжется с тем, что происходит на самом деле. Моя телега обычно трещит от вопросов, и я всегда отвечаю. Просто я не люблю быть чатом GPT и просто говорить, как тут сделать. Я всегда вместо "рыбы" пытаюсь дать "удочку".

Почему?

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

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

Я сказал: "Вот курс — смотри." Наверное, можно было сесть, все показать и рассказать, но зачем, если я уже снял видео? ))

Для нее это был жуткий марафон. С утра до ночи она сидела и все пробовала. И вот спустя неделю у нее уже работает pipeline, прикручен Allure, логи, степы, уведомления о прохождении сыпятся в телегу, и при падении могут тэгать нужных людей.

Как вы думаете, это быстро, учитывая, что человек до этого ни разу не писал код? 🤔

ЭТО ПИЗДЕЦ КАК БЫСТРО! 💥

А схема получилась проста: большую часть времени занимает написание клиентов, поэтому она их просто генерирует, а дальше заполнение сценария не сложнее, чем выполнять то же самое в Swagger или Postman.

Получился такой флоу:
генерирует клиент -> делает фикстуру -> пишет тест.

К концу второй недели она уже покрыла 90% сервиса смоками — 88 автотестов, и это не статус-код 200, а реальные бизнесовые проверки с хождением по разным сервисам.

Есть ли тут подвох?
Да, есть.

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

Вчера я тестировал инструмент, который будет итогом обучения professional, и это очень круто! 😍
Я думаю, вам понравится тоже.

Приглашаю на обучение)
  • 💩 75
Post #339 1.11K
удалил(-а) Вас из канала
  • 💩 75
  • 🤡 48
  • 😁 23
  • 👍 8
  • 😱 6
  • 🤔 4
Post #338 1.17K
Принцип YAGNI (You Ain’t Gonna Need It) — «Тебе это не понадобится»

Часто на картинках с DRY и KISS мелькает еще один важный принцип — YAGNI. Он гласит: не добавляй функциональность, которая может понадобиться в будущем, но не нужна прямо сейчас.

Основные идеи YAGNI:
1️⃣ Не реализуй то, что не требуется сейчас — если нет конкретного ТЗ, не пиши код «на всякий случай».
2️⃣ Избегай ненужной сложности — лишний код усложняет поддержку и тестирование.
3️⃣ Оптимизируй только при необходимости — преждевременная оптимизация может навредить.
4️⃣ Экономи время и ресурсы — меньше лишнего кода = чище и понятнее архитектура.

Примеры нарушения YAGNI:

- Абстрактная архитектура «на будущее», которая никогда не пригодится.

- Параметры в функции, которые пока не используются.

- Сложная система логирования для одноразового скрипта.

Хороший принцип… но я его нарушаю в автотестах😅

Вот как и почему:

🔹 Генерация клиентов
Если писать клиенты вручную, логично описать только нужный контроллер для нужного API сервиса. Но я давно использую сгенерированные клиенты — и если генерировать, то все сразу.

🔹 Версии API (V1, V2, OLD и т. д.)
Контроллеры в разных версиях API могут называться одинаково, и появляются фикстуры вроде old_controller_api, v2_controller_api и т. п. Проще сразу сделать фасад, который объединит API (контроллеры) одного сервиса, где через точку можно доставать нужный, вместо того чтобы путаться в версиях контроллеров.

🔹 Обёртки над методами
Контракты меняются, и если не подготовиться, придётся переписывать все тесты, а не один метод-абстракцию. Поэтому я довольно часто добавляю еще один класс, в виде наследника от API клиента, в котором упрощаю интерфейс тестируемого метода.

Да, это усложняет проект, но зато даёт архитектуру, которую легко поддерживать годами.
А если все это генерировать это вовсе сводит трудозатраты к минимуму, задавая чёткие рамки проекта.

Вывод: YAGNI — хорошее правило, но иногда его можно нарушать осознанно. Главное — понимать, зачем.

——————————-

📱 TG-сообщество

📱 Обучение

📱 Отзывы
  • 💩 79
  • 👍 9
Post #337 987
В посте выше в комментариях верное отметили, что говоря про DRY, нужно не забывать про KISS.

Принцип KISS (Keep It Simple, Stupid) — один из ключевых в разработке.
Его суть проста: чем проще ваш код, тем легче его поддерживать, масштабировать и понимать (вам и другим разработчикам).

Почему KISS важен?

1️⃣  Меньше багов — сложный код = больше ошибок.
2️⃣ Лучше читаемость — коллеги (и будущий вы) скажут спасибо.
3️⃣ Быстрее разработка — не нужно изобретать велосипед.

Я точно знаю, что есть проекты написанные мной еще 4 года назад, которые работают и поддерживаются по сей день. Все потому, что были написаны очень просто и понятно, как молоток.

Как применять KISS?

✔️ Дробите задачи — одна функция = одна ответственность.
✔️ Избегайте "умного" кода — если решение требует сложных объяснений, возможно, есть вариант проще.
✔️ Рефакторите — если код стал запутанным, упрощайте без жалости.
✔️ Используйте проверенные паттерны — но не усложняйте там, где можно без них.

"Совершенство достигнуто не тогда, когда нечего добавить, а когда нечего убрать."


Ваш код должен решать задачу, а не демонстрировать интеллект. Старайтесь писать проще!

——————————-

📱 TG-сообщество

📱 Обучение

📱 Отзывы
  • 💩 57
  • 👍 8
  • ❤ 1
Post #336 1.37K
✨ Принцип DRY (Don't Repeat Yourself)

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

Основные идеи DRY:

1️⃣ Устранение дублирования
Если код или логика повторяются — выноси их в отдельный модуль, функцию или класс. Так любые изменения потребуют правки только в одном месте.

2️⃣ Упрощение поддержки
Меньше дублирования → проще тестировать и обновлять код. Никаких "а не забыл ли я поправить это в другом файле?"

3️⃣ Улучшение читаемости
Чистый и структурированный код легче понимать и поддерживать.

💡 Как применять DRY?
Используй функции, классы, библиотеки и паттерны проектирования, чтобы избежать повторений.

🎯 Как мы применяем DRY с учениками?

✔️ Логгирующий клиент — один класс для всех логов, а не копипаста в каждом тесте 📋.
✔️ Хелперы и врапперы  — позволяет зашить логику проверок, разметки и другие полезные действия в одном месте, тем самым появляется как бы "авторазметка" и "автопроверки", так как использование методов из классов уже содержит все необходимое.
✔️ Декораторы  — позволяет добавлять новый функционал к уже существующим методам, всего одной строчкой.
✔️ Фасады — позволяют определить одну входную точку в нужный клиент, вместо написания большого количества очень похожих фикстур.
✔️ Используем наследование и другие возможности языка, для уменьшения дублирования кода.

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


——————————-

📱 TG-сообщество

📱 Обучение

📱 Отзывы
  • 💩 83
  • 👍 7
  • 🔥 4
Post #335 1.08K
🚀 От тестов к инструментам: как сеньеры создают фреймворки за пару кликов

Привет, коллега! 👋

Если ты хоть раз задумывался, почему одни пишут тесты часами, а другие — за пару минут, то этот вебинар для тебя!

Я расскажу:
✅ Как джуны, мидлы и сеньеры подходят к автоматизации — и почему это выглядит как "слоумо" vs "вау, как он это сделал?".
✅ Как сеньеры создают целые фреймворки в пару кликов, пока остальные пишут сотни строк кода?
✅ Как перейти от написания тестов к созданию инструментов, которые делают всю работу за тебя?

💡 Секретный ингредиент: Я покажу, как с помощью одного инструмента можно генерировать целые фреймворки автотестирования, которые экономят часы, дни и даже недели работы.

Минимум воды максимум пользы, мы будем заниматься лайфкодингом.

📅 Дата и время вебинара: 03.04.2025, 20-00 по МСК
📍 Где: 
В этой группе (обязательно подпишись, чтобы не пропустить!)

👉 Почему стоит присоединиться?

- Новичок? Узнаешь подходы к автоматизации и построению фреймворка.

- Имеешь опыт? Узнаешь, как выйти на новый уровень в автоматизации.

- Поймешь, как писать не просто тесты, а инструменты, которые делают тебя незаменимым.

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


🔥 Вау-эффект гарантирован!
Не упусти шанс узнать, как сделать свою работу проще, быстрее и круче.

👇 Что делать сейчас?


1. Подпишись на эту группу . И этот канал.

2. Расскажи друзьям — пусть тоже прокачают свои навыки.

3. Жди анонса вебинара и готовь вопросы!


Ссылка на группу: Webinar: От тестов к инструментам

Ну и немного обо мне)

Сейчас: старший разработчик - Python платформы OzonTech
В прошлом: Ведущий инженер по автоматизации тестирования OzonTech

Ну, и просто хороший человек)

До встречи на вебинаре! 🚀
  • 💩 64
  • 🔥 16
  • ❤ 1
Post #334 881
Стандартная метафора «переменные – это ящики» ведет к непониманию ссылочных переменных в объектно-ориентированных языках.

Переменные в Python похожи на ссылочные переменные, поэтому лучше представлять их как этикетки, приклеенные к объектам.

Следующий пример поможет понять, почему.

>>> a = [1, 2, 3]
>>> b = a
>>> a.append(4)
>>> b
>>> [1, 2, 3, 4]


1. Создать список [1, 2, 3] и связать с ним переменную a
2. Связать переменную b с тем же значением, на которое ссылается a.
3. Изменить список, на который ссылается a, добавив в конец еще один элемент.
4. Можно наблюдать, как это отразилось на переменной b.
5. Если считать b ящиком, в котором хранилась копия списка [1, 2, 3] из ящика a, то такое поведение бессмысленно.

Таким образом, предложение b = a не копирует содержимое ящика a в ящик b, а наклеивает метку b на объект, уже имеющий метку a.

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

Поскольку переменные – это просто этикетки, ничто не мешает наклеить на объект несколько этикеток. В этом случае образуются псевдонимы.

Тем самым оператор is проверяет, что разные этикетки наклеены на один и тот же ящик.
А оператор == проверяет, что разные ящики имеют одинаковое содержимое.

——————————-

📱 TG-сообщество

📱 Обучение

📱 Отзывы
  • 💩 66
  • 👍 16
  • 🫡 1
Post #333 944
  • 💩 55
  • 👍 1
Post #332 1.01K
Ответ на задачу выше. 🔍

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

Это тонкая ошибка. Из примера видно, что когда объект Bus инициализируется списком пассажиров, он работает правильно. ✅

Странности начинаются, когда Bus вначале пуст, потому что в этом случае self.passengers оказывается псевдонимом значения по умолчанию для параметра passengers.

Беда в том, что любое значение по умолчанию вычисляется в момент определения функции, т. е. обычно на этапе загрузки модуля, после чего значения по умолчанию становятся атрибутами объекта-функции.

Так что если значение по умолчанию – изменяемый объект и вы его изменили, то изменение отразится и на всех последующих вызовах функции. ⚠️

Если после выполнения кода из примера проинспектировать объект Bus.__init__, то мы обнаружим имена в его атрибуте __defaults__:

dir(Bus.__init__)
# ['__annotations__', '__call__', ..., '__defaults__', ...]


Bus.__init__.__defaults__
# (['Vasiliy', 'Julia'],)


Наконец, можно убедиться, что bus2.passengers – псевдоним первого элемента атрибута:
Bus.__init__.__defaults__[0] is bus2.passengers  # True


Описанная проблема и есть причина того, почему для параметров, принимающих изменяемые значения, часто по умолчанию задается значение None. ❗️
Например:
class Bus:  
def __init__(self, passengers=None):
if passengers is None:
# Создаем новый пустой список
self.passengers = []
else:
# Связываем копию
self.passengers = list(passengers)


В примере init проверяет, верно ли, что аргумент passengers совпадает с None, и, если это так, присваивает атрибуту self.passengers вновь созданный пустой список. Если passengers не совпадает с None, то правильное решение заключается в том, чтобы связать копию этого атрибута с self.passengers.

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

📱 TG-сообщество

📱 Обучение

📱 Отзывы
  • 💩 67
  • 🔥 7
  • 🤔 1
Post #331 859
  • 💩 75
  • 🤔 6
  • 🤓 2
Post #330 980
  • 💩 82
Post #326 966
  • 💩 90
Post #325 855
  • 💩 70
Post #323 850
Ребят спасибо большое, что забустили канал, даже не ожидал такой поддержки, это позволило чуть причесать канал, еще два голоса и даже можно будет устанавливать свои собственные обои. Но не буду уж наглеть вы и так накинули очень много, еще раз спасибо большое.

Ну а пока давайте поговорим, почему в срезы и диапазоны не включается последний элемент?

Принятое в Python соглашение не включать последний элемент в срезы(slice) и диапазоны(range) соответствует индексации с нуля, принятой в Python, C и многих других языках.

Приведем несколько полезных следствий из этого соглашения.

- В математике и теории множеств удобно работать с индексами, начинающимися с нуля, так как это упрощает ссылки на позиции в последовательности.

- Легко понять, какова длина среза или диапазона, если задана только конечная позиция: и range(3), и my_list[:3] содержат три элемента.

-  Легко вычислить длину среза или диапазона, если заданы начальная и конечная позиции, достаточно вычислить их разность stop - start.

-  Легко разбить последовательность на две непересекающиеся части по любому индексу x: нужно просто взять my_list[:x] и my_list[x:].

l = [10, 20, 30, 40, 50, 60]
l[:2] # [10, 20]
l[2:] # [30, 40, 50, 60]


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

TG-сообщество | Обучение | Отзывы
  • 💩 55
  • 👍 12
Older posts →
Threads Profile ViewerView any public Threads profile without an account.Open ThreadLook →Writing with AI? Make it sound human.Metric37 rewrites AI drafts so they read naturally. Free AI detector, 1,500 words free.Try Metric37 →