leanmind logo leanmind text logo

Blog

TDD Avanzado

Las herramientas que se necesitan para aplicar TDD en el mundo real en cualquier proyecto.

Explorando la eficiencia de Codium AI

Por Rubén Zamora

Introducción

Este artículo explora cómo Codium AI puede hacer para ayudarnos a desarrollar código y agilizar la creación de la batería de tests. Lo que intentaré simular es que estamos en un código legacy y tendremos que trabajar en él sin contexto alguno, pero nos ha pedido que añadamos una nueva funcionalidad y veremos si Codium AI nos puede ayudar con esto, teniendo en cuenta el tener controlados “corner cases” que puedan aparecer.

¿Qué es Codium AI?

Es una plataforma impulsada por Inteligencia Artificial que han sacado un plugin para nuestros IDE, Visual Studio Code o JetBrains, con matices el último, porque no está para toda la suite. La herramienta dice que es capaz de generarnos tests del código que analiza, es capaz de poder documentarlo y/o explicarlo.

¿Es esto seguro para trabajar con mi código de cliente, qué pasa con la privacidad?

Si leemos la política de privacidad que tienen, aparentemente parece seguro, pero es más interesante la versión de pago, que dice que nunca se utiliza para entrenar los modelos de IA. Los datos de Teams y Enterprise se borran en un plazo de 48 horas, con el fin de poder resolver problemas que se puedan dar.

En mi caso, evitaría trabajar con código de cliente si no es utilizando la versión de pago, y aún así siempre teniendo cuidado.

¿Qué son los Corner Cases?

Los “corner cases” o casos extremos son situaciones que ocurren fuera de las condiciones normales de operación. A menudo son inesperados, y pueden causar comportamientos anómalos en el software si no se manejan correctamente. Por lo tanto, identificar y manejar estos casos es crucial para garantizar un software robusto.

Herramientas en uso

Para el análisis, nos basamos en las siguientes herramientas y tecnologías:

Comenzamos

En principio como voy a utilizar C# para ponerlo a prueba, pensaba usar Rider como IDE, pero parece que ahora mismo para C# sólo tienen habilitado el plugin para Visual Studio Code, por lo que lo instalaremos ahí y trabajaremos con él.

Ya al posicionarnos en la clase que queremos analizar para testear la GildedRose, nos comienza a generar los test de comportamiento:

image1

Es increíble que de manera tan rápida, ya tengamos varios tests como arnés de seguridad, que podemos usar.

Si pinchas sobre uno, te da una preview del código que va a generar. Por ejemplo, seleccionamos el edge case “Quality of normal items never goes below 0

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
[Fact]
public void test_quality_of_normal_items_never_goes_below_0()
{
    // Arrange
    IList<Item> items = new List<Item> { new Item { Name = "Normal Item", SellIn = 10, Quality = 5 } };
    GildedRose gildedRose = new GildedRose(items);

    // Act
    gildedRose.UpdateQuality();

    // Assert
    Assert.Equal(4, items[0].Quality);
}

Por ahora, solo vemos que te genera el método de la clase de tests, no todo el fichero entero que necesitarías para poder lanzar el test, pero bueno, males menores.

En cierta manera, es increíble como hemos podido montar 15 casos de test de un tirón, aunque tengamos que modificar alguno que otro para perfeccionarlo y dejarlo mejor, tenemos un boilerplate que nos hará tener más foco en los casos más difíciles de detectar.

He realizado otro ejemplo con la kata Mars Rover, haciendo una comparativa más de si puede ayudarme a crear tests de valor que merezcan la pena mantener, y aquí mi clase Mars Rover aplica el patrón estado para hacer los cambios de orientación. Viendo los tests que me llega a generar, están bien como tests arnés, pero no podemos acostumbrarnos en esta fase todavía por madurar de la herramienta, a dejar este tipo de tests como válidos.

image2

Por ejemplo, estos que me genera:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// TurnLeft rotates the rover 90 degrees to the left
[Fact]
public void test_turn_left()
{
    // Arrange
    Gps gps = new Gps(0, 0);
    Facing initialFacing = new NorthFacing();
    MarsRovers rover = new MarsRovers(gps, initialFacing);

    // Act
    rover.TurnLeft();

    // Assert
    Assert.Equal('W', rover.GetFacing());
}


// TurnRight rotates the rover 90 degrees to the right
[Fact]
public void test_turn_right()
{
    // Arrange
    Gps gps = new Gps(0, 0);
    Facing initialFacing = new NorthFacing();
    MarsRovers rover = new MarsRovers(gps, initialFacing);

    // Act
    rover.TurnRight();

    // Assert
    Assert.Equal('E', rover.GetFacing());
}

Se podrían sacar con un [Theory] y viendo más escenarios. Además, considero más interesante apoyarnos en FluentAssertions para hacer más legibles los asserts.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
    public static IEnumerable<object[]> TurnRightData()
    {
        yield return new object[] { new FacingNorth(), 'E' };
        yield return new object[] { new FacingEast(), 'S' };
        yield return new object[] { new FacingSouth(), 'W' };
        yield return new object[] { new FacingWest(), 'N' };
    }
    
    [Theory]
    [MemberData(nameof(TurnRightData))]
    public void turn_right(Facing initialFacing, char expectedFacing)
    {
        var marsRovers = new MarsRovers(new Gps(0, 0), initialFacing);

        marsRovers.Move(new[] { Right });
        
        marsRovers.GetFacing().Should().Be(expectedFacing);
    }

Con esto quiero decir que la herramienta está genial, pero que no podemos dormirnos en los laureles y olvidarmos cómo usar el framework de testing y sus diferentes opciones, para mejorar nuestra pila de tests. Es fácil que podamos adoptar la manía “que lo haga la IA, yo me desentiendo, seguro que están bien” y perdamos facultades de tener mentalidad de testing.

Opción de Enhance Code

image3

Vemos que el código de esta kata es enrevesado, con varios “if” encadenados, números mágicos, bucles un poco difícil seguirle el trazo.

Antes ```
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
    using System.Collections.Generic;
    
    namespace csharp
    {
    public class GildedRose
    {
    IList<Item> Items;
    public GildedRose(IList<Item> Items)
    {
    this.Items = Items;
    }
    
            public void UpdateQuality()
            {
                for (var i = 0; i < Items.Count; i++)
                {
                    if (Items[i].Name != "Aged Brie" && Items[i].Name != "Backstage passes to a TAFKAL80ETC concert")
                    {
                        if (Items[i].Quality > 0)
                        {
                            if (Items[i].Name != "Sulfuras, Hand of Ragnaros")
                            {
                                Items[i].Quality = Items[i].Quality - 1;
                            }
                        }
                    }
                    else
                    {
                        if (Items[i].Quality < 50)
                        {
                            Items[i].Quality = Items[i].Quality + 1;
    
                            if (Items[i].Name == "Backstage passes to a TAFKAL80ETC concert")
                            {
                                if (Items[i].SellIn < 11)
                                {
                                    if (Items[i].Quality < 50)
                                    {
                                        Items[i].Quality = Items[i].Quality + 1;
                                    }
                                }
    
                                if (Items[i].SellIn < 6)
                                {
                                    if (Items[i].Quality < 50)
                                    {
                                        Items[i].Quality = Items[i].Quality + 1;
                                    }
                                }
                            }
                        }
                    }
    
                    if (Items[i].Name != "Sulfuras, Hand of Ragnaros")
                    {
                        Items[i].SellIn = Items[i].SellIn - 1;
                    }
    
                    if (Items[i].SellIn < 0)
                    {
                        if (Items[i].Name != "Aged Brie")
                        {
                            if (Items[i].Name != "Backstage passes to a TAFKAL80ETC concert")
                            {
                                if (Items[i].Quality > 0)
                                {
                                    if (Items[i].Name != "Sulfuras, Hand of Ragnaros")
                                    {
                                        Items[i].Quality = Items[i].Quality - 1;
                                    }
                                }
                            }
                            else
                            {
                                Items[i].Quality = Items[i].Quality - Items[i].Quality;
                            }
                        }
                        else
                        {
                            if (Items[i].Quality < 50)
                            {
                                Items[i].Quality = Items[i].Quality + 1;
                            }
                        }
                    }
                }
            }
        }
    }
    
```

Y parece que es capaz de dejarlo bastante decente.

Después ```
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
    using System.Collections.Generic;
    
    namespace csharp
    {
        public class GildedRose
        {
            private IList<Item> Items;
    
            public GildedRose(IList<Item> items)
            {
                Items = items;
            }
    
            public void UpdateQuality()
            {
                foreach (var item in Items)
                {
                    UpdateItemQuality(item);
                    UpdateItemSellIn(item);
                }
            }
    
            private void UpdateItemQuality(Item item)
            {
                if (item.Name == "Sulfuras, Hand of Ragnaros")
                {
                    return;
                }
    
                if (item.Name == "Aged Brie")
                {
                    IncreaseQuality(item);
                    if (item.SellIn < 0)
                    {
                        IncreaseQuality(item);
                    }
                }
                else if (item.Name == "Backstage passes to a TAFKAL80ETC concert")
                {
                    IncreaseQuality(item);
                    if (item.SellIn < 11)
                    {
                        IncreaseQuality(item);
                    }
                    if (item.SellIn < 6)
                    {
                        IncreaseQuality(item);
                    }
                    if (item.SellIn < 0)
                    {
                        item.Quality = 0;
                    }
                }
                else
                {
                    DecreaseQuality(item);
                    if (item.SellIn < 0)
                    {
                        DecreaseQuality(item);
                    }
                }
            }
    
            private void UpdateItemSellIn(Item item)
            {
                if (item.Name != "Sulfuras, Hand of Ragnaros")
                {
                    item.SellIn--;
                }
            }
    
            private void IncreaseQuality(Item item)
            {
                if (item.Quality < 50)
                {
                    item.Quality++;
                }
            }
    
            private void DecreaseQuality(Item item)
            {
                if (item.Quality > 0)
                {
                    item.Quality--;
                }
            }
        }
    }
    }
    
```

Pero mi opinión con este caso, es que creo que la IA tiene más que trabajadas este tipo de katas. Es capaz de dar un refactor bastante bueno para haberlo hecho ella sola. Normalmente los código legacy, tienen mucho más contexto de negocio que necesitaría saber, para poder aplicar bien un refactor, sin limitarse a mejoras que te podría dar el SonarQube. Pero démosle tiempo, porque si ya es capaz de hacer esto, ¿Qué podrá hacer en el futuro?

Publicado el 10/01/2024 por

¿Quieres más? te invitamos a suscribirte a nuestro boletín para avisarte cada vez que recopilemos contenido de calidad que compartir.

Si disfrutas leyendo nuestro blog, ¿imaginas lo divertido que sería trabajar con nosotros? ¿te gustaría?

Impulsamos el crecimiento profesional de tu equipo de developers