Sarebbe dinamicamente piuttosto che tipizzato staticamente. La tipizzazione anatra farebbe quindi lo stesso lavoro delle interfacce in linguaggi tipicamente statici. Inoltre, le sue classi sarebbero modificabili in fase di esecuzione in modo che un framework di test potesse facilmente stub o deridere metodi su classi esistenti. Ruby è una di queste lingue; rspec è il principale framework di test per TDD.
In che modo la tipizzazione dinamica aiuta i test
Con la digitazione dinamica, è possibile creare oggetti simulati semplicemente creando una classe che ha la stessa interfaccia (firme dei metodi) dell'oggetto collaboratore che è necessario deridere. Ad esempio, supponiamo di avere una classe che ha inviato messaggi:
class MessageSender
def send
# Do something with a side effect
end
end
Diciamo che abbiamo un MessageSenderUser che utilizza un'istanza di MessageSender:
class MessageSenderUser
def initialize(message_sender)
@message_sender = message_sender
end
def do_stuff
...
@message_sender.send
...
@message_sender.send
...
end
end
Si noti l'uso qui dell'iniezione di dipendenza , una graffetta del test unitario. Torneremo su quello.
Si desidera verificare che le MessageSenderUser#do_stuff
chiamate vengano inviate due volte. Proprio come in una lingua tipicamente statica, puoi creare un MessageSender falso che conta quante volte è send
stato chiamato. A differenza di un linguaggio tipicamente statico, non è necessaria alcuna classe di interfaccia. Basta andare avanti e crearlo:
class MockMessageSender
attr_accessor :send_count
def initialize
@send_count = 0
end
def send
@send_count += 1
end
end
E usalo nel tuo test:
mock_sender = MockMessageSender.new
MessageSenderUser.new(mock_sender).do_stuff
assert_equal(mock_sender.send_count, 2)
Di per sé, la "tipizzazione anatra" di una lingua tipizzata in modo dinamico non aggiunge molto ai test rispetto a una lingua tipizzata staticamente. Ma cosa succede se le classi non sono chiuse, ma possono essere modificate in fase di esecuzione? Questo è un punto di svolta. Vediamo come.
E se non fosse necessario utilizzare l'iniezione di dipendenza per rendere testabile una classe?
Supponiamo che MessageSenderUser utilizzerà sempre MessageSender per inviare messaggi e che non sia necessario consentire la sostituzione di MessageSender con un'altra classe. All'interno di un singolo programma questo è spesso il caso. Riscriviamo MessageSenderUser in modo che crei e utilizzi semplicemente un MessageSender, senza iniezione di dipendenza.
class MessageSenderUser
def initialize
@message_sender = MessageSender.new
end
def do_stuff
...
@message_sender.send
...
@message_sender.send
...
end
end
MessageSenderUser ora è più semplice da utilizzare: nessuno crearlo deve creare un MessageSender per poterlo utilizzare. Non sembra un grande miglioramento in questo semplice esempio, ma ora immagina che MessageSenderUser sia stato creato in più di una volta o che abbia tre dipendenze. Ora il sistema ha molte istanze di passaggio solo per rendere felici i test unitari, non perché migliora necessariamente il design.
Le classi aperte ti consentono di testare senza iniezione di dipendenza
Un framework di test in una lingua con tipizzazione dinamica e classi aperte può rendere TDD abbastanza piacevole. Ecco uno snippet di codice da un test rspec per MessageSenderUser:
mock_message_sender = mock MessageSender
MessageSender.should_receive(:new).and_return(mock_message_sender)
mock_message_sender.should_receive(:send).twice.with(no_arguments)
MessageSenderUser.new.do_stuff
Questo è l'intero test. Se MessageSenderUser#do_stuff
non si richiama MessageSender#send
esattamente due volte, questo test fallisce. La vera classe MessageSender non viene mai invocata: abbiamo detto al test che ogni volta che qualcuno cerca di creare un MessageSender, dovrebbe invece ottenere il nostro finto MessageSender. Nessuna iniezione di dipendenza necessaria.
È bello fare così tanto in un test così semplice. È sempre meglio non usare l'iniezione di dipendenza a meno che non abbia davvero senso per il tuo design.
Ma cosa c'entra questo con le classi aperte? Nota la chiamata a MessageSender.should_receive
. Non abbiamo definito #should_receive quando abbiamo scritto MessageSender, quindi chi l'ha fatto? La risposta è che il framework di test, apportando alcune accurate modifiche alle classi di sistema, è in grado di farlo apparire come attraverso #should_receive è definito su ogni oggetto. Se ritieni che la modifica di classi di sistema del genere richieda una certa cautela, hai ragione. Ma è la cosa perfetta per ciò che la libreria di test sta facendo qui, e le classi aperte lo rendono possibile.