Il serait typé dynamiquement plutôt que statiquement. Le typage canard ferait alors le même travail que les interfaces dans les langages typés statiquement. De plus, ses classes seraient modifiables au moment de l'exécution afin qu'un framework de test puisse facilement stub ou simuler des méthodes sur des classes existantes. Le rubis est une de ces langues; rspec est son premier framework de test pour TDD.
Comment la dactylographie dynamique facilite les tests
Avec la saisie dynamique, vous pouvez créer des objets fantômes en créant simplement une classe qui a la même interface (signatures de méthode) que l'objet collaborateur dont vous avez besoin de se moquer. Par exemple, supposons que vous ayez eu une classe qui a envoyé des messages:
class MessageSender
def send
# Do something with a side effect
end
end
Disons que nous avons un MessageSenderUser qui utilise une instance de MessageSender:
class MessageSenderUser
def initialize(message_sender)
@message_sender = message_sender
end
def do_stuff
...
@message_sender.send
...
@message_sender.send
...
end
end
Notez l'utilisation ici de l' injection de dépendances , un aliment de base des tests unitaires. Nous y reviendrons.
Vous souhaitez tester que les MessageSenderUser#do_stuff
appels sont envoyés deux fois. Tout comme vous le feriez dans un langage tapé statiquement, vous pouvez créer un faux MessageSender qui compte le nombre de fois send
appelé. Mais contrairement à un langage typé statiquement, vous n'avez besoin d'aucune classe d'interface. Allez-y et créez-le:
class MockMessageSender
attr_accessor :send_count
def initialize
@send_count = 0
end
def send
@send_count += 1
end
end
Et utilisez-le dans votre test:
mock_sender = MockMessageSender.new
MessageSenderUser.new(mock_sender).do_stuff
assert_equal(mock_sender.send_count, 2)
En soi, le "typage canard" d'une langue typée dynamiquement n'ajoute pas grand-chose aux tests par rapport à une langue typée statiquement. Mais que se passe-t-il si les classes ne sont pas fermées, mais peuvent être modifiées au moment de l'exécution? Cela change la donne. Voyons comment.
Et si vous n'aviez pas à utiliser l'injection de dépendance pour rendre une classe testable?
Supposons que MessageSenderUser n'utilise que MessageSender pour envoyer des messages et que vous n'avez pas besoin d'autoriser la substitution de MessageSender par une autre classe. Au sein d'un même programme, c'est souvent le cas. Réécrivons MessageSenderUser pour qu'il crée et utilise simplement un MessageSender, sans injection de dépendance.
class MessageSenderUser
def initialize
@message_sender = MessageSender.new
end
def do_stuff
...
@message_sender.send
...
@message_sender.send
...
end
end
MessageSenderUser est désormais plus simple à utiliser: personne qui le crée n'a besoin de créer un MessageSender pour l'utiliser. Cela ne ressemble pas à une grande amélioration dans cet exemple simple, mais imaginez maintenant que MessageSenderUser est créé à plusieurs reprises, ou qu'il a trois dépendances. Maintenant, le système a beaucoup d'instances de passage juste pour rendre les tests unitaires heureux, pas parce qu'il améliore nécessairement la conception du tout.
Les classes ouvertes vous permettent de tester sans injection de dépendance
Un framework de test dans un langage avec typage dynamique et classes ouvertes peut rendre TDD assez agréable. Voici un extrait de code d'un test rspec pour 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
C'est tout le test. Si MessageSenderUser#do_stuff
n'appelle pas MessageSender#send
exactement deux fois, ce test échoue. La vraie classe MessageSender n'est jamais invoquée: nous avons dit au test que chaque fois que quelqu'un essaie de créer un MessageSender, il devrait obtenir notre faux MessageSender à la place. Aucune injection de dépendance nécessaire.
C'est agréable de faire autant dans un test aussi simple. Il est toujours plus agréable de ne pas avoir à utiliser l'injection de dépendances à moins que cela ne soit réellement logique pour votre conception.
Mais qu'est-ce que cela a à voir avec les classes ouvertes? Notez l'appel à MessageSender.should_receive
. Nous n'avons pas défini #should_receive lorsque nous avons écrit MessageSender, alors qui l'a fait? La réponse est que le framework de test, en apportant quelques modifications soigneuses aux classes système, est capable de le faire apparaître car à travers #should_receive est défini sur chaque objet. Si vous pensez que la modification de classes système comme celle-ci nécessite une certaine prudence, vous avez raison. Mais c'est la chose parfaite pour ce que fait la bibliothèque de tests ici, et les classes ouvertes le permettent.