ChatGPT в написании юнит тестов

ChatGPT в написании юнит тестов

ChatGPT в написании юнит тестов

Добро пожаловать в мир инновационных подходов к тестированию программного обеспечения! В данной статье я хочу поделиться своим опытом использования ChatGPT — мощной модели искусственного интеллекта — для написания Unit-тестов. 

Так начал статью ИИ по моей просьбе составить вступление к эссе о том, как использовать ChatGPT при написании Unit-тестов. Видимо, все-таки придется писать самому, ибо градус пафоса зашкаливает, а высокопарность вызывает испанский стыд. 

В этой статье я расскажу о своих наблюдениях и размышлениях при написании Unit-тестов с помощью ChatGPT. Я рассмотрю, как использовать ChatGPT для генерации тестовых сценариев, формулировки вопросов о правильности кода, создания заглушек (moq). 
 
Для начала давайте вспомним, что такое Unit-тесты.  

Unit-тестирование является фундаментальным инструментом в разработке программного обеспечения, который помогает обнаруживать ошибки и улучшать качество кода. Однако, написание Unit-тестов может быть трудоемким процессом, особенно когда приходится создавать множество тестовых сценариев для различных частей кода. 
При построении запроса для написания Unit-тестов следует учитывать несколько основных принципов, которые обеспечат эффективность и качество ваших тестов. Вот некоторые из них:  

  • Изолированность и независимость: каждый Unit-тест должен быть изолирован от других тестов и от внешних зависимостей, таких как базы данных, файловые системы или сетевые ресурсы. Используйте механизмы мокирования (moq) или фейков (fakes) для создания заглушек, которые эмулируют поведение внешних зависимостей. Это обеспечит независимость тестов и позволит изолировать ошибки. 

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

  • Покрытие функциональности: убедитесь, что ваши Unit- тесты покрывают все важные сценарии использования и граничные условия. Не забывайте о проверке неверных входных данных и обработке ошибок. Целью тестирования является проверка как правильного поведения кода, так и его неправильных вариантов. 

  • Тестирование различных путей выполнения: проверьте, что ваши Unit-тесты покрывают различные пути выполнения кода, включая разветвления и исключительные ситуации. Это поможет выявить потенциальные ошибки и неправильное поведение приложения в различных сценариях. 

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

  • Быстрота выполнения: старайтесь создавать Unit-тесты, которые выполняются быстро. Это позволит вам часто запускать тесты и получать результаты быстрее. Быстрый оборот Unit-тестов облегчит процесс разработки и снизит время обнаружения ошибок. 

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

Хочу заметить, что ChatGPT не предназначен для выполнения кода напрямую, включая написание Unit-тестов. Он обучен на огромном объеме текстовых данных и предназначен для генерации текста на основе предоставленного контекста. 

Однако, я могу показать пример вопроса, который можно было бы задать ChatGPT для получения сценариев тестов:  

Вопрос: «Какие тестовые случаи следует рассмотреть для метода CalculateTotalPrice в классе ShoppingCart, который принимает список товаров и возвращает общую стоимость?» 

Примерный ответ ChatGPT:  

  • Тестирование на пустой список товаров. Ожидаемый результат: вернуть 0, так как нет товаров для подсчета стоимости. 

  • Тестирование на один товар с положительной ценой. Ожидаемый результат: вернуть стоимость этого товара. 

  • Тестирование на несколько товаров с положительными ценами. Ожидаемый результат: вернуть сумму всех цен товаров. 

  • Тестирование на товары с отрицательными ценами. Ожидаемый результат: игнорировать такие товары и вернуть сумму только положительных цен. 

  • Тестирование на товары с нулевой ценой. Ожидаемый результат: вернуть сумму всех товаров с ненулевыми ценами. 

  • Тестирование на товары с дробными ценами. Ожидаемый результат: вернуть сумму всех товаров с округлением до двух десятичных знаков.  

Хотя этот пример не предполагает, что ChatGPT создаст полностью рабочие Unit-тесты, он может помочь сформулировать возможные сценарии для написания Unit-тестов. Однако эти сценарии должны быть реализованы программистами, а сами Unit-тесты — написаны и выполнены с использованием соответствующих фреймворков для тестирования в C# (например, NUnit, MSTest или xUnit). 

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

Следующим этап был практически Hello World, хотелось добиться того, что бы ИИ сгенерировал код.  

Вопрос:  
напиши тесты для xUnit на языке C# для калькулятора. 

{ 
    private readonly Calculator calculator;  

    public CalculatorTests() 

    { 

        calculator = new Calculator(); 

    } 

  

    [Fact] 

    public void Add_TwoPositiveNumbers_ReturnsSum() 

    { 

        // Arrange 

        int a = 5; 

        int b = 7;   

        // Act 

        int result = calculator.Add(a, b); 

  

        // Assert 

        Assert.Equal(12, result); 

    } 

  

    [Fact] 

    public void Add_PositiveAndNegativeNumber_ReturnsCorrectSum() 

    { 

        // Arrange 

        int a = 10; 

        int b = -5; 

  

        // Act 

        int result = calculator.Add(a, b); 

  

        // Assert 

        Assert.Equal(5, result); 

    } 

  

    [Fact] 

    public void Subtract_TwoPositiveNumbers_ReturnsDifference() 

    { 

        // Arrange 

        int a = 10; 

        int b = 5; 

  

        // Act 

        int result = calculator.Subtract(a, b); 

  

        // Assert 

        Assert.Equal(5, result); 

    } 

  

    [Fact] 

    public void Multiply_TwoPositiveNumbers_ReturnsProduct() 

    { 

        // Arrange 

        int a = 3; 

        int b = 4; 

  

        // Act 

        int result = calculator.Multiply(a, b); 

  

        // Assert 

        Assert.Equal(12, result); 

    } 

  

    [Fact] 

    public void Divide_DivisibleNumbers_ReturnsQuotient() 

    { 

        // Arrange 

        int a = 10; 

        int b = 2; 

  

        // Act 

        int result = calculator.Divide(a, b); 

  

        // Assert 

        Assert.Equal(5, result); 

    } 

  

    [Fact] 

    public void Divide_DivisionByZero_ThrowsDivideByZeroException() 

    { 

        // Arrange 

        int a = 10; 

        int b = 0; 

  

        // Act and Assert 

        Assert.Throws(() => calculator.Divide(a, b)); 

    } 

} 

Что, если честно, уже очень неплохо. Да, нужны доработки, но, они сопоставимы с рутинным код-ревьюю. Опять же, при правильно составленном запросе можно использовать сгенерированные Unit-тесты для TDD подхода. Немного воодушевившись, я скормил ему класс из своего проекта.  

Код:

using System; 

using System.Collections.Generic; 

using System.Linq; 

using System.Threading.Tasks; 

using AutoMapper; 

using BB.AssetPerformanceManagement.Application.Repositories; 

using BB.AssetPerformanceManagement.Domain.Models.Plant; 

using BB.AssetPerformanceManagement.Domain.SeedWork; 

using BB.AssetPerformanceManagement.Infrastructure.Entities.PlantEntities; 

using BB.AssetPerformanceManagement.Infrastructure.Exceptions; 

using BB.Common.Paginator; 

using Microsoft.EntityFrameworkCore; 

using Moq; 

using Xunit; 

  

namespace BB.AssetPerformanceManagement.Infrastructure.Repositories.Tests 

{ 

    public class PlantRepositoryTests 

    { 

        // Метод для создания мок-контекста и настройки данных 

        private AssetPerformanceManagementContext CreateMockContext() 

        { 

            var data = new List 

            { 

                new PlantDao { Id = Guid.NewGuid(), Name = "Plant A" }, 

                new PlantDao { Id = Guid.NewGuid(), Name = "Plant B" }, 

                new PlantDao { Id = Guid.NewGuid(), Name = "Plant C" } 

            }.AsQueryable(); 

  

            var mockSet = new Mock>(); 

            mockSet.As>().Setup(m => m.Provider).Returns(data.Provider); 

            mockSet.As>().Setup(m => m.Expression).Returns(data.Expression); 

            mockSet.As>().Setup(m => m.ElementType).Returns(data.ElementType); 

            mockSet.As>().Setup(m => m.GetEnumerator()).Returns(data.GetEnumerator()); 

  

            var mockContext = new Mock(); 

            mockContext.Setup(c => c.Plants).Returns(mockSet.Object); 

  

            return mockContext.Object; 

        } 

  

        [Fact] 

        public async Task GetAll_WithNoNameFilter_ReturnsAllPlants() 

        { 

            // Arrange 

            var mockPaginator = new Mock(); 

            var mockMapper = new Mock(); 

            var mockCommonRepository = new Mock>(); 

  

            var context = CreateMockContext(); 

            var repository = new PlantRepository(context, mockPaginator.Object, mockMapper.Object, mockCommonRepository.Object); 

  

            // Act 

            var result = await repository.GetAll(); 

  

            // Assert 

            Assert.Equal(3, result.Count); 

        } 

  

        [Fact] 

        public async Task GetAll_WithNameFilter_ReturnsFilteredPlants() 

        { 

            // Arrange 

            var mockPaginator = new Mock(); 

            var mockMapper = new Mock(); 

            var mockCommonRepository = new Mock>(); 

  

            var context = CreateMockContext(); 

            var repository = new PlantRepository(context, mockPaginator.Object, mockMapper.Object, mockCommonRepository.Object); 

  

            // Act 

            var result = await repository.GetAll("Plant A"); 

  

            // Assert 

            Assert.Single(result); 

            Assert.Equal("Plant A", result.First().Name); 

        } 

  

        [Fact] 

        public async Task GetById_ExistingPlantId_ReturnsPlant() 

        { 

            // Arrange 

            var mockPaginator = new Mock(); 

            var mockMapper = new Mock(); 

            var mockCommonRepository = new Mock>(); 

  

            var context = CreateMockContext(); 

            var repository = new PlantRepository(context, mockPaginator.Object, mockMapper.Object, mockCommonRepository.Object); 

  

            var existingPlantId = context.Plants.First().Id; 

  

            // Act 

            var result = await repository.GetById(existingPlantId); 

  

            // Assert 

            Assert.NotNull(result); 

            Assert.Equal(existingPlantId, result.Id); 

        } 

  

        [Fact] 

        public async Task GetById_NonExistingPlantId_ThrowsNotFoundException() 

        { 

            // Arrange 

            var mockPaginator = new Mock(); 

            var mockMapper = new Mock(); 

            var mockCommonRepository = new Mock>(); 

  

            var context = CreateMockContext(); 

            var repository = new PlantRepository(context, mockPaginator.Object, mockMapper.Object, mockCommonRepository.Object); 

  

            var nonExistingPlantId = Guid.NewGuid(); 

  

            // Act and Assert 

            await Assert.ThrowsAsync(async () => await repository.GetById(nonExistingPlantId)); 

        } 

    } 

}

Вопрос:  
напиши Unit-тесты на xUnit,    
Результат:

using System; 

using System.Collections.Generic; 

using System.Linq; 

using System.Threading.Tasks; 

using AutoMapper; 

using BB.AssetPerformanceManagement.Application.Repositories; 

using BB.AssetPerformanceManagement.Domain.Models.Plant; 

using BB.AssetPerformanceManagement.Domain.SeedWork; 

using BB.AssetPerformanceManagement.Infrastructure.Entities.PlantEntities; 

using BB.AssetPerformanceManagement.Infrastructure.Exceptions; 

using BB.Common.Paginator; 

using Microsoft.EntityFrameworkCore; 

using Moq; 

using Xunit; 

  

namespace BB.AssetPerformanceManagement.Infrastructure.Repositories.Tests 

{ 

    public class PlantRepositoryTests 

    { 

        // Метод для создания мок-контекста и настройки данных 

        private AssetPerformanceManagementContext CreateMockContext() 

        { 

            var data = new List 

            { 

                new PlantDao { Id = Guid.NewGuid(), Name = "Plant A" }, 

                new PlantDao { Id = Guid.NewGuid(), Name = "Plant B" }, 

                new PlantDao { Id = Guid.NewGuid(), Name = "Plant C" } 

            }.AsQueryable(); 

  

            var mockSet = new Mock>(); 

            mockSet.As>().Setup(m => m.Provider).Returns(data.Provider); 

            mockSet.As>().Setup(m => m.Expression).Returns(data.Expression); 

            mockSet.As>().Setup(m => m.ElementType).Returns(data.ElementType); 

            mockSet.As>().Setup(m => m.GetEnumerator()).Returns(data.GetEnumerator()); 

  

            var mockContext = new Mock(); 

            mockContext.Setup(c => c.Plants).Returns(mockSet.Object); 

  

            return mockContext.Object; 

        } 

  

        [Fact] 

        public async Task GetAll_WithNoNameFilter_ReturnsAllPlants() 

        { 

            // Arrange 

            var mockPaginator = new Mock(); 

            var mockMapper = new Mock(); 

            var mockCommonRepository = new Mock>(); 

  

            var context = CreateMockContext(); 

            var repository = new PlantRepository(context, mockPaginator.Object, mockMapper.Object, mockCommonRepository.Object); 

  

            // Act 

            var result = await repository.GetAll(); 

  

            // Assert 

            Assert.Equal(3, result.Count); 

        } 

  

        [Fact] 

        public async Task GetAll_WithNameFilter_ReturnsFilteredPlants() 

        { 

            // Arrange 

            var mockPaginator = new Mock(); 

            var mockMapper = new Mock(); 

            var mockCommonRepository = new Mock>(); 

  

            var context = CreateMockContext(); 

            var repository = new PlantRepository(context, mockPaginator.Object, mockMapper.Object, mockCommonRepository.Object); 

  

            // Act 

            var result = await repository.GetAll("Plant A"); 

  

            // Assert 

            Assert.Single(result); 

            Assert.Equal("Plant A", result.First().Name); 

        } 

  

        [Fact] 

        public async Task GetById_ExistingPlantId_ReturnsPlant() 

        { 

            // Arrange 

            var mockPaginator = new Mock(); 

            var mockMapper = new Mock(); 

            var mockCommonRepository = new Mock>(); 

  

            var context = CreateMockContext(); 

            var repository = new PlantRepository(context, mockPaginator.Object, mockMapper.Object, mockCommonRepository.Object); 

  

            var existingPlantId = context.Plants.First().Id; 

  

            // Act 

            var result = await repository.GetById(existingPlantId); 

  

            // Assert 

            Assert.NotNull(result); 

            Assert.Equal(existingPlantId, result.Id); 

        } 

  

        [Fact] 

        public async Task GetById_NonExistingPlantId_ThrowsNotFoundException() 

        { 

            // Arrange 

            var mockPaginator = new Mock(); 

            var mockMapper = new Mock(); 

            var mockCommonRepository = new Mock>(); 

  

            var context = CreateMockContext(); 

            var repository = new PlantRepository(context, mockPaginator.Object, mockMapper.Object, mockCommonRepository.Object); 

  

            var nonExistingPlantId = Guid.NewGuid(); 

  

            // Act and Assert 

            await Assert.ThrowsAsync(async () => await repository.GetById(nonExistingPlantId)); 

        } 

    } 

}

И даже написал пояснения к Unit-тестам:  
В этом примере мы использовали Moq для создания мок-контекста базы данных и настройки данных, которые будут использоваться в Unit-тестах. Затем мы создали несколько тестовых методов для проверки различных сценариев, таких как получение всех растений, получение растений с фильтром по имени и получение растения по идентификатору. 

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

При запуске тестов xUnit выполнит каждый из них и сообщит об успешном или неуспешном прохождении каждого теста. Если все тесты пройдут успешно, вы увидите сообщение об успешном выполнении. Если хотя бы один тест не пройдет, xUnit сообщит об ошибке и укажет, какой именно тест не выполнен.  

Дальше стало интереснее, и я купил подписку на ChatGPT, чтоб проверить работу 4-й версии. Я задал тот же вопрос. Но в этой версии ИИ работает более прихотливо и выдает только Unit-тесты для одного метода, и только для одного тест-кейса:  

using System.Collections.Generic; 

using System.Linq; 

using System.Threading; 

using System.Threading.Tasks; 

using AutoMapper; 

using ...Repositories; 

using ...Plant; 

using ...PlantEntities; 

using ...Infrastructure.Repositories; 

using ...Common.Paginator; 

using Microsoft.EntityFrameworkCore; 

using Moq; 

using Xunit; 

  

namespace ...Tests 

{ 

    public class PlantRepositoryTests 

    { 

        private readonly Mock _contextMock; 

        private readonly Mock _paginatorMock; 

        private readonly Mock _mapperMock; 

        private readonly Mock> _commonRepoMock; 

  

        public PlantRepositoryTests() 

        { 

            _contextMock = new Mock(); 

            _paginatorMock = new Mock(); 

            _mapperMock = new Mock(); 

            _commonRepoMock = new Mock>(); 

        } 

  

        [Fact] 

        public async Task GetAll_ReturnsCorrectPlants() 

        { 

            // Arrange 

            var plants = new List 

            { 

                new PlantDao { Id = Guid.NewGuid(), Name = "Plant1" }, 

                new PlantDao { Id = Guid.NewGuid(), Name = "Plant2" } 

            }.AsQueryable(); 

  

            var mockSet = new Mock>(); 

            mockSet.As>().Setup(m => m.Provider).Returns(plants.Provider); 

            mockSet.As>().Setup(m => m.Expression).Returns(plants.Expression); 

            mockSet.As>().Setup(m => m.ElementType).Returns(plants.ElementType); 

            mockSet.As>().Setup(m => m.GetEnumerator()).Returns(plants.GetEnumerator()); 

  

            _contextMock.Setup(c => c.Plants).Returns(mockSet.Object); 

            _paginatorMock.Setup(p => p.ApplyAsync(It.IsAny>(), It.IsAny())) 

                .ReturnsAsync(plants); 

            _mapperMock.Setup(m => m.Map>(It.IsAny>())) 

                .Returns(new List { new Plant { Id = Guid.NewGuid(), Name = "Plant1" }, new Plant { Id = Guid.NewGuid(), Name = "Plant2" } }); 

  

            var plantRepository = new PlantRepository(_contextMock.Object, _paginatorMock.Object, _mapperMock.Object, _commonRepoMock.Object); 

  

            // Act 

            var result = await plantRepository.GetAll(); 

  

            // Assert 

            Assert.Equal(2, result.Count); 

            Assert.Equal("Plant1", result.First().Name); 

            Assert.Equal("Plant2", result.Last().Name); 

        } 

  

        // You can write similar tests for GetById and GetByIds methods.  

    } 

}

Разница довольно велика, и подход версии 3.5 мне понравился больше. Я продолжил, и с помощью версии 4 сгенерировал все Unit-тесты для класса. Основное отличие — в 4-й версии разработчику нужно проявить большую вовлеченность, для получения результата. 
 
ChatGPT очень приятно удивил генерацией Unit-тестов — это хороший инструмент, который может сильно облегчить работу разработчику.  
На мой взгляд, правильный алгоритм его использования примерно такой:  

— Загрузить класс 
— с помощью ИИ составить стратегию тестирования 
— генерировать тестовые методы, в соответствии со стратегией 
— отладка 
… (при необходимости повторить :) 
— PROFIT 
 
Небольшая ложка дёгтя — без правильно поставленной цепочки вопросов, компетенций проведения код ревью и опыта отладки кода — это решение, пока еще, нежизнеспособно. Но если вы обладаете нужным опытом, то с ним можно работать. Через какое-то время, немного набив руку, можно будет выполнять такого рода задачи очень быстро. Но, используя такого рода системы, нужно не забывать, что ИИ постоянно обучается/меняется, как в позитивном, так и в негативном направлении, вот ссылка на исследования актуальные на июль 2023 года https://arxiv.org/pdf/2307.09009.pdf. Происходит это по воле руководства компании, регулирующих органов, пользователей троллей или в процессе естественного самообучения системы — я не знаю, но это существенный фактор, который говорит о необходимости контроля за выходными данными таких систем. 
Я не эксперт в области информационной безопасности, так что, это исключительно рассуждения, но вопрос возникает сам собой: скармливая этому инструменту программный код своего проекта или интегрируя данные решения в свой продукт, есть вероятность, что данные попадут в какие-то третьи системы, или будут проанализированы и могут быть использованы, например, для натаскивания систем по автоматизированным информационным атакам или банальное отключение от системы вас как пользователя.  

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

© Habrahabr.ru