Test di gerarchie

Scritto da Michele Della Torre il 19 aprile 2008 – 15:52

Il Ruby Tower Defence sta procedendo molto lentamente per una cronica mancanza di tempo e per la tipica ansia da tema: alla superiori quando il professore di italiano dettava la traccia non si sapeva mai come iniziare, ma una volta scritte le prime frasi si procedeva in modo spedito.
Nonostante questo qualcosa di interessante è arrivato.

Una delle prime classi che ho definito è stata Monster che, tra le varie cose, espone il metodo draw(surface) per disegnarsi sulla superficie di destinazione, con il relativo test, e ora sto definendo la classe Tower ed anch’essa dovrà essere mostrata a schermo, quindi necessiterà di un metodo di disegno, come indicato dal seguente test:

class TowerTest < Test::Unit::TestCase
 
  def setup
    @tower_image = SurfaceMock.new([20, 20])
    @position = Position.new(100, 200)
   
    @tower = Tower.new(@position, @tower_image)
  end
 
  def test_draw
    destination_surface = SurfaceMock.new([500, 500])
   
    @tower.draw(destination_surface)
   
    assert_equal(destination_surface, @tower_image.target)
    assert_equal([90, 190], @tower_image.destination)
  end
 
end

Il fatto che i due test siano praticamente identici, e quindi esista una duplicazione, ci deve spingere a cercare un modo per rimuoverla anche perché appare evidente che tale duplicazione si troverà anche nelle due classi. Procediamo per passi.

Iniziamo a duplicare il codice tra Monster e Tower in modo da far passare entrambi i test:

class Tower
  def initialize(position, image)
    @image = image
    @position = position
  end
 
  def draw(destination_surface)
    @image.blit( destination_surface, top_left_image_corner() )
  end
 
  private
  def top_left_image_corner()
    new_top = @position.x - @image.height / 2
    new_left = @position.y - @image.width / 2
   
    return [new_top, new_left]
  end
 
end
class Monster
  initialize(initial_health, initial_position, image, speed)
    @image = image
    @position = position
    [...]
  end
 
  def draw(destination_surface)
    @image.blit( destination_surface, top_left_image_corner() )
  end
 
  private
  def top_left_image_corner()
    new_top = @position.x - @image.height / 2
    new_left = @position.y - @image.width / 2
   
    return [new_top, new_left]
  end
 
   [...]
end

In questo caso credo che sia ragionevole dire che sia Tower che Monster siano due Sprite e che il metodo draw sia definito nella superclasse, quindi applichiamo un Extract Superclass a Tower, portando tutto nella nuova classe Sprite.

class Sprite
   

  def initialize(position, image)
    @image = image
    @position = position
  end
 
  def draw(destination_surface)
    @image.blit( destination_surface, top_left_image_corner() )
  end
 
  private
  def top_left_image_corner()
    new_top = @position.x - @image.height / 2
    new_left = @position.y - @image.width / 2
   
    return [new_top, new_left]
  end
 
end
class Tower < Sprite
[...]

end

Testiamo tutto: barra verde. Facciamo lo stesso con Monster e ci accorgiamo che in Sprite bisogna definire l’attributo position, in particolare in lettura deve essere pubblico, mentre in scrittura deve essere disponibile solo alle sotto classi, quindi protected: questo perchè Monster si muove e ha bisogno di aggiornare la posizione (il codice relativo non è stato riportato qui)

class Sprite
 
  public
  attr_reader :position
 
  protected
  attr_writer :position

  [...]

end
class Monster < Sprite
  [...]
end

Lanciamo i test e tutti passano. In questo momento abbiamo tolto la duplicazione dalle classi sotto test, ma non da quelle di test.
Iniziamo a scrivere il test della classe Sprite:

class SpriteTest < Test::Unit::TestCase
 
  def setup
    @image = SurfaceMock.new([20, 20])
    @position = Position.new(100, 200)
   
    @sprite = Sprite.new(@position, @image)
  end
 
  def test_draw
    destination_surface = SurfaceMock.new([500, 500])
   
    @sprite.draw(destination_surface)
   
    assert_equal(destination_surface, @image.target)
    assert_equal([90, 190], @image.destination)
  end
 
end

rendiamo quindi il membro @sprite accessibile sia in scrittura che in lettura dalle sottoclassi e ricordiamoci il principio di sostituzione di Liskov che dice che le sottoclassi non devono invalidare il contratto delle superclassi o, per dirlo in un altro modo, tutti i test validi per la superclasse devono esserlo anche per la sottoclasse. Per definire questa relazione nel programma è sufficiente far estendere ai test delle sottoclassi il test della superclasse; quindi ora modifichiamo MonsterTest e TowerTest, ricordandoci di richiamare sempre il setup e di usare le variabili position e image della superclasse, di assegnare a sprite l’elemento da testare e di cancellare i test duplicati:

class TowerTest < SpriteTest
 
  def setup
    super()
    @tower = Tower.new(self.position, self.image)
    self.sprite = @tower
  end

end

Lanciamo i test e vediamo una barra verde: i test della superclasse vengono lanciati perchè ereditati nella sottoclasse.

Una nota: Sprite e SpriteTest in Java o C# sarebbe state classi astratte, ma in Ruby questo concetto non esiste, per cui sono classi semplici.

Posted under Generale | No Comments »

Leave a Comment