Skip to main content

Escritura de pruebas con GitHub Copilot

Usa Copilot para generar pruebas unitarias y de integración, así como ayudar a mejorar la calidad del código.

Introducción

GitHub Copilot puede ayudarte a desarrollar pruebas con rapidez y a mejorar la productividad. En este artículo, vamos a demostrar cómo puedes usar Copilot para escribir pruebas tanto unitarias como de integración. Aunque Copilot funciona bien en la generación de pruebas para funciones básicas, los escenarios complejos requieren indicaciones y estrategias más detalladas. En este artículo, se describen ejemplos prácticos del uso de Copilot para desglosar las tareas y comprobar la corrección del código.

Requisitos previos

Antes de comenzar, necesitarás lo siguiente:

Escritura de pruebas unitarias con el Copilot Chat

En esta sección, vamos a explorar cómo usar GitHub Copilot Chat a fin de generar pruebas unitarias para una clase de Python. En este ejemplo, se muestra cómo puedes usar Copilot a fin de crear pruebas unitarias para una clase como BankAccount. Te mostraremos cómo solicitar a Copilot que genere pruebas, las ejecute y compruebe los resultados.

Clase de ejemplo: BankAccount

Vamos a comenzar con una clase BankAccount que contiene métodos para realizar depósitos, retirar fondos y obtener el saldo de una cuenta. Crea un archivo bank_account.py en un repositorio de GitHub y agrega la siguiente clase BankAccount en Python.

class BankAccount:
    def __init__(self, initial_balance=0):
        if initial_balance < 0:
            raise ValueError("Initial balance cannot be negative.")
        self.balance = initial_balance

    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("Deposit amount must be positive.")
        self.balance += amount

    def withdraw(self, amount):
        if amount <= 0:
            raise ValueError("Withdrawal amount must be positive.")
        if amount > self.balance:
            raise ValueError("Cannot withdraw more than the current balance.")
        self.balance -= amount

    def get_balance(self):
        return self.balance

Generación de pruebas unitarias con el Copilot Chat

Puedes solicitar a Copilot que escriba pruebas que cubran una variedad de escenarios. Debes especificar bien los requisitos de la prueba para obtener los mejores resultados. A continuación, se muestra una solicitud de ejemplo que puedes usar a fin de generar pruebas unitarias para la clase BankAccount.

Solicitud: Desarrolla un conjunto completo de pruebas unitarias para la función BankAccount() en Python. Escribe varios métodos de prueba que cubran una amplia gama de escenarios e incluyan casos perimetrales, control de excepciones y validación de datos.

Esta solicitud hace algunas cosas importantes:

  • Ámbito: pide de forma explícita un conjunto integral de pruebas unitarias, por lo que Copilot no se limita a generar solo pruebas básicas y abarca una variedad de posibles casos de uso.
  • Escenarios: la solicitud especifica que las pruebas deben incluir:
    • Casos perimetrales: comprueba las condiciones de límite (por ejemplo, valores negativos para los depósitos o retiros, cuando se retira una cantidad superior al saldo disponible).
    • Control de excepciones: pruebas para errores que deben activarse cuando se producen determinadas acciones no válidas (por ejemplo, retirar una cantidad superior al saldo disponible, depositar cantidades negativas).
    • Validación de datos: garantiza que los métodos validen y controlen las entradas correctamente.

Copilot le proporcionará una respuesta similar a la siguiente.

import unittest
from bank_account import BankAccount

class TestBankAccount(unittest.TestCase):
    def setUp(self):
        self.account = BankAccount()

    def test_initial_balance(self):
        self.assertEqual(self.account.get_balance(), 0)

    def test_deposit_positive_amount(self):
        self.account.deposit(100)
        self.assertEqual(self.account.get_balance(), 100)

    def test_withdraw_within_balance(self):
        self.account.deposit(100)
        self.account.withdraw(50)
        self.assertEqual(self.account.get_balance(), 50)

    def test_deposit_negative_amount_raises_error(self):
        with self.assertRaises(ValueError):
            self.account.deposit(-100)

    def test_withdraw_negative_amount_raises_error(self):
        with self.assertRaises(ValueError):
            self.account.withdraw(-50)

    def test_withdraw_more_than_balance_raises_error(self):
        self.account.deposit(100)
        with self.assertRaises(ValueError):
            self.account.withdraw(200)

    def test_initial_balance_negative_raises_error(self):
        with self.assertRaises(ValueError):
            BankAccount(-100)

if __name__ == '__main__':
    unittest.main()

Ejecución de los casos de pruebas

El conjunto de pruebas que genera Copilot incluye un caso de prueba para cada escenario especificado en la solicitud. Se prueba cada una de las funciones de la clase BankAccount para ver si se cumplen una serie de condiciones, incluidas operaciones válidas como los depósitos y retiros, además de casos perimetrales como importes negativos e intentos de retirar cantidades superiores al saldo disponible.

Una vez que estés satisfecho con el conjunto de pruebas que Copilot ha generado, agrega el código a un nuevo archivo test_bank_account.py. Puedes preguntarle cómo ejecutar las pruebas.

Solicitud: "¿Cómo puedo ejecutar estas pruebas unitarias en Python con el marco unittest?"

Copilot te proporcionará el siguiente comando de Bash.

python -m unittest test_bank_account.py

Después de ejecutar las pruebas, verás la salida en tu terminal o IDE. Si se superan todas las pruebas, no hay duda de que la clase BankAccount funciona según lo previsto.

Comando de barra oblicua

Además, puedes solicitar a Copilot que escriba un conjunto completo de pruebas unitarias con el comando de barra diagonal /tests. Asegúrate de tener abierto el archivo en la pestaña actual del IDE y Copilot generará pruebas unitarias para ese archivo. Puede que las pruebas que Copilot genera no abarquen todos los escenarios, por lo que siempre debes revisar el código generado y agregar cualquier prueba adicional que pueda ser necesaria.

Tip

Si pides a Copilot que escriba pruebas para un archivo de código que no cubren las pruebas unitarias, puedes abrir uno o varios archivos de prueba existentes en pestañas adyacentes del editor para proporcionar a Copilot contexto útil. Copilot podrá ver el marco de pruebas que usas, por lo que será más probable que escriba una prueba coherente con tus pruebas existentes.

Copilot generará un conjunto de pruebas unitarias como el siguiente.

import unittest
from bank_account import BankAccount

class TestBankAccount(unittest.TestCase):
    def setUp(self):
        self.account = BankAccount()

    def test_initial_balance(self):
        self.assertEqual(self.account.get_balance(), 0)

Escritura de pruebas de integración con Copilot

Las pruebas de integración son esenciales para garantizar que los distintos componentes del sistema funcionan de forma correcta al combinarse. En esta sección, ampliaremos nuestra clase BankAccount para incluir interacciones con un servicio NotificationSystem externo y usaremos simulacros para probar el comportamiento del sistema sin necesidad de establecer conexiones reales. El objetivo de las pruebas de integración es comprobar la interacción entre la clase BankAccount y los servicios NotificationSystem para garantizar que funcionan correctamente de forma conjunta.

Clase de ejemplo: BankAccount con servicios de notificación

Vamos a actualizar la clase BankAccount para incluir interacciones con un servicio externo como NotificationSystem, que envía notificaciones a los usuarios. NotificationSystem representa la integración que debe probarse.

Actualiza la clase BankAccount en el archivo bank_account.py con el siguiente fragmento de código.

class BankAccount:
    def __init__(self, initial_balance=0, notification_system=None):
        if initial_balance < 0:
            raise ValueError("Initial balance cannot be negative.")
        self.balance = initial_balance
        self.notification_system = notification_system

    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("Deposit amount must be positive.")
        self.balance += amount
        if self.notification_system:
            self.notification_system.notify(f"Deposited {amount}, new balance: {self.balance}")

    def withdraw(self, amount):
        if amount <= 0:
            raise ValueError("Withdrawal amount must be positive.")
        if amount > self.balance:
            raise ValueError("Cannot withdraw more than the current balance.")
        self.balance -= amount
        
        if self.notification_system:
            self.notification_system.notify(f"Withdrew {amount}, new balance: {self.balance}")
        
    def get_balance(self):
        return self.balance

Aquí desglosaremos la solicitud que hemos hecho a Copilot respecto a escribir pruebas de integración para la clase BankAccount en partes más pequeñas y manejables. Esto ayudará a Copilot a generar pruebas más precisas y pertinentes.

Solicitud: "Escribe pruebas de integración para la función deposit en la clase BankAccount. Usa simulaciones de NotificationSystem y comprueba que se le llama correctamente después de un depósito".

Esta solicitud hace algunas cosas importantes:

  • Ámbito: especifica pruebas de integración, para lo que se centra en la interacción entre la función deposit y NotificationSystem, en lugar de solo pruebas unitarias.
  • Simulacros: solicita de forma explícita el uso de simulacros para la simulación de NotificationSystem con la garantía de que se prueba la interacción con los sistemas externos sin depender de su implementación real.
  • Verificación: la solicitud enfatiza que hay que comprobar que se llama correctamente a NotificationSystem después de un depósito, lo que garantiza que la integración entre los componentes funciona según lo previsto.
  • Especificidad: la solicitud indica claramente el método (deposit) y la clase (BankAccount) que se van a probar.

Tip

Si Copilot genera pruebas no válidas, proporciona ejemplos de entradas y salidas para la función que quieres probar. Esto ayudará a Copilot a evaluar el comportamiento esperado de la función.

Copilot generará un conjunto de pruebas como el siguiente.

import unittest
from unittest.mock import Mock
from bank_account import BankAccount

class TestBankAccountIntegration(unittest.TestCase):
    def setUp(self):
        self.notification_system = Mock()

    def test_deposit_with_notification(self):
        account = BankAccount(initial_balance=100, notification_system=self.notification_system)
        account.deposit(50)
        self.assertEqual(account.get_balance(), 150)
        self.notification_system.notify.assert_called_once_with("Deposited 50, new balance: 150")
        
if __name__ == '__main__':
    unittest.main()

Agrega el código generado a un nuevo archivo test_bank_account_integration.py.

Mejora de los casos de prueba

La solicitud anterior ha generado un único caso de prueba que comprueba que se llama a NotificationSystem cuando se realiza un depósito válido. Sin embargo, no cubre los casos en los que se genera un error durante el depósito. En esos escenarios, no debe llamarse a NotificationSystem. Debemos agregar un caso de prueba que aborde los depósitos no válidos y garantice que no se desencadene el sistema de notificación.

Solicitud: "Agrega un caso de prueba de importes de depósito no válidos para comprobar que la función genera las excepciones correctas y que no se llama a NotificationService".

Copilot generará un caso de prueba como el siguiente.

    def test_deposit_negative_amount_raises_error(self):
        account = BankAccount(initial_balance=100, notification_system=self.notification_system)
        with self.assertRaises(ValueError):
            account.deposit(0)
        self.notification_system.notify.assert_not_called()

Preguntas sobre las áreas de mejora

Ahora que hemos escrito casos de prueba para validar la funcionalidad de integración de los depósitos, se nos presenta una gran oportunidad para buscar mejoras en el conjunto de pruebas. Si bien las pruebas actuales son funcionales, podemos solicitar a Copilot que evalúe la cobertura de código y sugiera áreas de mejora.

Solicitud: "¿Qué otras pruebas deben incluirse para garantizar la cobertura completa de la integración entre la clase BankAccount y NotificationSystem?"

Plantear esta pregunta a Copilot puede ayudarte a identificar casos de prueba no señalados que pueden haberse pasado por alto. En esta situación, si bien se han probado los depósitos válidos y no válidos, no hemos cubierto aún la funcionalidad de retirada de fondos.

Copilot generará un conjunto de pruebas actualizado como el siguiente.

Haz clic para expandir el ejemplo de código generado completo.
import unittest
from unittest.mock import Mock
from bank_account import BankAccount

class TestBankAccountIntegration(unittest.TestCase):
    def setUp(self):
        self.notification_system = Mock()

    def test_deposit_with_notification(self):
        account = BankAccount(initial_balance=100, notification_system=self.notification_system)
        account.deposit(50)
        self.assertEqual(account.get_balance(), 150)
        self.notification_system.notify.assert_called_once_with("Deposited 50, new balance: 150")

    def test_deposit_negative_amount_raises_error(self):
        account = BankAccount(initial_balance=100, notification_system=self.notification_system)
        with self.assertRaises(ValueError):
            account.deposit(-50)
        self.notification_system.notify.assert_not_called()

    def test_deposit_zero_amount_raises_error(self):
        account = BankAccount(initial_balance=100, notification_system=self.notification_system)
        with self.assertRaises(ValueError):
            account.deposit(0)
        self.notification_system.notify.assert_not_called()

    def test_withdraw_with_notification(self):
        account = BankAccount(initial_balance=100, notification_system=self.notification_system)
        account.withdraw(30)
        self.assertEqual(account.get_balance(), 70)
        self.notification_system.notify.assert_called_once_with("Withdrew 30, new balance: 70")

    def test_withdraw_exceeding_balance_raises_error(self):
        account = BankAccount(initial_balance=100, notification_system=self.notification_system)
        with self.assertRaises(ValueError):
            account.withdraw(150)
        self.notification_system.notify.assert_not_called()

    def test_withdraw_negative_amount_raises_error(self):
        account = BankAccount(initial_balance=100, notification_system=self.notification_system)
        with self.assertRaises(ValueError):
            account.withdraw(-30)
        self.notification_system.notify.assert_not_called()

    def test_withdraw_zero_amount_raises_error(self):
        account = BankAccount(initial_balance=100, notification_system=self.notification_system)
        with self.assertRaises(ValueError):
            account.withdraw(0)
        self.notification_system.notify.assert_not_called()

    def test_initial_negative_balance_raises_error(self):
        with self.assertRaises(ValueError):
            BankAccount(initial_balance=-100, notification_system=self.notification_system)

if __name__ == '__main__':
    unittest.main()

Una vez que Copilot haya generado un conjunto de pruebas que cumpla tus expectativas, ejecuta las pruebas con el comando siguiente para comprobar los resultados.

python -m unittest test_bank_account_integration.py