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 errors
Idea: 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
end
Pero 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.
end
Es 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 end
Ahora 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 requires
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