DEV Community

Yaroslav
Yaroslav

Posted on

AI-Powered Unit Tests: A Step-by-Step Approach with ChatGPT

Hello, Community.

I am working as QA Automation / SDET, and I want to share my experience using ChatGPT for writing unit tests with specific examples.

Unit tests are an essential part of the software development process, allowing us to verify the correctness of individual components of the program. Well-written unit tests help ensure code stability and reliability, facilitate refactoring, and improve the overall product quality.

One way to use ChatGPT is to create templates for unit tests. This model can help you formulate general testing rules that can be used to create multiple test cases.

For example, you can ask the model to generate test templates to check the correctness of form validation, exception handling, or interactions with the database. This allows you to quickly create new test cases based on predefined templates.

However, writing unit tests can be a resource-intensive and not very exciting process that may require a lot of time. In my interactions with developers and QA professionals, I noticed that most of them prefer to write unit tests manually and do not enjoy tasks related to writing them.

In this topic, I will provide examples of how I significantly reduced the time spent on writing unit tests.

Task:

There is a method that creates a selector based on a given attribute and matcher

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

where AttributeType is a string, and attributeMatcher is a comparison literal.

And there is a set of methods that use this method as a wrapper for specific attributes, for example:

export function getRoleSelector(
 key: string,
 attributeMatcher?: AttributeMatcher,
) {
 return getSelectorWithAttributeType(
   key,
   AttributeType.ROLE,
   attributeMatcher,
 );
}
Enter fullscreen mode Exit fullscreen mode

There are more than 10 such methods, and it's good to have at least one unit test for each. While they might not be modified frequently, if one of them breaks, many tests across multiple projects may start failing.

Let's try to train ChatGPT on this task so that we don't need to do anything other than copy/pasting.

First, we describe the task to ChatGPT:

Task for ChatGPT

Next, we get the code block as a response:

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]');
  });
});
Enter fullscreen mode Exit fullscreen mode

It doesn't look bad, but not all matchers were used.

Condition correction

Next, we get the code block:

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]');
  });
});
Enter fullscreen mode Exit fullscreen mode

Now it's more or less what we need. We adjust imports, reuse key and attributeType in expect, and the first part of the work is done.

Next task:

Second task in context

As we are using the base function in wrappers, we only need to check that the base function was called.

Next, we get the code block:

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();
  });
});
Enter fullscreen mode Exit fullscreen mode

spyOn doesn't fit in this case because getSelectorWithAttributeType is not an object.

Let's adjust the task:

Second task correction

ChatGPT Response:

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
    );
  });
});
Enter fullscreen mode Exit fullscreen mode

Now it looks good. The bot correctly wrote the names, guessed the attribute name in the object, and even used a different matcher than in my example, which is good practice.

Now I can generate the entire test suite without reviewing the next cases the bot generates for this scenario. And, of course, I saved a lot of time because all these actions took me about 15-20 minutes.

Sometimes there are problems because ChatGPT often loses context, and the training process has to start from scratch.

Conclusion:

In this topic, I shared my experience of using ChatGPT to optimize the writing of unit tests with specific examples.

Thanks to the help of the bot, you can quickly generate the basic test template and fill it with specific data.

Additionally, ChatGPT can suggest variations of tests and adjust the code based on input data.

It is essential to note that ChatGPT cannot completely replace a developer and their experience in writing tests. However, it can significantly save time by simplifying routine tasks and generating basic test templates.

Top comments (0)