Le Module Time
Vous pouvez trouver tout le code de ce chapitre ici
Le propriétaire du produit souhaite que nous élargissions les fonctionnalités de notre application en ligne de commande en aidant un groupe de personnes à jouer au Poker Texas-Holdem.
Juste assez d'informations sur le poker
Vous n'aurez pas besoin de connaître grand-chose sur le poker, seulement qu'à certains intervalles de temps, tous les joueurs doivent être informés d'une valeur croissante de "blind" (mise obligatoire).
Notre application aidera à suivre quand la blind doit augmenter, et de combien elle doit être.
Au démarrage, elle demande combien de joueurs participent. Cela détermine le temps qui s'écoule avant que la mise "blind" n'augmente.
Il y a un temps de base de 5 minutes.
Pour chaque joueur, 1 minute est ajoutée.
Par exemple, 6 joueurs équivalent à 11 minutes pour la blind.
Après l'expiration du temps de la blind, le jeu doit alerter les joueurs du nouveau montant de la mise blind.
La blind commence à 100 jetons, puis 200, 400, 600, 1000, 2000 et continue à doubler jusqu'à la fin de la partie (notre fonctionnalité précédente de "Ruth wins" doit toujours terminer la partie)
Rappel du code
Dans le chapitre précédent, nous avons commencé à créer une application en ligne de commande qui accepte déjà une commande de type {name} wins. Voici à quoi ressemble le code actuel de CLI, mais assurez-vous de vous familiariser avec le reste du code avant de commencer.
type CLI struct {
playerStore PlayerStore
in *bufio.Scanner
}
func NewCLI(store PlayerStore, in io.Reader) *CLI {
return &CLI{
playerStore: store,
in: bufio.NewScanner(in),
}
}
func (cli *CLI) PlayPoker() {
userInput := cli.readLine()
cli.playerStore.RecordWin(extractWinner(userInput))
}
func extractWinner(userInput string) string {
return strings.Replace(userInput, " wins", "", 1)
}
func (cli *CLI) readLine() string {
cli.in.Scan()
return cli.in.Text()
}time.AfterFunc
time.AfterFuncNous voulons pouvoir programmer notre application pour qu'elle affiche les valeurs des mises blind à certaines durées dépendant du nombre de joueurs.
Pour limiter la portée de ce que nous devons faire, nous allons oublier pour l'instant la partie concernant le nombre de joueurs et supposer simplement qu'il y a 5 joueurs, donc nous testerons que toutes les 10 minutes, la nouvelle valeur de la mise blind est imprimée.
Comme d'habitude, la bibliothèque standard nous couvre avec func AfterFunc(d Duration, f func()) *Timer
AfterFuncattend que la durée s'écoule puis appelle f dans sa propre goroutine. Il renvoie unTimerqui peut être utilisé pour annuler l'appel en utilisant sa méthode Stop.
Une Duration représente le temps écoulé entre deux instants sous forme d'un comptage de nanosecondes en int64.
La bibliothèque time possède un certain nombre de constantes pour vous permettre de multiplier ces nanosecondes afin qu'elles soient un peu plus lisibles pour le type de scénarios que nous allons réaliser.
Lorsque nous appellerons PlayPoker, nous programmerons toutes nos alertes blind.
Tester cela peut être un peu délicat. Nous voudrons vérifier que chaque période est programmée avec le bon montant de blind, mais si vous regardez la signature de time.AfterFunc, son deuxième argument est la fonction qu'il exécutera. Vous ne pouvez pas comparer des fonctions en Go, nous ne pourrions donc pas tester quelle fonction a été envoyée. Nous devrons donc créer une sorte d'enveloppe autour de time.AfterFunc qui prendra le temps d'exécution et le montant à imprimer pour que nous puissions les espionner.
Écrivez d'abord le test
Ajoutez un nouveau test à notre suite
Vous remarquerez que nous avons créé un SpyBlindAlerter que nous essayons d'injecter dans notre CLI puis nous vérifions qu'après avoir appelé PlayPoker, une alerte est programmée.
(Rappelez-vous que nous visons d'abord le scénario le plus simple, puis nous itérerons.)
Voici la définition de SpyBlindAlerter
Essayez d'exécuter le test
Écrivez le minimum de code pour que le test s'exécute et vérifiez la sortie du test qui échoue
Nous avons ajouté un nouvel argument et le compilateur se plaint. Strictement parlant, le minimum de code est de faire en sorte que NewCLI accepte un *SpyBlindAlerter, mais trompons-nous un peu et définissons simplement la dépendance comme une interface.
Puis ajoutez-le au constructeur
Vos autres tests échoueront maintenant car ils n'ont pas de BlindAlerter passé à NewCLI.
L'espionnage sur BlindAlerter n'est pas pertinent pour les autres tests, donc dans le fichier de test, ajoutez
Utilisez-le ensuite dans les autres tests pour résoudre les problèmes de compilation. En l'étiquetant comme "dummy", il est clair pour le lecteur du test qu'il n'est pas important.
Les tests devraient maintenant se compiler et notre nouveau test échoue.
Écrivez suffisamment de code pour le faire passer
Nous devrons ajouter le BlindAlerter comme champ sur notre CLI pour pouvoir y faire référence dans notre méthode PlayPoker.
Pour faire passer le test, nous pouvons appeler notre BlindAlerter avec ce que nous voulons
Ensuite, nous voudrons vérifier qu'il programme toutes les alertes que nous espérons pour 5 joueurs
Écrivez d'abord le test
Les tests basés sur des tableaux fonctionnent bien ici et illustrent clairement ce que sont nos exigences. Nous parcourons le tableau et vérifions le SpyBlindAlerter pour voir si l'alerte a été programmée avec les bonnes valeurs.
Essayez d'exécuter le test
Vous devriez avoir beaucoup d'échecs qui ressemblent à ceci
Écrivez suffisamment de code pour le faire passer
Ce n'est pas beaucoup plus compliqué que ce que nous avions déjà. Nous itérons maintenant sur un tableau de blinds et appelons le planificateur sur un blindTime croissant
Refactorisation
Nous pouvons encapsuler nos alertes programmées dans une méthode juste pour rendre la lecture de PlayPoker un peu plus claire.
Enfin, nos tests semblent un peu encombrants. Nous avons deux structs anonymes représentant la même chose, une ScheduledAlert. Refactorisons cela en un nouveau type puis créons des helpers pour les comparer.
Nous avons ajouté une méthode String() à notre type pour qu'il s'affiche joliment si le test échoue
Mettez à jour notre test pour utiliser notre nouveau type
Implémentez vous-même assertScheduledAlert.
Nous avons passé pas mal de temps ici à écrire des tests et avons été un peu vilains en ne nous intégrant pas à notre application. Résolvons ce problème avant d'ajouter d'autres exigences.
Essayez d'exécuter l'application et elle ne se compilera pas, se plaignant de ne pas avoir assez d'arguments pour NewCLI.
Créons une implémentation de BlindAlerter que nous pourrons utiliser dans notre application.
Créez blind_alerter.go et déplacez notre interface BlindAlerter et ajoutez les nouvelles choses ci-dessous
Rappelez-vous que n'importe quel type peut implémenter une interface, pas seulement les structs. Si vous créez une bibliothèque qui expose une interface avec une fonction définie, il est courant d'exposer également un type MyInterfaceFunc.
Ce type sera une func qui implémentera également votre interface. De cette façon, les utilisateurs de votre interface ont la possibilité d'implémenter votre interface avec juste une fonction, plutôt que de devoir créer un type struct vide.
Nous créons ensuite la fonction StdOutAlerter qui a la même signature que la fonction et utilisons simplement time.AfterFunc pour la programmer pour imprimer sur os.Stdout.
Mettez à jour main où nous créons NewCLI pour voir cela en action
Avant d'exécuter, vous voudrez peut-être changer l'incrément de blindTime dans CLI à 10 secondes plutôt que 10 minutes juste pour que vous puissiez le voir en action.
Vous devriez voir l'impression des valeurs de blind comme nous l'espérions toutes les 10 secondes. Remarquez que vous pouvez toujours taper Shaun wins dans le CLI et il arrêtera le programme comme nous l'attendons.
Le jeu ne sera pas toujours joué avec 5 personnes, nous devons donc demander à l'utilisateur d'entrer un nombre de joueurs avant le début du jeu.
Écrivez d'abord le test
Pour vérifier que nous demandons le nombre de joueurs, nous voudrons enregistrer ce qui est écrit sur StdOut. Nous l'avons fait plusieurs fois maintenant, nous savons que os.Stdout est un io.Writer donc nous pouvons vérifier ce qui est écrit si nous utilisons l'injection de dépendance pour passer un bytes.Buffer dans notre test et voir ce que notre code va écrire.
Nous ne nous soucions pas de nos autres collaborateurs dans ce test pour le moment, donc nous avons créé des doublures (dummies) dans notre fichier de test.
Nous devrions être un peu méfiants du fait que nous avons maintenant 4 dépendances pour CLI, cela semble peut-être commencer à avoir trop de responsabilités. Vivons avec pour l'instant et voyons si une refactorisation émerge au fur et à mesure que nous ajoutons cette nouvelle fonctionnalité.
Voici notre nouveau test
Nous passons ce qui sera os.Stdout dans main et voyons ce qui est écrit.
Essayez d'exécuter le test
Écrivez le minimum de code pour que le test s'exécute et vérifiez la sortie du test qui échoue
Nous avons une nouvelle dépendance, nous devrons donc mettre à jour NewCLI
Maintenant, les autres tests ne se compileront pas car ils n'ont pas d'io.Writer passé à NewCLI.
Ajoutez dummyStdout pour les autres tests.
Le nouveau test devrait échouer comme ceci
Écrivez suffisamment de code pour le faire passer
Nous devons ajouter notre nouvelle dépendance à notre CLI pour pouvoir y faire référence dans PlayPoker
Enfin, nous pouvons écrire notre invite au début du jeu
Refactorisation
Nous avons une chaîne dupliquée pour l'invite que nous devrions extraire en une constante
Utilisez ceci dans le code de test et dans CLI.
Maintenant, nous devons envoyer un nombre et l'extraire. La seule façon de savoir si cela a eu l'effet désiré est de voir quelles alertes blind ont été programmées.
Écrivez d'abord le test
Ouch ! Beaucoup de changements.
Nous supprimons notre doublure pour StdIn et envoyons plutôt une version simulée représentant notre utilisateur entrant 7
Nous supprimons également notre doublure sur le blind alerter afin de voir que le nombre de joueurs a eu un effet sur la planification
Nous testons quelles alertes sont programmées
Essayez d'exécuter le test
Le test devrait toujours se compiler et échouer en indiquant que les temps programmés sont incorrects car nous avons codé en dur pour que le jeu soit basé sur 5 joueurs
Écrivez suffisamment de code pour le faire passer
Rappelez-vous, nous sommes libres de commettre tous les péchés dont nous avons besoin pour que cela fonctionne. Une fois que nous avons un logiciel fonctionnel, nous pouvons ensuite travailler sur la refactorisation du désordre que nous sommes sur le point de créer !
Nous lisons le
numberOfPlayersInputdans une chaîneNous utilisons
cli.readLine()pour obtenir l'entrée de l'utilisateur, puis appelonsAtoipour la convertir en entier - en ignorant tous les scénarios d'erreur. Nous devrons écrire un test pour ce scénario plus tard.À partir de là, nous modifions
scheduleBlindAlertspour accepter un nombre de joueurs. Nous calculons ensuite un tempsblindIncrementà utiliser pour ajouter àblindTimeau fur et à mesure que nous itérons sur les montants de blind
Bien que notre nouveau test ait été corrigé, beaucoup d'autres ont échoué car maintenant notre système ne fonctionne que si le jeu commence par un utilisateur entrant un nombre. Vous devrez corriger les tests en changeant les entrées utilisateur de sorte qu'un nombre suivi d'un saut de ligne soit ajouté (cela met en évidence encore plus de défauts dans notre approche actuelle).
Refactorisation
Tout cela semble un peu horrible, non ? Écoutons nos tests.
Pour tester que nous programmons des alertes, nous avons configuré 4 dépendances différentes. Chaque fois que vous avez beaucoup de dépendances pour une chose dans votre système, cela implique qu'elle fait trop. Visuellement, nous pouvons le voir à quel point notre test est encombré.
Pour moi, il semble que nous devons faire une abstraction plus claire entre la lecture de l'entrée utilisateur et la logique métier que nous voulons faire
Un meilleur test serait étant donné cette entrée utilisateur, appelons-nous un nouveau type
Gameavec le bon nombre de joueurs.Nous extrairions ensuite le test de la planification dans les tests pour notre nouveau
Game.
Nous pouvons d'abord refactoriser vers notre Game et notre test devrait continuer à passer. Une fois que nous avons fait les changements structurels que nous voulons, nous pouvons réfléchir à la façon dont nous pouvons refactoriser les tests pour refléter notre nouvelle séparation des préoccupations
Rappelez-vous que lors des changements de refactorisation, essayez de les garder aussi petits que possible et continuez à réexécuter les tests.
Essayez-le vous-même d'abord. Réfléchissez aux limites de ce qu'un Game offrirait et de ce que notre CLI devrait faire.
Pour l'instant ne changez pas l'interface externe de NewCLI car nous ne voulons pas changer le code de test et le code client en même temps, c'est trop à jongler et nous pourrions finir par casser des choses.
Voici ce que j'ai proposé :
Du point de vue du "domaine" :
Nous voulons
Start(démarrer) unGame, en indiquant combien de personnes jouentNous voulons
Finish(terminer) unGame, en déclarant le gagnant
Le nouveau type Game encapsule cela pour nous.
Avec ce changement, nous avons passé BlindAlerter et PlayerStore à Game car il est maintenant responsable des alertes et du stockage des résultats.
Notre CLI ne s'occupe maintenant que de :
Construire
Gameavec ses dépendances existantes (que nous refactoriserons ensuite)Interpréter l'entrée utilisateur comme des invocations de méthode pour
Game
Nous voulons essayer d'éviter de faire de "grandes" refactorisations qui nous laissent dans un état de tests défaillants pendant des périodes prolongées, car cela augmente les chances d'erreurs. (Si vous travaillez dans une équipe grande/distribuée, c'est encore plus important)
La première chose que nous ferons est de refactoriser Game pour que nous l'injectons dans CLI. Nous ferons les plus petits changements dans nos tests pour faciliter cela, puis nous verrons comment nous pouvons décomposer les tests dans les thèmes de l'analyse de l'entrée utilisateur et de la gestion du jeu.
Tout ce que nous devons faire maintenant est de changer NewCLI
Cela semble déjà être une amélioration. Nous avons moins de dépendances et notre liste de dépendances reflète notre objectif global de conception du CLI concerné par l'entrée/sortie et déléguant des actions spécifiques au jeu à un Game.
Si vous essayez de compiler, il y a des problèmes. Vous devriez être capable de résoudre ces problèmes vous-même. Ne vous inquiétez pas de créer des mocks pour Game pour l'instant, initialisez simplement de vrais Game juste pour que tout se compile et que les tests soient verts.
Pour ce faire, vous devrez faire un constructeur
Voici un exemple d'une des configurations pour les tests étant corrigée
Il ne devrait pas falloir beaucoup d'efforts pour corriger les tests et revenir au vert (c'est le but !) mais assurez-vous de corriger également main.go avant la prochaine étape.
Maintenant que nous avons extrait Game, nous devrions déplacer nos assertions spécifiques au jeu dans des tests séparés de CLI.
C'est juste un exercice de copie de nos tests CLI mais avec moins de dépendances
L'intention derrière ce qui se passe lorsqu'un jeu de poker démarre est maintenant beaucoup plus claire.
Assurez-vous de déplacer également le test pour quand le jeu se termine.
Une fois que nous sommes satisfaits d'avoir déplacé les tests pour la logique du jeu, nous pouvons simplifier nos tests CLI afin qu'ils reflètent plus clairement nos responsabilités prévues
Traiter l'entrée utilisateur et appeler les méthodes de
Gameau moment appropriéEnvoyer la sortie
Crucialement, il ne connaît pas le fonctionnement réel des jeux
Pour ce faire, nous devrons faire en sorte que CLI ne dépende plus d'un type concret Game mais accepte une interface avec Start(numberOfPlayers) et Finish(winner). Nous pouvons ensuite créer un espion de ce type et vérifier que les appels corrects sont effectués.
C'est ici que nous nous rendons compte que le nommage est parfois maladroit. Renommez Game en TexasHoldem (car c'est le genre de jeu que nous jouons) et la nouvelle interface s'appellera Game. Cela reste fidèle à l'idée que notre CLI ne connaît pas le jeu réel que nous jouons et ce qui se passe lorsque vous Start et Finish.
Remplacez toutes les références à *Game dans CLI et remplacez-les par Game (notre nouvelle interface). Comme toujours, continuez à réexécuter les tests pour vérifier que tout est vert pendant que nous refactorisons.
Maintenant que nous avons découplé CLI de TexasHoldem, nous pouvons utiliser des espions pour vérifier que Start et Finish sont appelés quand nous nous y attendons, avec les bons arguments.
Créez un espion qui implémente Game
Remplacez tout test CLI qui teste une logique spécifique au jeu par des vérifications sur la façon dont notre GameSpy est appelé. Cela reflétera alors clairement les responsabilités du CLI dans nos tests.
Voici un exemple de l'un des tests étant corrigé ; essayez de faire le reste vous-même et consultez le code source si vous êtes coincé.
Maintenant que nous avons une séparation claire des préoccupations, vérifier les cas limites autour de l'IO dans notre CLI devrait être plus facile.
Nous devons aborder le scénario où un utilisateur met une valeur non numérique lorsqu'on lui demande le nombre de joueurs :
Notre code ne devrait pas démarrer le jeu et il devrait afficher une erreur utile à l'utilisateur puis quitter.
Écrivez d'abord le test
Nous commencerons par nous assurer que le jeu ne démarre pas
Vous devrez ajouter à notre GameSpy un champ StartCalled qui n'est défini que si Start est appelé
Essayez d'exécuter le test
Écrivez suffisamment de code pour le faire passer
Là où nous appelons Atoi, nous devons juste vérifier l'erreur
Ensuite, nous devons informer l'utilisateur de ce qu'il a fait de mal, donc nous allons affirmer ce qui est imprimé sur stdout.
Écrivez d'abord le test
Nous avons déjà affirmé ce qui a été imprimé sur stdout avant, donc nous pouvons copier ce code pour l'instant
Nous stockons tout ce qui est écrit sur stdout, nous attendons donc toujours le poker.PlayerPrompt. Nous vérifions ensuite qu'une chose supplémentaire est imprimée. Nous ne sommes pas trop préoccupés par la formulation exacte pour l'instant, nous l'aborderons lors de la refactorisation.
Essayez d'exécuter le test
Écrivez suffisamment de code pour le faire passer
Changez le code de gestion des erreurs
Refactorisation
Maintenant, refactorisez le message dans une constante comme PlayerPrompt
et mettez un message plus approprié
Enfin, nos tests autour de ce qui a été envoyé à stdout sont assez verbeux, écrivons une fonction d'assertion pour les nettoyer.
L'utilisation de la syntaxe vararg (...string) est pratique ici car nous devons affirmer sur des quantités variables de messages.
Utilisez ce helper dans les deux tests où nous affirmons sur les messages envoyés à l'utilisateur.
Il y a un certain nombre de tests qui pourraient être aidés avec des fonctions assertX, alors entraînez-vous à la refactorisation en nettoyant nos tests pour qu'ils se lisent bien.
Prenez le temps et réfléchissez à l'intérêt de certains des tests que nous avons mis en place. Rappelez-vous que nous ne voulons pas plus de tests que nécessaire, pouvez-vous en refactoriser/supprimer certains et toujours être confiant que tout fonctionne ?
Voici ce que j'ai proposé
Les tests reflètent maintenant les principales capacités du CLI, il est capable de lire l'entrée utilisateur en termes de nombre de personnes qui jouent et qui a gagné, et gère lorsqu'une mauvaise valeur est entrée pour le nombre de joueurs. En faisant cela, il est clair pour le lecteur ce que fait CLI, mais aussi ce qu'il ne fait pas.
Que se passe-t-il si au lieu de mettre Ruth wins, l'utilisateur met Lloyd is a killer ?
Terminez ce chapitre en écrivant un test pour ce scénario et en le faisant passer.
Récapitulation
Un rapide récapitulatif du projet
Au cours des 5 derniers chapitres, nous avons lentement TDD-isé une bonne quantité de code
Nous avons deux applications, une application en ligne de commande et un serveur web.
Ces deux applications s'appuient sur un
PlayerStorepour enregistrer les gagnantsLe serveur web peut également afficher un classement de qui gagne le plus de jeux
L'application en ligne de commande aide les joueurs à jouer à un jeu de poker en suivant la valeur actuelle de la blind.
time.Afterfunc
Une façon très pratique de programmer un appel de fonction après une durée spécifique. Il vaut vraiment la peine de prendre le temps de consulter la documentation pour time car elle contient beaucoup de fonctions et méthodes qui vous feront gagner du temps.
Certaines de mes préférées sont
time.After(duration)renvoie unchan Timequand la durée a expiré. Donc si vous souhaitez faire quelque chose après un temps spécifique, cela peut vous aider.time.NewTicker(duration)renvoie unTickerqui est similaire à ce qui précède en ce qu'il renvoie un canal mais celui-ci "tique" à chaque durée, plutôt qu'une seule fois. C'est très pratique si vous voulez exécuter du code toutes lesN durée.
Plus d'exemples de bonne séparation des préoccupations
Généralement, il est de bonne pratique de séparer les responsabilités de traitement des entrées et réponses de l'utilisateur du code de domaine. Vous le voyez ici dans notre application en ligne de commande et aussi dans notre serveur web.
Nos tests sont devenus désordonnés. Nous avions trop d'assertions (vérifier cette entrée, programmer ces alertes, etc.) et trop de dépendances. Nous pouvions visuellement voir qu'il était encombré ; il est si important d'écouter vos tests.
Si vos tests semblent désordonnés, essayez de les refactoriser.
Si vous avez fait cela et qu'ils sont toujours désordonnés, il est très probable que cela pointe vers un défaut dans votre conception
C'est l'une des vraies forces des tests.
Même si les tests et le code de production étaient un peu encombrés, nous pouvions librement refactoriser soutenus par nos tests.
Rappelez-vous que lorsque vous vous trouvez dans ces situations, prenez toujours de petites mesures et réexécutez les tests après chaque changement.
Il aurait été dangereux de refactoriser à la fois le code de test et le code de production en même temps, nous avons donc d'abord refactorisé le code de production (dans l'état actuel, nous ne pouvions pas améliorer beaucoup les tests) sans changer son interface pour pouvoir nous fier à nos tests autant que possible tout en changeant des choses. Ensuite, nous avons refactorisé les tests après l'amélioration de la conception.
Après la refactorisation, la liste des dépendances reflétait notre objectif de conception. C'est un autre avantage de l'injection de dépendances en ce qu'elle documente souvent l'intention. Lorsque vous vous appuyez sur des variables globales, les responsabilités deviennent très peu claires.
Un exemple de fonction implémentant une interface
Lorsque vous définissez une interface avec une méthode, vous pourriez envisager de définir un type MyInterfaceFunc pour la compléter afin que les utilisateurs puissent implémenter votre interface avec juste une fonction.
En faisant cela, les personnes utilisant votre bibliothèque peuvent implémenter votre interface avec juste une fonction. Ils peuvent utiliser la conversion de type pour convertir leur fonction en un BlindAlerterFunc puis l'utiliser comme un BlindAlerter (car BlindAlerterFunc implémente BlindAlerter).
Ce que ceci veut dire d'un point de vue plus général est qu'en Go, vous pouvez ajouter des méthodes à des types, pas seulement à des structs. C'est une fonctionnalité très puissante, et vous pouvez l'utiliser pour implémenter des interfaces de manière plus pratique.
Considérez que vous pouvez non seulement définir des types de fonctions, mais aussi définir des types autour d'autres types, afin de pouvoir leur ajouter des méthodes.
Ici, nous avons créé un gestionnaire HTTP qui implémente un "blog" très simple où il utilisera les chemins URL comme clés pour les publications stockées dans une map.
Mis à jour