Sarebbe dinamicamente anziché tipizzato staticamente. Duck typing farebbe quindi lo stesso lavoro che le interfacce fanno nelle lingue tipizzate staticamente. Inoltre, le sue classi sarebbero modificabili in fase di runtime in modo che un framework di test potesse facilmente stubare o simulare i metodi su classi esistenti. Il rubino è una di queste lingue; rspec è il principale framework di test per TDD.
In che modo la tipizzazione dinamica aiuta a testare
Con la digitazione dinamica, puoi creare oggetti mock semplicemente creando una classe che abbia la stessa interfaccia (firme del metodo) l'oggetto collaboratore di cui hai bisogno per prendere in giro. Ad esempio, supponi 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 usa 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
Nota l'uso qui di iniezione di dipendenza , una graffetta di test unitari. Torneremo a quello.
Desideri verificare che MessageSenderUser#do_stuff
invii due volte. Proprio come faresti in un linguaggio tipizzato staticamente, puoi creare un finto MessageSender che conta quante volte è stato chiamato send
. Ma a differenza di un linguaggio tipizzato staticamente, non hai bisogno di alcuna classe di interfaccia. Devi solo 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 "digitazione anatra" di un linguaggio digitato in modo dinamico non aggiunge molto ai test rispetto a un linguaggio tipizzato in modo statico. Ma cosa succede se le classi non sono chiuse, ma possono essere modificate in fase di runtime? Questo è un punto di svolta. Vediamo come.
Che cosa succede se non hai dovuto usare l'iniezione di dipendenza per rendere una classe testabile?
Supponi che MessageSenderUser utilizzi sempre MessageSender solo per inviare messaggi e non hai bisogno di consentire la sostituzione di MessageSender con qualche altra classe. All'interno di un singolo programma questo è spesso il caso. Riscriviamo MessageSenderUser in modo che crei e utilizzi semplicemente MessageSender, senza l'aggiunta di dipendenze.
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 lo crea per creare un MessageSender da utilizzare. Non sembra un grande miglioramento in questo semplice esempio, ma ora immagina che MessageSenderUser venga creato in più di una posizione o che abbia tre dipendenze. Ora il sistema ha un sacco di istanze passanti in giro solo per rendere felici i test unitari, non perché necessariamente migliora il design.
Le classi aperte ti consentono di eseguire il test senza l'iniezione delle dipendenze
Un framework di test in una lingua con digitazione dinamica e classi aperte può rendere il TDD piuttosto 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 richiama MessageSender#send
esattamente due volte, questo test ha esito negativo. La vera classe MessageSender non viene mai invocata: abbiamo detto al test che ogni volta che qualcuno tenta di creare un MessageSender, dovrebbe ottenere il nostro finto MessageSender. Nessuna iniezione di dipendenza necessaria.
È bello fare così tanto in un test così semplice. È sempre più bello non dover usare l'iniezione di dipendenza a meno che non abbia effettivamente 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 è stato? La risposta è che il framework di test, apportando alcune attente modifiche alle classi di sistema, è in grado di farlo apparire come se #should_receive sia definito su ogni oggetto. Se pensi che modificare le classi di sistema in questo modo richiede 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.