Test: perché expect_any_instance_of è considerato un odore di design?

4

In questa domanda, userò un esempio di rubino, ma penso che sia una domanda generale.

Secondo il più popolare framework di test di Ruby (RSpec), il mocking di qualsiasi istanza di una classe ( allow_any_instance_of ) è un odore di progettazione.

In realtà, non sono d'accordo con questa affermazione.

Quindi, vorrei sapere come sarebbe il "modo migliore / corretto" (o qualcosa del genere) per implementare / testare una classe come questa di seguito, e testare se il metodo format_phone sta formattando correttamente i numeri di telefono .

class SmsSender
  def initialize(message)
    @message = message
    @client = Twilio::REST::Client.new(TWILIO_CREDENTIALS)
  end    

  def send_to(phone)
    return false unless validate_phone(phone)
    @client.send({
      from: '123',
      to: format_phone(phone),
      content: @message
    })
  end
end

Ecco come sarebbe il mio test:

expect_any_instance_of(Twilio::REST::Client).to receive(:send).with({ from: '123', to: '+10002225555', content: 'hi' })

SmsSender.new('hi').send_to('0002225555')
    
posta Rodrigo 06.10.2016 - 22:19
fonte

1 risposta

1

Usando questo meccanismo di simulazione, non si limita a influenzare @client . Hai effetto su tutti gli oggetti corrispondenti all'interno di quel test. Qui, questo non è un grosso problema dato che solo uno di questi oggetti esiste, ma pensa a cosa succederebbe se dovessi influenzare un tipo più fondamentale come stringhe o numeri che sono usati in tutto il tuo programma.

La dipendenza da questo meccanismo di derisione è un strong indicatore del fatto che il tuo progetto non è testabile in sé stesso, in particolare la dipendenza fissa da Twilio::REST::Client è problematica. Usando un meccanismo di iniezione delle dipendenze come l'iniezione del costruttore, possiamo scrivere lo stesso test ma con molta meno magia.

Ecco lo pseudocodice per illustrare il concetto:

class SmsSender
  def initialise(message, sender)
    @message = message
    @sender = sender or make_default_sender()
  end

  def send_to(phone)
    return false unless validate(phone)
    @sender({
      from: '123',
      to: format_phone(phone),
      message: @message,
    })
  end
end

def make_default_sender():
  client = Twilio::REST::Client.new(SECRET)
  return do |message|
    client.send(message)
  end
end

Nel test, ora possiamo facilmente verificare che stiamo inviando il messaggio corretto:

expected_message = ...

checker_was_called = false

def checking_sender(message)
  checker_was_called = true
  assert message == expected_message
  # you could still send the message here if you want to.
end

SmsSender.new('...', checking_sender).send_to('...')

assert checker_was_called

Naturalmente questo è anche molto più codice del tuo attuale test, e l'integrazione delle dipendenze introduce sempre qualche fragilità nel sistema - hai davvero bisogno di test di integrazione per assicurarti che tutte le dipendenze siano cablate correttamente per la produzione / distribuzione. Può quindi avere molto senso mantenere il tuo approccio attuale, purché tu sia a conoscenza dei compromessi.

    
risposta data 07.10.2016 - 09:14
fonte

Leggi altre domande sui tag