Come apparirebbe una nuova lingua se fosse stata progettata da zero per essere facile da TDD?

9

Con alcuni dei linguaggi più comuni (Java, C #, Java, ecc.) a volte sembra che tu stia lavorando in disaccordo con la lingua quando vuoi inserire completamente il tuo codice TDD.

Ad esempio, in Java e in C # vorrai prendere in giro qualsiasi dipendenza delle tue classi e la maggior parte dei framework di derisione ti consiglierà di prendere in giro le interfacce e non le classi. Questo spesso significa che hai molte interfacce con una singola implementazione (questo effetto è ancora più evidente perché TDD ti costringerà a scrivere un numero maggiore di classi più piccole). Le soluzioni che ti consentono di simulare correttamente le classi concrete fanno cose come alterare il compilatore o sovrascrivere i programmi di caricamento della classe ecc., Che è piuttosto sgradevole.

Quindi come apparirebbe un linguaggio se fosse stato progettato da zero per essere eccezionale con TDD? Forse un modo in qualche modo a livello di linguaggio per descrivere le dipendenze (piuttosto che passare interfacce a un costruttore) e essere in grado di separare l'interfaccia di una classe senza farlo in modo esplicito?

    
posta Geoff 25.08.2012 - 17:18
fonte

4 risposte

6

Molti anni fa ho messo insieme un prototipo che rispondeva a una domanda simile; ecco uno screenshot:

L'idea era che le asserzioni fossero in linea con il codice stesso e che tutti i test funzionassero fondamentalmente a ogni battitura. Quindi, non appena esegui il test, vedi il metodo diventare verde.

    
risposta data 25.08.2012 - 18:08
fonte
5

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.

    
risposta data 25.08.2012 - 18:23
fonte
3

So what would a language look like if it was designed from scratch to be great to TDD with?

'funziona bene con TDD' sicuramente non è sufficiente per descrivere una lingua, quindi potrebbe "sembrare" come qualcosa. Lisp, Prolog, C ++, Ruby, Python ... fate la vostra scelta.

Inoltre, non è chiaro che il supporto di TDD sia qualcosa che viene gestito meglio dal linguaggio stesso. Certo, potresti creare una lingua in cui ogni funzione o metodo ha un test associato e potresti creare supporto per la scoperta e l'esecuzione di tali test. Ma i framework di testing unitario gestiscono già la parte di scoperta ed esecuzione, ed è difficile vedere come aggiungere in modo pulito il requisito di un test per ogni funzione. I test richiedono anche test? O ci sono due classi di funzioni - quelle normali che richiedono test e funzioni di test che non ne hanno bisogno? Non sembra molto elegante.

Forse è meglio supportare TDD con strumenti e framework. Costruiscilo nell'IDE. Crea un processo di sviluppo che lo incoraggia.

Inoltre, se stai progettando una lingua, è bene pensare a lungo termine. Ricorda che TDD è solo una metodologia, e non il modo preferito di lavorare di tutti. Potrebbe essere difficile da immaginare, ma è possibile che anche i modi migliori vengano. Come designer del linguaggio, vuoi che le persone abbandonino la tua lingua quando succede?

Tutto quello che puoi veramente dire per rispondere alla domanda è che un linguaggio del genere sarebbe favorevole ai test. So che non aiuta molto, ma penso che il problema sia con la domanda.

    
risposta data 25.08.2012 - 18:07
fonte
0

Bene, i linguaggi digitati dinamicamente non richiedono interfacce esplicite. Vedi Ruby o PHP, ecc.

D'altra parte, linguaggi tipizzati staticamente come Java e C # o C ++ impongono tipi e costringe a scrivere quelle interfacce.

Quello che non capisco è qual è il tuo problema con loro. Le interfacce sono un elemento chiave del design e sono utilizzate in tutti i modelli di progettazione e nel rispetto dei principi SOLID. Io, per esempio, utilizzo spesso interfacce in PHP perché rendono il design esplicito e impongono anche il design. D'altra parte in Ruby non hai modo di imporre un tipo, è un linguaggio dattiloscritto. Ma comunque, devi immaginare l'interfaccia lì e devi astrarre il design nella tua mente per implementarlo correttamente.

Quindi, anche se la tua domanda può sembrare interessante, implica che hai problemi con la comprensione o l'applicazione delle tecniche di iniezione di dipendenza.

E per rispondere direttamente alla tua domanda, Ruby e PHP dispongono di una grande infrastruttura di simulazione, costruita nei loro framework di test unitari e consegnata separatamente (vedi Mockery per PHP). In alcuni casi, questi framework ti consentono persino di fare ciò che stai suggerendo, cose come il prendere in giro chiamate statiche o inizializzazioni di oggetti senza iniettare esplicitamente una dipendenza.

    
risposta data 25.08.2012 - 17:42
fonte

Leggi altre domande sui tag