Come sarebbe un nuovo linguaggio se fosse progettato 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 contrasto con la lingua quando vuoi TDD completamente il tuo codice.

Ad esempio, in Java e C # vorrai deridere qualsiasi dipendenza delle tue classi e la maggior parte dei framework di derisione ti consiglieranno di deridere interfacce e non 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 consentono di deridere classi concrete in modo corretto fanno cose come alterare il compilatore o sovrascrivere caricatori di classi ecc., Il che è piuttosto brutto.

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


Che ne dici di una lingua che non ha bisogno del TDD? blog.8thlight.com/uncle-bob/2011/10/20/Simple-Hickey.html
Lavoro

2
Nessuna lingua ha bisogno di TDD. TDD è una pratica utile e uno dei punti di Hickey è che solo perché test non significa che potresti smettere di pensare .
Frank Shearar,

Lo sviluppo guidato dai test consiste nel mettere a posto le API interne ed esterne e farlo in anticipo. Quindi in Java si tratta di interfacce: le classi effettive sono sottoprodotti.

Risposte:


6

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

Test pulsante zero

L'idea era che le asserzioni fossero in linea con il codice stesso e che tutti i test fossero eseguiti sostanzialmente ad ogni sequenza di tasti. Quindi, non appena il test viene superato, il metodo diventa verde.


2
Haha, è fantastico! In realtà mi piace l'idea di mettere insieme i test con il codice. È abbastanza noioso (anche se ci sono ottime ragioni) in .NET avere assembly separati con spazi dei nomi paralleli per i test unitari. Inoltre semplifica il refactoring perché lo spostamento del codice sposta automaticamente i test: P
Geoff il

Ma vuoi lasciare i test lì dentro? Li lasceresti abilitati per il codice di produzione? Forse potrebbero essere # ifdef'd per C, altrimenti stiamo osservando hit di dimensione del codice / run-time.
Mawg dice di reintegrare Monica

È puramente un prototipo. Se dovesse diventare reale, allora dovremmo considerare cose come prestazioni e dimensioni, ma è troppo presto per preoccuparsene, e se mai arrivassimo a quel punto, non sarebbe difficile scegliere cosa lasciare fuori o, se lo si desidera, lasciare le asserzioni fuori dal codice compilato. Grazie per il tuo interesse.
Carl Manaster,

5

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_stuffchiamate vengano inviate due volte. Proprio come in una lingua tipicamente statica, puoi creare un MessageSender falso che conta quante volte è sendstato 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_stuffnon si richiama MessageSender#sendesattamente 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.


Bella risposta! Ragazzi, state iniziando a parlarmi di linguaggi dinamici :) Penso che la digitazione delle anatre sia la chiave qui, il trucco con .new potrebbe anche essere in un linguaggio tipicamente statico (anche se sarebbe molto meno elegante).
Geoff,

3

Quindi, come sarebbe un linguaggio se fosse progettato da zero per essere eccezionale con TDD?

'funziona bene con TDD' sicuramente non è sufficiente per descrivere una lingua, quindi potrebbe "apparire" come niente. Lisp, Prolog, C ++, Ruby, Python ... fai la tua scelta.

Inoltre, non è chiaro che supportare TDD sia qualcosa che è meglio gestito dal linguaggio stesso. Certo, potresti creare una lingua in cui ogni funzione o metodo ha un test associato e potresti creare supporto per scoprire ed eseguire quei test. Ma i framework di unit test già gestiscono bene la parte di individuazione ed esecuzione ed è difficile capire come aggiungere in modo pulito i requisiti di un test per ogni funzione. Anche i test hanno bisogno di test? Oppure ci sono due classi di funzioni: quelle normali che richiedono test e le funzioni di test che non ne hanno bisogno? Non sembra molto elegante.

Forse è meglio supportare TDD con strumenti e framework. Incorporalo nell'IDE. Creare un processo di sviluppo che lo incoraggi.

Inoltre, se stai progettando una lingua, è bene pensare a lungo termine. Ricorda che TDD è solo una metodologia e non il modo di lavorare preferito da tutti. Può essere difficile da immaginare, ma è possibile che stiano arrivando modi ancora migliori . Come designer di lingue, vuoi che le persone debbano abbandonare la tua lingua quando succede?

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


D'accordo, è una domanda molto difficile da pronunciare bene. Penso che ciò che intendo sia che gli attuali strumenti di test per linguaggi come Java / C # abbiano l'impressione che il linguaggio si stia intromettendo e che alcune funzionalità linguistiche extra / alternative renderebbero l'intera esperienza più elegante (cioè non avere interfacce per 90 % delle mie lezioni, solo quelle in cui ha senso da un punto di vista progettuale di livello superiore).
Geoff,

0

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

D'altra parte, linguaggi tipicamente statici come Java e C # o C ++ impongono tipi e ti costringono 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 gli schemi di progettazione e nel rispetto dei principi SOLID. Ad esempio, utilizzo frequentemente le interfacce in PHP perché rendono esplicito il design e impongono anche il design. D'altra parte in Ruby non hai modo di imporre un tipo, è un linguaggio tipizzato da papera. Tuttavia, devi immaginare l'interfaccia lì e devi astrarre il design nella tua mente per implementarlo correttamente.

Quindi, sebbene la tua domanda possa 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 hanno una grande infrastruttura di derisione, sia costruita nei loro framework di test unitari che consegnata separatamente (vedi Mockery per PHP). In alcuni casi, questi framework ti consentono persino di fare ciò che stai suggerendo, cose come deridere chiamate statiche o inizializzazioni di oggetti senza iniettare esplicitamente una dipendenza.


1
Concordo sul fatto che le interfacce sono fantastiche e un elemento chiave di progettazione. Tuttavia, nel mio codice trovo che il 90% delle classi abbia un'interfaccia e che ci siano solo due implementazioni di quell'interfaccia, la classe e le beffe di quella classe. Sebbene questo sia tecnicamente esattamente il punto di interfaccia, non posso fare a meno di pensare che sia inelegante.
Geoff,

Non ho molta familiarità con il deridere in Java e C #, ma per quanto ne so, un oggetto deriso imita l'oggetto reale. Faccio spesso l'iniezione di dipendenza utilizzando un parametro del tipo di oggetto e inviando invece una simulazione al metodo / classe. Qualcosa di simile alla funzione someName (AnotherClass $ object = null) {$ this-> anotherObject = $ object? : nuovo AnotherClass; } Questo è un trucco usato frequentemente per iniettare dipendenza senza derivare da un'interfaccia.
Patkos Csaba,

1
Questo è sicuramente dove i linguaggi dinamici hanno il vantaggio rispetto ai linguaggi di tipo Java / C # rispetto alla mia domanda. Una tipica derisione di una classe concreta creerà effettivamente una sottoclasse della classe, il che significa che verrà chiamato il costruttore della classe concreta, che è qualcosa che sicuramente si desidera evitare (ci sono eccezioni, ma hanno i loro problemi). Una derisione dinamica sfrutta solo la tipizzazione delle anatre, quindi non esiste alcuna relazione tra la classe concreta e una derisione. Ero solito scrivere codice in Python, ma era prima dei miei giorni TDD, forse è tempo di dare un'altra occhiata!
Geoff,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.