Todos los sistemas sufren una
evolución a lo largo de su vida activa. En cada nueva versión se supone que o
bien se corrigen defectos, o se añaden nuevas funciones, o ambas cosas. En
cualquier caso, una nueva versión exige una nueva pasada por las pruebas. Si
éstas se han sistematizado en una fase anterior, ahora pueden volver a pasarse
automáticamente, simplemente para comprobar que las modificaciones no provocan
errores donde antes no los había.
Las pruebas de regresión son
particularmente espectaculares cuando se trata de probar la interacción con un
agente externo. Existen empresas que viven de comercializar productos que
"graban" la ejecución de una prueba con operadores humanos para luego
repetirla cuantas veces haga falta "reproduciendo la grabación". Y,
obviamente, deben monitorizar la respuesta del sistema en ambos casos,
compararla, y avisar de cualquier discrepancia significativa.
Recorridos (walkthroughs)
Quizás es una técnica más aplicada
en control de calidad que en pruebas. Consiste en sentar alrededor de una mesa
a los desarrolladores y a una serie de críticos, bajo las órdenes de un
moderador que impida un recalentamiento de los ánimos. El método consiste en
que los revisores se leen el programa línea a línea y piden explicaciones de
todo lo que no está meridianamente claro. Puede que simplemente falte un
comentario explicativo, o que detecten un error auténtico o que simplemente el
código sea tan complejo de entender/explicar que más vale que se rehaga de
forma más simple. Para un sistema complejo pueden hacer falta muchas sesiones.
Esta técnica es muy eficaz
localizando errores de naturaleza local; pero falla estrepitosamente cuando el
error deriva de la interacción entre dos partes alejadas del programa. Nótese
que no se está ejecutando el programa, sólo mirándolo con lupa, y de esta forma
sólo se ve en cada instante un trocito del listado.
Aleatorias (random testing)
Ciertos autores consideran
injustificada una aproximación sistemática a las pruebas. Alegan que la
probabilidad de descubrir un error es prácticamente la misma si se hacen una
serie de pruebas aleatoriamente elegidas, que si se hacen siguiendo las
instrucciones dictadas por criterios de cobertura (caja negra o blanca).
Como esto es muy cierto,
probablemente sea muy razonable comenzar la fase de pruebas con una serie de
casos elegidos al azar. Esto pondrá de manifiesto los errores más patentes. No
obstante, pueden permanecer ocultos errores más sibilinos que sólo se muestran
ante entradas muy precisas.
Si el programa es poco crítico
(una aplicación personal, un juego, ...) puede que esto sea suficiente. Pero si
se trata de una aplicación militar o con riesgo para vidas humanas, es de todo
punto insuficiente.
Solidez (robustness testing)
Se prueba la capacidad del sistema
para salir de situaciones embarazosas provocadas por errores en el suministro
de datos. Estas pruebas son importantes en sistemas con una interfaz al
exterior, en particular cuando la interfaz es humana.
Por ejemplo, en un sistema que
admite una serie de órdenes (commands) se deben probar los siguientes extremos:
·órdenes correctas, todas y cada
una
·órdenes con defectos de sintaxis,
tanto pequeñas desviaciones como errores de bulto
·órdenes correctas, pero en orden
incorrecto, o fuera de lugar
·la orden nula (línea vacía, una o
más)
·órdenes correctas, pero con datos
de más
·provocar una interrupción (BREAK,
^C, o lo que corresponda al sistema soporte) justo después de introducir una orden.
·órdenes con delimitadores
inapropiados (comas, puntos, ...)
·órdenes con delimitadores
incongruentes consigo mismos (por ejemplo, esto]
Aguante (stress testing)
En ciertos sistemas es conveniente
saber hasta dónde aguantan, bien por razones internas (¿hasta cuantos datos
podrá procesar?), bien externas (¿es capaz de trabajar con un disco al 90%?,
¿aguanta una carga de la CPU del 90?, etc.)
Prestaciones (performance testing)
A veces es importante el tiempo de
respuesta, u otros parámetros de gasto. Típicamente nos puede preocupar cuánto
tiempo le lleva al sistema procesar tantos datos, o cuánta memoria consume, o
cuánto espacio en disco utiliza, o cuántos datos transfiere por un canal de
comunicaciones, o ... Para todos estos parámetros suele ser importante conocer
cómo evolucionan al variar la dimensión del problema (por ejemplo, al
duplicarse el volumen de datos de entrada).
Mutación (mutation testing)
Es una técnica curiosa consistente
en alterar ligeramente el sistema bajo pruebas (introduciendo errores) para
averiguar si nuestra batería de pruebas es capaz de detectarlo. Si no, más vale
introducir nuevas pruebas. Todo esto es muy laborioso y francamente artesano.
ACTIVIDAD
1 (OBLIGATORIA)
Consulta los siguientes enlaces. Elige un error
de cada enlace y elabora un documento donde los expliques con tus palabras.
Pruebas de caja negra
(Pruebas Funcionales): no conocemos la implementación del
código, sólo la interfaz. Tan sólo podemos probar dando distintos valores
a las entradas y salidas.
Pruebas de caja blanca (Pruebas
Estructurales): conocemos el código (la implementación de
éste) que se va a ejecutar y podemos definir las pruebas que cubran todos
los posibles caminos del código.
Según el grado de automatización:
Pruebas manuales:
son las que se hacen normalmente al programar o las que ejecuta una
persona con la documentación generada durante la codificación (P. ej.-
comprobar cómo se visualiza el contenido de una página web en dos
navegadores diferentes).
Pruebas automáticas:
se usa un determinado software para sistematizar las pruebas y obtener
los resultados de las mismas (P. ej.- verificar un método de ordenación).
En función de qué se prueba:
Pruebas unitarias:
se aplican a un componente del software. Podemos considerar como
componente (elemento indivisible) a una función, una clase, una librería,
etc. En nuestro caso, generalmente hablaremos de una clase como
componente de software.
Pruebas de integración: consiste en
construir el sistema a partir de los distintos componentes y probarlo con
todos integrados. Estas pruebas deben realizarse progresivamente. Se
centran en probar la coherencia
semántica entre los diferentes módulos, tanto de semántica estática (se
importan los módulos adecuados; se llama correctamente a los
procedimientos proporcionados por cada módulo), como de semántica
dinámica (un módulo recibe de otro lo que esperaba). Normalmente estas
pruebas se van realizando por etapas, englobando progresivamente más y
más módulos en cada prueba.
oPruebas funcionales: sobre el sistema funcionando se comprueba
que cumple con la especificación (normalmente a través de los casos de uso).
Pruebas de rendimiento:
los tres primeros tipos de pruebas de los que ya se ha hablado comprueban
la eficacia del sistema. Las pruebas de rendimiento se basan en comprobar
que el sistema puede soportar el volumen de carga definido en la
especificación, es decir, hay que comprobar la eficiencia (P. ej.- Se ha
montado una página web sobre un servidor y hay que probar qué capacidad
tiene, estado de aceptar peticiones).
Pruebas de aceptación:
son las únicas pruebas que son realizadas por los usuarios, todas las
anteriores las lleva a cabo el equipo de desarrollo. Podemos distinguir
entre dos pruebas:
Pruebas alfa: las
realiza el usuario en presencia de personal de desarrollo del proyecto
haciendo uso de una máquina preparada para tal fin.
Pruebas beta:
las realiza el usuario después de que el equipo de desarrollo les
entregue una versión casi definitiva del producto.
El cliente final decide qué pruebas va a aplicarle al producto antes de
darlo por bueno y pagarlo. De nuevo, el objetivo del que prueba es encontrar
los fallos lo antes posible, en todo caso antes de pagarlo y antes de poner el
programa en producción.
1.4 Otros conceptos relacionados con las pruebas de software
Completitud: nos da una idea
del grado de fiabilidad de las pruebas y por consiguiente la fiabilidad
del software. No es posible llegar al 100% puesto que nunca llegaremos a
realizar todas las pruebas posibles al software, puesto que las pruebas
tienen un coste (P. ej.- Bug del Excel).
Depuración: ejecución
controlada del software que nos permite corregir un error (P. ej.- Usar el
debugger de nuestra máquina).
No es fácil establecer una
clasificación dentro de la variedad de entornos de desarrollo existentes. En
algún momento se describieron las siguientes clases de entornos, no
excluyentes:
Ejercicio1: Busca los distintos tipos de entornos de
desarrollo enumerados anteriormente, defínelos y pon 2 ejemplos de cada uno de
ellos.
Ejercicio2: Clasifica entornos de desarrollo en
comerciales y libres (los más utilizados), comentando las ventajas e
inconvenientes de cada uno de ellos
En las primeras etapas
de la informática, la preparación de programas se
realizaba mediante una cadena de
operaciones tales como las que se muestra en la figura para un lenguaje
procesado mediante compilador. Cada una de las herramientas debía invocarse
manualmente por separado. En estas condiciones no puede hablarse propiamente de
un entorno de desarrollo
Componentes Entorno Desarrollo
El editor es un editor de
texto simple
El compilador traduce cada
fichero de código fuente a código objeto
El montador (linker
/ builder / loader) combina varios ficheros objeto para
generar un fichero ejecutable
El depurador maneja
información en términos de lenguaje de máquina
Un entorno de programación
propiamente dicho combina herramientas como
éstas, mejoradas y mejor integradas. Los
componentes cuya evolución ha sido más aparente son los que realizan la
interacción con el usuario:
El
editor ya no es un simple editor de texto, sino que tiene una clara
orientación al lenguaje de programación usado (reconoce y maneja
determinados elementos sintácticos)
El
depurador no presenta información en términos del lenguaje de máquina,
sino del lenguaje fuente
El
editor está bien integrado con las demás herramientas (se posiciona
directamente en los puntos del código fuente en los que hay errores de
compilación, o que se están ejecutando con el depurador en un momento
dado.
Podemos concluir, enumerando los
componentes que incorpora cualquier
entorno de desarrollo:
-Un editor de texto
-Un compilador
-Un intérprete
-Un depurador
-Posibilidad de ofrecer un sistema de control de versiones
-Factibilidad para ayuda en la construcción de interfaces
gráficas de usuario
Antes de enumerar las funciones de un entorno de desarrollo,
tendremos que
definir en qué consiste un entorno de
desarrollo.
Un
entorno de desarrollo o IDE (Integrated Development Environment) es un programa
informático que tiene el objetivo de asistir al programador en la difícil tarea
de diseñar y codificar un software mediante la inclusión de múltiples
herramientas destinadas para dichas tareas.
Ahora
pasamos a enumerar las funciones que podemos utilizar en un entorno de
desarrollo:
Las tareas esenciales de la
fase de codificación:
oEdición (creación y
modificación del código fuente)
Los lenguajes de
programación se pueden clasificar según varios criterios. Hay que tener en
cuenta también, que en la práctica, la mayoría de lenguajes no pueden ser
puramente clasificados en una categoría, pues surgen incorporando ideas de
otros lenguajes y de otras filosofías de programación, pero no importa al
establecer las clasificaciones, pues el auténtico objetivo de las mismas es
mostrar los rangos, las posibilidades y tipos de lenguajes que hay.
1. Nivel de abstracción.
Según el nivel de abstracción, es decir, según el
grado de cercanía a la máquina:
·Lenguajes de bajo nivel: La programación se
realiza teniendo muy en cuenta las características del procesador. Ejemplo:
Lenguajes ensamblador.
·Lenguajes de nivel medio: Permiten un mayor grado
de abstracción pero al mismo tiempo mantienen algunas cualidades de los lenguajes
de bajo nivel. Ejemplo: C puede realizar operaciones lógicas y de
desplazamiento con bits, tratar todos los tipos de datos como lo que son en
realidad a bajo nivel (números), etc.
·Lenguajes de alto nivel: Más parecidos al
lenguaje humano. Manejan conceptos, tipos de datos, etc., de una manera cercana
al pensamiento humano ignorando (abstrayéndose) del funcionamiento de la
máquina. Ejemplos: Java, Ruby.
Hay quien sólo considera lenguajes de bajo nivel y
de alto nivel, (en ese caso, C es considerado de alto nivel).
2. Propósito.
Según el propósito, es decir, el tipo de problemas
a tratar con ellos:
·Lenguajes de propósito general: Aptos para todo
tipo de tareas: Ejemplo: C.
·Lenguajes de propósito específico: Hechos para un
objetivo muy concreto. Ejemplo: Csound (para crear ficheros de
audio).
·Lenguajes de programación de sistemas: Diseñados
para realizar sistemas operativos o drivers. Ejemplo: C.
·Lenguajes de script: Para realizar tareas varias
de control y auxiliares. Antiguamente eran los llamados lenguajes de procesamiento
por lotes (batch) o JCL (“Job Control
Languages”). Se subdividen en varias clases (de shell, de GUI, de programación
web, etc.). Ejemplos: bash (shell), mIRC script, JavaScript (programación web).
3. Evolución histórica.
Con el paso del tiempo, se va incrementando el nivel
de abstracción, pero en la práctica, los de una generación no terminan de
sustituir a los de la anterior:
·Lenguajes de primera generación (1GL): Código
máquina.
·Lenguajes de segunda generación (2GL): Lenguajes
ensamblador.
·Lenguajes de tercera generación (3GL): La mayoría
de los lenguajes modernos, diseñados para facilitar la programación a los
humanos. Ejemplos: C, Java.
·Lenguajes de cuarta generación (4GL): Diseñados
con un propósito concreto, o sea, para abordar un tipo concreto de problemas.
Ejemplos: NATURAL, Mathematica.
·Lenguajes de quinta generación(5GL): La intención es que el programador establezca el qué problema ha
de ser resuelto y las condiciones a reunir, y la máquina lo resuelve. Se usan
en inteligencia artificial. Ejemplo: Prolog.
4. Manera de ejecutarse.
Según la manera de ejecutarse:
·Lenguajes compilados:
·Lenguajes interpretados:
·También los hay mixtos,
5. Manera de abordar la tarea a realizar.
Según la manera de abordar la tarea a realizar,
pueden ser:
·Lenguajes imperativos: Indican cómo hay que hacer
la tarea, es decir, expresan los pasos a realizar. Ejemplo: C.
·Lenguajes declarativos: Indican qué hay que hacer.
Ejemplos: Lisp, Prolog. Otros ejemplos de lenguajes declarativos, pero que no
son lenguajes de programación, son HTML (para describir páginas web) o SQL (para consultar bases
de datos).
6. Paradigma de programación.
El paradigma de
programación es el estilo de programación empleado. Algunos lenguajes
soportan varios paradigmas, y otros sólo uno. Se puede decir que históricamente
han ido apareciendo para facilitar la tarea de programar según el tipo de
problema a abordar, o para facilitar el mantenimiento del software, o por otra
cuestión similar, por lo que todos corresponden a lenguajes de alto nivel (o
nivel medio), estando los lenguajes ensambladores “atados” a la arquitectura de
su procesador correspondiente. Los principales son:
·Lenguajes de programación procedural: Divide el
problema en partes más pequeñas, que serán realizadas por subprogramas
(subrutinas, funciones, procedimientos), que se llaman unas a otras para ser
ejecutadas. Ejemplos: C, Pascal.
·Lenguajes de programación orientada a objetos:
Crean un sistema de clases y objetos siguiendo el ejemplo del mundo real, en el
que unos objetos realizan acciones y se comunican con otros objetos. Ejemplos: C++, Java.
·Lenguajes de programación funcional: La tarea se
realiza evaluando funciones, (como en Matemáticas), de manera recursiva.
Ejemplo: Lisp.
·Lenguajes de programación lógica: La tarea a
realizar se expresa empleando lógica formal matemática. Expresa qué computar.
Ejemplo: Prolog.
Hay muchos paradigmas de programación:
Programación genérica, programación reflexiva, programación orientada a
procesos, etc.
7. Lugar de ejecución.
En sistemas distribuidos, según dónde se
ejecute:
·Lenguajes de servidor: Se ejecutan en el servidor.
Ejemplo: PHP es el más utilizado en
servidores web.
·Lenguajes de cliente: Se ejecutan en el cliente.
Ejemplo: JavaScript en navegadores web.