Creando una Palabra Clave

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 errors
El 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.

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