Supongamos que estamos escribiendo
un programa C#
que conecta con un servidor remoto
y que tenemos un objeto que representa dicha conexión:
RemoteConnection conn = new RemoteConnection("my_server"); String stuff = conn.readStuff(); conn.dispose(); // close the connection to avoid a leak
Este código libera los recursos asociados a la conexión después de usarla.
Sin embargo, no considera la presencia de excepciones.
Si readStuff( )
lanza una excepción, conn.dispose()
no
se ejecutará.
C#
proporciona una palabra clave using
que simplifica el manejo de la liberación de recursos:
RemoteConnection conn = new RemoteConnection("some_remote_server"); using (conn) { conn.readSomeData(); doSomeMoreStuff(); }La palabra
using
espera que el objeto conn
tenga un método con nombre dispose()
.
Este método es llamado automáticamente después del código entre llaves,
tanto si se genera una excepción como si no.
Supongamos que se nos pide como ejercicio que extendamos Ruby con una palabra clave using
que funcione de manera similar al using
de C#
.
Para comprobar que lo hagamos correctamente se nos da el siguiente programa de test (véase Unit Testing en la wikipedia y la documentación de la librería Test::Unit Test::Unit::Assertions ):
~/rubytesting$ cat -n using_test.rb 1 require 'test/unit' 2 3 class TestUsing < Test::Unit::TestCase 4 class Resource 5 def dispose 6 @disposed = true 7 end 8 9 def disposed? 10 @disposed 11 end 12 end 13 14 def test_disposes_of_resources 15 r = Resource.new 16 using(r) {} 17 assert r.disposed? 18 end 19 20 def test_disposes_of_resources_in_case_of_exception 21 r = Resource.new 22 assert_raises(Exception) { 23 using(r) { 24 raise Exception 25 } 26 } 27 assert r.disposed? 28 end 29 end
Las clases que representan tests deben ser subclases de
Test::Unit::TestCase
.
Los métodos que contienen assertions
deben empezar por la palabra test
.
Test::Unit
utiliza introspección para decidir para encontrar que métodos debe ejecutar.
Por supuesto, si se ejecutan ahora, nuestras pruebas fallan:
~/rubytesting$ ruby using_test.rb Loaded suite using_test Started EF Finished in 0.005613 seconds. 1) Error: test_disposes_of_resources(TestUsing): NoMethodError: undefined method `using' for #<TestUsing:0x100348608> using_test.rb:16:in `test_disposes_of_resources' 2) Failure: test_disposes_of_resources_in_case_of_exception(TestUsing) [using_test.rb:22]: <Exception> exception expected but was Class: <NoMethodError> Message: <"undefined method `using' for #<TestUsing:0x1003485e0>"> ---Backtrace--- using_test.rb:23:in `test_disposes_of_resources_in_case_of_exception' using_test.rb:22:in `test_disposes_of_resources_in_case_of_exception' --------------- 2 tests, 1 assertions, 1 failures, 1 errorsIdea: No podemos definir
using
como una palabra clave, por supuesto, pero podemos
producir un efecto parecido definiendo using
como un método de Kernel
:
~/rubytesting$ cat -n using1.rb 1 module Kernel 2 def using(resource) # resource: el recurso a liberar con dispose 3 begin # llamamos al bloque 4 yield 5 resource.dispose # liberamos 6 end 7 end 8 end
Sin embargo, esta versión no está a prueba de excepciones. Cuando ejecutamos las pruebas obtenemos:
~/rubytesting$ ruby -rusing1 using_test.rb Loaded suite using_test Started .F Finished in 0.005043 seconds. 1) Failure: test_disposes_of_resources_in_case_of_exception(TestUsing) [using_test.rb:27]: <nil> is not true. 2 tests, 3 assertions, 1 failures, 0 errorsEl mensaje nos indica la causa del fallo:
test_disposes_of_resources_in_case_of_exception(TestUsing) [using_test.rb:27]: <nil> is not true.Es posible también ejecutar un test específico:
~/rubytesting$ ruby -rusing1 -w using_test.rb --name test_disposes_of_resources Loaded suite using_test Started . Finished in 0.000196 seconds. 1 tests, 1 assertions, 0 failures, 0 errors ~/rubytesting$(Para saber mas sobre Unit Testing en Ruby vea Ruby Programming/Unit testing en Wikibooks).
Volviendo a nuestro fallo, recordemos que
la cláusula rescue
es usada cuando queremos que un código se ejecute si se produce una excepción:
begin file = open("/tmp/some_file", "w") # ... write to the file ... file.close rescue file.close fail # raise an exception endPero en este caso es mejor usar la cláusula
ensure
.
El código bajo un ensure
se ejecuta, tanto si se ha producido excepción como si no:
begin file = open("/tmp/some_file", "w") # ... write to the file ... rescue # ... handle the exceptions ... ensure file.close # ... and this always happens. endEs posible utilizar
ensure
sin la cláusula rescue
, y viceversa,
pero si aparecen ambas en el mismo bloque begin...end
entonces rescue
debe preceder a ensure
.
Reescribimos nuestro using
usando ensure
:
~/rubytesting$ cat -n using.rb 1 module Kernel 2 def using(resource) 3 begin 4 yield 5 ensure 6 resource.dispose 7 end 8 end 9 endAhora los dos tests tienen éxito:
~/rubytesting$ ruby -rusing using_test.rb Loaded suite using_test Started .. Finished in 0.000346 seconds. 2 tests, 3 assertions, 0 failures, 0 errors
Lo normal durante el desarrollo es guardar las pruebas en una carpeta con nombtre test
o t
:
project | `-lib/ | | | `-using.rb | | | `-otros ficheros ... `-test/ | `-using_test | `-otros tests ..El problema con esta estructura es que hay que decirle a Ruby donde debe encontrar la librería cuando se ejecutan las pruebas.
$LOAD_PATH
$:.unshift File.join(File.dirname(__FILE__), "..", "lib") require ...La variable
$:
es una array conteniendo el PATH de búsqueda de librerías.
Lo habitual, cuando un proyecto crece
es que los tests se acumulen. Lo normal es clasificarlos
por afinidades en diferentes ficheros.
Los podemos agrupar en suites. Para ello creamos
un fichero con una lista de require
s
require 'test/unit' require 'tc_smoke' require 'tc_client_requirements' require 'tc_extreme'
Ahora, simplemente ejecutando este fichero ejecutaremos todos los casos en la suite.
Casiano Rodriguez León 2015-01-07