Як зробити розробку продуктивнішою через автоматизацію юніт-тестів з ChatGPT

💡 Усі статті, обговорення, новини про тестування — в одному місці. Приєднуйтесь до QA спільноти!

Привіт усім! Мене звуть Ярослав. Я працюю в компанії WiX на позиції QA Automation / SDET, і сьогодні я хочу поділитися з вами досвідом використання ChatGPT для написання юніт-тестів на конкретних прикладах.

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

Одним зі способів використання ChatGPT може бути створення шаблонів для юніт-тестів. Ця модель може допомогти вам сформулювати загальні правила тестування, які можна використовувати для створення багатьох тестових випадків.

Наприклад, ви можете попросити модель згенерувати тестові шаблони для перевірки коректності валідації форм, обробки виключень або взаємодії з базою даних. Це дозволить вам швидко створювати нові тестові випадки на основі заздалегідь визначених шаблонів.

Але написання юнітів зазвичай — ресурсомісткий та не дуже цікавий процес, який може потребувати багато часу. Спілкуючись з розробниками та QA я зрозумів, що більшість з них пише юніти власноруч, і таски з написання юнітів їм не дуже подобаються.

В цій статті я наведу приклади, як я значно скоротив час на їх написання.

Задача

Є метод, який створює селектор на основі заданого атрибуту та матчеру:

export function getSelectorWithAttributeType(
 key: string,
 attributeType: AttributeType,
 attributeMatcher?: AttributeMatcher,
) {
 const attributeMatcherString = attributeMatcher
   ? attributeMatcher
   : AttributeMatcher.EQUALS;
 return `[${attributeType}${attributeMatcherString}"${key}" i]`;
}

де AttributeType — рядок, а attributeMatcher — літерал порівняння.

І є низка методів, яка використовує цей метод обгорткою для певних атрибутів, наприклад:

export function getRoleSelector(
 key: string,
 attributeMatcher?: AttributeMatcher,
) {
 return getSelectorWithAttributeType(
   key,
   AttributeType.ROLE,
   attributeMatcher,
 );

Таких методів більш як 10, і добре мати хоча б по одному юніту на кожен. Навряд їх взагалі будуть чіпати, але якщо раптом зламають певний з них — багато тестів на кількох проєктах почнуть падати.

Спробуємо натаскати ChatGPT на цю таску так, щоб окрім copy/ paste ніяких дій не потрібно було робити.

Перший крок — описуємо йому задачу:

Отримуємо блок коду:

import { getSelectorWithAttributeType, AttributeMatcher } from './your-file';

describe('getSelectorWithAttributeType', () => {
  it('should return selector with EQUALS attribute matcher when no attribute matcher is provided', () => {
    const key = 'value';
    const attributeType = 'data-key';
    const selector = getSelectorWithAttributeType(key, attributeType);
    expect(selector).toBe('[data-key="value" i]');
  });

  it('should return selector with the provided attribute matcher', () => {
    const key = 'value';
    const attributeType = 'data-key';
    const attributeMatcher = AttributeMatcher.CONTAINS;
    const selector = getSelectorWithAttributeType(key, attributeType, attributeMatcher);
    expect(selector).toBe('[data-key*="value" i]');
  });

  it('should return selector with case-insensitive matching', () => {
    const key = 'value';
    const attributeType = 'data-key';
    const attributeMatcher = AttributeMatcher.EQUALS;
    const selector = getSelectorWithAttributeType(key, attributeType, attributeMatcher);
    expect(selector).toBe('[data-key="value" i]');
  });
});

Виглядає не так вже і погано. Але були використані не всі матчери.

Отримуємо блок коду:

import { getSelectorWithAttributeType, AttributeMatcher } from './your-file';

describe('getSelectorWithAttributeType', () => {
  it('should return selector with CONTAINS attribute matcher', () => {
    const key = 'value';
    const attributeType = 'data-key';
    const attributeMatcher = AttributeMatcher.CONTAINS;
    const selector = getSelectorWithAttributeType(key, attributeType, attributeMatcher);
    expect(selector).toBe('[data-key*="value" i]');
  });

  it('should return selector with EQUALS attribute matcher', () => {
    const key = 'value';
    const attributeType = 'data-key';
    const attributeMatcher = AttributeMatcher.EQUALS;
    const selector = getSelectorWithAttributeType(key, attributeType, attributeMatcher);
    expect(selector).toBe('[data-key="value" i]');
  });

  it('should return selector with STARTS_WITH attribute matcher', () => {
    const key = 'value';
    const attributeType = 'data-key';
    const attributeMatcher = AttributeMatcher.STARTS_WITH;
    const selector = getSelectorWithAttributeType(key, attributeType, attributeMatcher);
    expect(selector).toBe('[data-key^="value" i]');
  });

  it('should return selector with ENDS_WITH attribute matcher', () => {
    const key = 'value';
    const attributeType = 'data-key';
    const attributeMatcher = AttributeMatcher.ENDS_WITH;
    const selector = getSelectorWithAttributeType(key, attributeType, attributeMatcher);
    expect(selector).toBe('[data-key$="value" i]');
  });
});

Тепер це більш-менш те, що нам потрібно. Коригуємо імпорти/ перевикористовуємо key, attributeType в expect, і перша частина роботи зроблена.

Формулюємо наступну задачу

Оскільки в обгортках ми використовуємо базову функцію, нам достатньо перевірки, що базова функція була викликана.

Отримуємо код:

import { getDataAidSelector, getSelectorWithAttributeType, AttributeType, AttributeMatcher } from './your-file';

describe('getDataAidSelector', () => {
  it('should call getSelectorWithAttributeType with key and attributeMatcher for DATA_AID', () => {
    const key = 'value';
    const attributeMatcher = AttributeMatcher.CONTAINS;
    const getSelectorSpy = jest.spyOn(getSelectorWithAttributeType, 'getSelectorWithAttributeType');
    getDataAidSelector(key, attributeMatcher);
    expect(getSelectorSpy).toHaveBeenCalledWith(key, AttributeType.DATA_AID, attributeMatcher);
    getSelectorSpy.mockRestore();
  });

  it('should call getSelectorWithAttributeType with key and default attributeMatcher for DATA_AID', () => {
    const key = 'value';
    const getSelectorSpy = jest.spyOn(getSelectorWithAttributeType, 'getSelectorWithAttributeType');
    getDataAidSelector(key);
    expect(getSelectorSpy).toHaveBeenCalledWith(key, AttributeType.DATA_AID, undefined);
    getSelectorSpy.mockRestore();
  });
});

spyOn мені не підходить в цьому кейсі, бо насправді getSelectorWithAttributeType не є об’єктом.

Коригуємо задачу:

Відповідь бота:

import { getDataAidSelector } from './yourModule'; // import the module containing the function
import { getSelectorWithAttributeType, AttributeType } from './your-file'; // import the module containing getSelectorWithAttributeType

jest.mock('./your-file', () => ({
  ...jest.requireActual('./your-file'),
  getSelectorWithAttributeType: jest.fn(),
}));

describe('getDataAidSelector', () => {
 afterEach(() => {
    jest.clearAllMocks();
  });

  it('should call getSelectorWithAttributeType with the specified parameters', () => {
    const key = 'testKey';
    const attributeMatcher = AttributeMatcher.CONTAINS;

    getDataAidSelector(key, attributeMatcher);

    expect(getSelectorWithAttributeType).toHaveBeenCalledWith(
      key,
      AttributeType.DATA_AID,
      attributeMatcher
    );
  });

  it('should call getSelectorWithAttributeType with only the key parameter', () => {
    const key = 'testKey';

    getDataAidSelector(key);

    expect(getSelectorWithAttributeType).toHaveBeenCalledWith(
      key,
      AttributeType.DATA_AID,
      undefined
    );
  });
});

Коригуємо тест до остаточного вигляду у проєкті та показуємо боту:

Результат:

import {
  AttributeMatcher,
  AttributeType,
  getDataAutomationIdSelector,
  getSelectorWithAttributeType,
} from '../../src';

jest.mock('../../src/selector/getSelectorWithAttributeType', () => ({
  ...jest.requireActual('../../src/selector/getSelectorWithAttributeType'),
  getSelectorWithAttributeType: jest.fn(),
}));

describe('Selectors: getDataAutomationIdSelector()', () => {
  afterEach(() => {
    jest.clearAllMocks();
  });

  it('should call getSelectorWithAttributeType with key and provided attribute matcher', () => {
    const key = 'value';
    const attributeMatcher = AttributeMatcher.EQUALS;
    getDataAutomationIdSelector(key, attributeMatcher);
    expect(getSelectorWithAttributeType).toHaveBeenCalledWith(
      key,
      AttributeType.DATA_AUTOMATION_ID,
      attributeMatcher,
    );
  });
});

Бот коректно написав назви, вгадав ім’я атрибута в об’єкті і навіть підставив інший матчер, ніж у моєму прикладі, що є гарною практикою.

Тепер я можу згенерувати весь тес- сьют без рев’ю наступних кейсів, що бот буде генерувати для цього випадку. І, звісно, я значно зекономив свій час, бо на всі ці дії мені знадобилось близько 15-20 хвилин.

Іноді виникають проблеми, бо третя версія ChatGPT часто втрачає контекст, і процес наведення доводиться починати з нуля.

Висновки

У цій статті я поділився своїм досвідом роботи з ChatGPT для оптимізації написання юніт-тестів на конкретних прикладах.

Завдяки допомозі бота, можна швидко згенерувати основний шаблон тесту та заповнити його конкретними даними.

Крім того, ChatGPT може пропонувати варіації тестів та коригувати код на основі вхідних даних.

Необхідно зауважити, що ChatGPT наразі не може повністю замінити розробника і його досвід у написанні тестів. Проте він може суттєво зекономити час, спростивши рутинні завдання та генеруючи основний шаблон тесту.

👍ПодобаєтьсяСподобалось6
До обраногоВ обраному6
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Вбити велику сферу за допомогою АІ це занадто деструктивно. Ніхто на таке не піде. Створення АІ дало гарний поштовх для її розвитку. АІ дозволить виконати більшу частину рутинних задач на які завжди не вистачає часу. Дозволить позбутись технічого боргу для тих хто занадто зосереджений на створенні «value». Перфекціоністам дозволить побачити код у вигляді в якому вони хочуть, перевірити випадки які ніхто ніколи не перевіряв. Бізнесу отримати більше «value» за одиницю часу. Всі у виграші... покищо

Погрався я з ChatGPT: від вигадує неіснуючі апі функції, передає в них неіснуючі структури, ініціалізує в неіснуючих структурах неіснуючі поля. Довіряти йому не можна. Тим паче «без рев’ю наступних кейсів».

Тому він не замінить розробників)

Так, все вірно, він може генерити не існуючи функції або поля.
Тоді йому потрібно вказати на помилки та показати приклади, як потрібно писати, і який результат очікуван.
Після кількох уточнень ТЗ він починає генерувати прийнятні структури.
Це залежить від деталізації вимог та везіння, що він не забуде про контекст. (Я чув, що в 4 версії з контекстом краще, ніж в 3)

a good developer writes a test before writing the actual code ;)

Prompt: I have the following tests for a function Foo() in %language% using %framework%. Can you help me writing the function that will pass the tests?

Чи можете Ви покрити краще саме наведений приклад? :)
Які Ви бачите кейси, які критичні для використання цієї функції, та виходять за область даного покриття?

Не пам’ятаю, щоб юніти писались з огляду, що існують тести, які ’do not pass’ одразу після імплементації (бо це значить, що саме цей шматок коду ніхто не використовує, та не факт, що колись буде).

Мета — забезпечити коректний output функції залежно від переданих аргументів у тому вигляді, у якому це використовується на проекті. В даному випадку мета досягнута.
В більшості випадків, де немає складних моків (де мок — це саме і є тест), бот задовільно виконує задачу.

Пан Дмитро сказав, що тести непогано писати до коду (red-green approach). Тобто замість фунції пишеться стаб, пишутся тести по акцептанс критеріям, потім вже сама функція. Мій меседж в тому, що чат жпт допоможе і в цьому випадку (ось тобі тести і сигнатура, давай мені функцію).

Соррі, хибно зрозумів зміст коммента. :)
Не часто бачив цей підхід в реальному житті — частіше бачив ситуацію з боку ПМа: «Давай мені в понеділок працюючу функцію, а тести в п’ятницю напишеш».

Головне щоб він не нагенерив switch чи пачку if’ів на основі вхідних данних з ретурнами очікуваних значень.

І де факто всі тести проходять, але є один момент, коли в іnput’і буде значення, якого нема в тестах.

Йой, то ж жарт був) Але це основний аргумент проти ред-грін підходу як такого, а не проти чатжпт. Так можна чатжпт замінити на джуна і аргумент буде валідним (теж свічів накрутить).

Підписатись на коментарі