Como ya se ha mencionado previamente, el programador tiene la tarea de plasmar en código una solución a un problema dado. Este sistema debe de llevar a cabo las ideas planteadas y trabajar con toda la información que pudiera ser necesaria para solucionar el problema planteado. Este trabajo, por sí mismo, es complejo y muy delicado, y como tal, así como en otras profesiones, existe la posibilidad de cometer un error.
En el caso de las piezas de software, pueden existir diversos tipos de error. De entre los más comunes al momento de desarrollar un sistema, un programador se puede enfrentar ante estos:
- Errores de sintaxis.
- Errores semánticos.
- Errores de lógica.
En el caso de los errores de sintaxis, éstos se refieren a que el programador ha escrito una instrucción de forma incorrecta. Algo similar a tener faltas de ortografía en una frase. Estos errores, sin embargo, suelen ser no tan complejos de solucionar, ya que las herramientas (llamadas compiladores) que apoyan al desarrollador a traducir su código al idioma interpretable por la máquina, se encargan, por sí mismas y de forma automática, de detectar este tipo de errores, e incluso, de proveer sugerencias de qué está mal y cómo se puede corregir. Algo similar a lo que hacen los editores de texto al marcar los errores de ortografía. Por tanto, estos errores, si bien los más frecuentes, suelen ser de los más sencillos se detectar, encontrar y solucionar.
Los errores semánticos hacen referencia a un tipo de error más abstracto y complejo de detectar. El siguiente ejemplo de código (de lenguaje C++) puede ejemplificar la situación:
if ( condicion1 )
if ( condicion2 )
instruccion1 ();
else
instruccion2 ();
La idea original del programador era definir la siguiente frase: “Si la condición 1 se cumple, continuaré con la condición 2. Si la condición 2 se cumple, entonces haré lo que indique ‘instruccion1’. Si la condición 1 no se cumple, entonces haré lo que indique ‘instruccion2’”. Como puede verse, la idea del programador es que ‘instruccion2’ se realice sólo si la condición 1 no se cumple. Si la condición 1 se cumple, ‘instruccion2’ nunca se usará. Esa es la idea original del programador.
Sin embargo, por la naturaleza misma del lenguaje, este código es procesado por el compilador de una forma diferente. En este caso, el compilador entiende lo siguiente: “Si la condición 1 se cumple, continuaré con la condición 2. Si la condición 2 se cumple, entonces haré lo que indique ‘instruccion1’. Si la condición 2 no se cumple, entonces haré lo que indique ‘instruccion2’”. Nótese que la diferencia está en que si la condición 1 no se cumple, no se hará nada, mientras que si la condición 2 no se cumple, entonces se usará ‘instruccion2’.
Un cambio como el anterior, puede desencadenar problemas importantes, cambiando por completo el comportamiento del sistema desarrollado. Este tipo de errores, si bien menos frecuentes que los de sintaxis, son más complejos de encontrar, pues no es tan sencillo identificar dónde se ha tomado una decisión incorrecta en un sistema, ni el porqué. Estos casos, suelen ser tediosos y consumen mucho tiempo. Pero, para la suerte de un desarrollador, no son imposibles de solucionar, ya que con relativa sencillez (comparados con otros errores), se logra identificar que algo va mal con el sistema desarrollado, en cuyo momento se comienza la investigación de dónde está el error.
Finalmente, tenemos los errores de lógica. El tipo de error que ningún desarrollador quisiera enfrentarse en su carrera. Este tipo de errores suelen ser más sutiles que los de sintaxis o semántica, ya que involucran situaciones no planeadas que no suelen resolverse tan fácilmente agregando algunas líneas extras. Algunos casos, dependiendo de la severidad, pueden requerir, incluso, el cambiar por completo una solución planteada, ya que el error que pudiera presentarse ha mostrado ir más allá de corregir el código escrito. El siguiente bloque de código muestra tal situación:
for ( int i = 0; i != 10; i++ )
if ( condicion )
i = i + 1;
Este código, si bien sencillo, puede ilustrar el tipo de errores relacionados a la lógica implementada. En este caso, el código establece lo siguiente: “Se tendrá un contador llamado ‘i’, el cual comienza con el valor 0. El valor de ‘i’ se incrementará de uno en uno de forma automática mientras que el valor de ‘i’ sea diferente de 10. Antes de revisar mi condición, le sumaré 1 a ‘i’. Si mi condición es cierta, revisaré mi contenido. Mi contenido establece que si se cumple cierta condición, le sume 1 a ‘i’.” La lógica mostrada parece no ser compleja ni tener fallas. Se espera que el valor de ‘i’ crezca de uno en uno, mientras que éste sea diferente de 10, momento en el que el código se detendrá pues habrá terminado. Los siguientes son valores posibles de ‘i’ cuando se revisa su valor, para saber si es 10 o no:
1, 2, 4, 5, 7, 8, 9, 10
Como se observa, en este caso, la condición fue cierta cuando ‘i’ valía 3, por lo que se incrementó en 1 y la condición que detendría al ciclo, se brincó al 3 mismo. Este ejemplo es totalmente posible. Sin embargo, por un error de lógica, no se contempló que la condición podría ser cierta cuando ‘i’ tuviera un valor de 9, momento en el que se le suma 1, convirtiéndose en 10. Una vez que termina la condición, el valor de ‘i’ se incrementa en 1 de forma automática, pasando a ser un 11. De esta forma, cuando se realiza la pregunta que detendría al código, ‘i’ ya no es 10, es un 11. En este momento, se dice que hay un error de lógica, ya que se dio una combinación de condiciones tales que nuestro código no detectó que debió detenerse, por lo que continuará con el 11, el 12, el 13, etc.
Este tipo de errores suelen ser los menos frecuentes, pero, a la vez, los más complejos de solucionar, ya que muchas veces su presencia es observada sólo en casos no comunes de uso del sistema. Este tipo de situaciones suelen presentarse cuando un usuario final usa un sistema, ya sea por desconocimiento de su uso (realizando acciones que jamás se plantearon como posibles) o por la simple casualidad de su uso (cuando un usuario intenta establecer una combinación de configuraciones que jamás se probaron).
Pero, ¿porqué se presentan este tipo de errores?
La respuesta a la pregunta anterior es compleja. Por un lado, un desarrollador, normalmente sólo probará aquel cambio que ha hecho en un sistema, y sólo ese. Así mismo, aún si probase todo el sistema, un desarrollador, de forma inconsciente, tiende a usar correctamente el sistema, pues lo conoce lo suficiente como para saber qué se puede hacer y qué no. Un desarrollador, casi de forma natural, omitirá acciones que sabe que no deberían hacerse con el sistema. Pero estos “instintos” suelen ser contraproducentes al momento de hacer pruebas, ya que un usuario no se detendrá a considerar si algo es posible o no con el sistema. Un usuario, en general, lo intentará.
Por las situaciones anteriores, es que se han planteado diversas estrategias para intentar encontrar los errores de lógica de un sistema, así como errores que pueden presentarse por condiciones de uso no contemplados:
- Smoke Tests
- Internal QA
- External QA
En mi experiencia profesional, he visto las tres estrategias, y todas son de un valor importante. Todas proveen diversos recursos que permiten a una empresa o grupo de desarrolladores el garantizar la calidad de un sistema al encontrar errores en un sistema para poder resolverlos.
Los Smoke Tests (o Pruebas de Humo), son pruebas rápidas que los desarrolladores pueden realizar sobre el sistema. En este caso, estas pruebas son rápidas, concretas y sencillas. El objetivo de estas pruebas suele ser solamente el verificar que las partes o funcionalidades más importantes de un sistema sigan funcionando bien, por lo que se le suele emplear cada cierto tiempo para garantizar que los cambios o adiciones que se han hecho a un sistema no han hecho que otras partes dejen de funcionar.
El Internal QA (o Aseguramiento de Calidad Interno) son pruebas extensivas que los propios desarrolladores pueden realizar al sistema. En este caso, las pruebas son completas. Se realiza una verificación de todas y cada una de las funcionalidades del sistema. Esta prueba, por su propia naturaleza, es más tardada y suele hacerse de forma menos frecuente, pero es una forma apropiada de garantizar que el sistema cuenta con una calidad considerable y que funciona tal cual se ha especificado que debería de funcionar. Esta prueba suele recomendarse hacerse antes de comenzar las pruebas de External QA.
El External QA (o Aseguramiento de Calidad Externo) son pruebas extensivas que un grupo independiente de personas (que no se han visto involucrados en el desarrollo del sistema) realizan al sistema. Estas pruebas suelen guiarse con listas de puntos que los desarrolladores les proporcionan, las cuales indican qué se ha de probar y cómo, así cómo qué resultado debería de verse. Estas pruebas suelen ser las más tardadas, a la vez que son las que encuentran más errores, ya que el personal involucrado en las pruebas, al no conocer el sistema, cómo funciona ni qué es capaz de hacer o no hacer, tienen la objetividad necesaria para adentrarse en acciones nunca contempladas, descubriendo errores bajo situaciones y condiciones no esperadas.
Algunos de los errores más comunes que he visto al terminar el trabajo de QA son estos:
- El sistema no inicia en equipos con ciertas características (poca memoria, poco espacio en disco, procesador muy lento, etc).
- El sistema deja de responder tras estar activo por más de X cantidad de horas.
- El manual dice que se debe de hacer la acción 1 y luego la acción 2, se descubrió que el sistema falla cuando se intenta hacer la acción 2 sin haber hecho la acción 1.
- El sistema reporta errores con una conexión lenta a internet.
- El sistema presenta problemas si no hay internet.
- Si se le proporciona información incorrecta en X paso, el sistema falla.
- Si se le proporciona información diferente a la esperada en X paso, el sistema da un resultado incorrecto.
Es por eso, que es importante que en una empresa haya un equipo de QA externo al de los desarrolladores. De esta forma, se logrará poder hacer pruebas de forma objetiva y real sobre el sistema probado. Todo desarrollador debe de comprender y aceptar que su trabajo, si bien de mucha calidad, es propenso a errores pues sería imposible encontrar a alguien que jamás cometa errores de lógica, ni que sea capaz de prever desde un inicio absolutamente todas las condiciones de uso de su sistema.