Comment gérer les références de l’API de composition dans Vue 3 lors des tests unitaires
Réutilisez la logique d’état dans Vue avec des composables écrits avec l’API Composition. En savoir plus.
L’API Composition pour Vue a ouvert une manière très puissante et flexible de coder les composants Vue. Une grande partie de cette flexibilité réside dans la possibilité de créer des composables, qui sont, comme décrit dans la documentation Vue, «une fonction qui exploite l’API de composition de Vue pour encapsuler et réutiliser la logique avec état.»
Un problème que j’ai souvent rencontré est que la manière de tester le utiliser La présence de ces composables au sein d’un composant n’est pas immédiatement évidente, et peut conduire à la création de mauvaises pratiques.
Dans cet article, nous explorerons comment tester l’utilisation d’un composable dans un composant Vue 3 avec Jest et Vue Test Utils. Les concepts, cependant, sont directement traduisibles en Vitest.
Concepts clés
Avant de plonger dans le code, je souhaite établir quelques concepts clés.
Nous tirerons parti de l’utilisation de jest.mock pour simuler l’ensemble du composable lors du test, cela signifie que nous ne testerons pas directement le composants internes du composable. Ceci est particulièrement important lors du test de composables qui effectuent des appels d’API en interne ou sont fournis par une bibliothèque tierce.
Il n’est pas rare que certaines équipes évitent cela au profit de tests approfondis de l’intégration d’un composant avec toutes les dépendances, mais cette pratique peut s’avérer problématique car elle brise le concept d’encapsulation des tests unitaires. L’essentiel est qu’un test unitaire qui teste deux composants ou fichiers différents entre dans le territoire d’un test d’intégration.
Cela ne veut en aucun cas dire qu’il ne faut pas tester le composable de manière approfondie, mais il faut le tester séparément comme une unité. En tant que consommateur de cette unité, le composant qui l’utilise doit être sûr que les entrées et sorties (E/S) contractuelles du composant correspondent à ce qu’il attend, et toute refactorisation des E/S de l’unité doit être traitée. comme un changement radical dans le contexte plus large de la base de code. (Ouf.)
Cependant, dans certains cas, comme les composables simples qui, par exemple, ne fournissent qu’un sous-ensemble de computed valeurs, il peut être acceptable de ne pas se moquer des retours de la fonction. Cependant, en général, je recommande de se moquer du contenu de ces fichiers dans le composant qui les importe, sinon nous créerions des tests d’intégration comme indiqué précédemment.
Après avoir exposé tout cela, passons à l’exemple.
Le code
Supposons que nous disposions d’un composable qui obtient des données du point de terminaison de notre API pour un utilisateur. Dans ce composable, quelques appels réseau ont lieu et les données sont analysées dans quelques propriétés calculées différentes qui sont renvoyées par la fonction afin que nous puissions vérifier les autorisations et l’état des utilisateurs. De plus, nous aurons un update fonction qui modifierait la configuration de l’utilisateur.
Les composants internes de ce composable ne sont pas importants. La seule chose que nous devons savoir en tant que consommateur est le contrat : quelles propriétés ce composable attend-il et que retournera-t-il lorsque je l’appellerai ?
useUser.js
export const useUser = (userId) => {
[...]
return {
update, // a function
user, // a ref with the User object
isEmployee // a computed Boolean
}
}
Chaque fois que nous appelons le useUser composable, on sait qu’il faudra passer un userId et nous recevrons en retour les trois propriétés déclarées ci-dessus.
Créons un composant de démonstration simplifié qui utilisera notre useUser composable.
Il se passe quelques choses ici, alors examinons-les en profondeur.
Nous créons une version extrêmement simplifiée d’un formulaire pour notre utilisateur où nous pouvons modifier à la fois l’e-mail et, dans le cas où l’utilisateur est un employé, son identifiant d’employé.
Notez que l’entrée de l’ID de l’employé ne sera affichée que dans le cas où le montant calculé isEmploye est vrai – nous devrons tester cela.
Lorsque l’utilisateur sera soumis, il appellera notre submit méthode.
Nous importons nos useUser composable et appelez-le avec le userId reçu par l’accessoire userId… nous voudrons tester cela. Nous en extrayons les trois propriétés consignées en tirant parti de la destruction.
Dans notre fonction de soumission, nous appelons le update fonction avec l’ID de l’utilisateur comme premier paramètre et un deuxième paramètre nous mettons à jour les propriétés de l’utilisateur email et employeeID avec les valeurs mises à jour.
Nous supposons par souci de simplicité qu’il est acceptable de muter le user objet qui nous est rendu par le composable. Dans certains cas, cela peut ne pas être vrai, mais nous utiliserons cela comme exemple plus tard pour clarifier un point.
Maintenant que nous avons notre composant, nous pouvons commencer à écrire nos tests.
Test du composant UpdateUserForm
La première chose que nous voudrons tester est que notre useUser composable est appelé avec la valeur correcte. Nous ne voulons pas être dans une situation où il ne reçoit pas correctement l’identifiant utilisateur en tant que propriété. Pour cela, nous devrons déjà configurer notre maquette, parcourons donc le code ensemble.
Il se passe beaucoup de choses ici, alors allons-y étape par étape.
Nous utilisons jest.mock pour se moquer du tout /useUser déposer. Nous contrôlerons le retour du useUser fonction à la ligne 16.
Dans un beforeEach appelle, nous clearAllMocks pour plaisanter parce que nous utiliserons jest.fn pour garder une trace de update fonction. Nous utilisons également des plaisanteries mockReturnValue pour se moquer du retour du useUser composable. Cela signifie que nous devons fournir ici les retours du composable.
Le premier retour update est déclaré ci-dessus comme un jest.fnnous le déclarons ci-dessus comme un const afin que nous puissions y accéder facilement dans tous nos tests sans avoir à rétablir le mockReturnValue dans chaque test que nous vérifions. Cela peut sembler excessif, mais le fait de se moquer à nouveau de la valeur de retour à chaque test peut rapidement gonfler le document.
Nous déclarons également aux lignes 10 et 11 ref valeurs pour notre user valeur et un ref pour le isEmployee calculé, plus tard à l’intérieur du beforeEach bloc, nous réinitialisons les valeurs de ces deux valeurs.
Quelques questions peuvent se poser à ce stade, la première étant pourquoi utilisons-nous un ref simuler une valeur calculée ?
La réponse est par simplicité. Vous pouvez absolument définir le isEmployee constant à un calculé comme tel.
const isEmployee = computed(() => false)
Cependant, cela rendra la tâche un peu difficile lors de l’écriture de tests, car vous pourrez facilement modifier la valeur de retour de cette propriété à la volée pour vérifier les différents états de votre composant sans avoir à simuler à nouveau le composable. Cela aura plus de sens lorsque nous écrirons des tests pour cela.
La deuxième question qui peut se poser est la suivante : pourquoi déclarons-nous les références en dehors du champ d’application de tous nos tests, puis les réinitialisons-nous dans le haut de la liste ? beforeEach bloc?
Au fur et à mesure que vous écrivez de plus en plus de tests utilisant ces valeurs, vous serez confronté à deux défis. Soit vous devez faire le choix de re-simuler les valeurs de retour du composable à chaque test ou de décrire un bloc pour l’adapter à ce que vous essayez de tester, ce qui peut être écrasant et sujet aux erreurs, ou de les conserver dans une portée où elles se trouvent. accessible à tous vos tests.
Pour la propreté, je recommande de faire tout cela au plus haut niveau describe bloc de votre test comme indiqué ici.
La deuxième partie est que nous choisissons de réinitialiser les valeurs de ces références. beforeEach test parce que ces références le feront retenir la valeur que le dernier test leur a laissée car leur portée est en dehors des tests eux-mêmes. Nous utiliserons le niveau supérieur beforeEach comme point de configuration pour l’ensemble de notre suite de tests, il est donc logique d’y définir les valeurs par défaut. C’est pourquoi le user ref est initialement créé avec un objet vide comme valeur, car cette valeur est bientôt remplacée.
Passons à autre chose et écrivons un test qui couvre le v-if de notre identifiant d’employé input.
UpdateUserForm.spec.js
import UpdateUserForm from './UpdateUserForm.vue'
import { useUser } from './useUser'
import { shallowMount } from 'vue@test-utils'
import { ref } from 'vue'
jest.mock('./useUser')
describe('UpdateUserForm', () => {
const update = jest.fn().mockResolvedValue(true)
const user = ref({})
const isEmployee = ref(false)
beforeEach(() => {
jest.clearAllMocks()
user.value = { email: 'test@test.com', employeeID: null }
isEmployee.value = false
useUser.mockReturnValue({
update,
user,
isEmployee
})
})
it('passes the userId prop to the useUser composable', () => {
const userId = 123
shallowMount({
props: { userId }
})
expect(useUser).toHaveBeenCalledWith(123)
})
// NEW BLOCK
describe('When the user is an employee', () => {
beforeEach(() => {
isEmployee.value = true
})
it('displays the employee ID input field when the user is an employee', () => {
const wrapper = shallowMount({
props: { userId: 123 }
})
expect(wrapper.find('[data-testid="employee-id"]').exists()).toBe(true)
})
})
// NEW BLOCK
describe('When the user is not an employee', () => {
beforeEach(() => {
isEmployee.value = false
})
it('displays the employee ID input field when the user is an employee', () => {
const wrapper = shallowMount({
props: { userId: 123 }
})
expect(wrapper.find('[data-testid="employee-id"]').exists()).toBe(false)
})
})
})
Examinons encore une fois en profondeur.
Nous avons opté pour la création de deux describe des blocs qui décrivent avec précision l’état de l’application que nous voulons tester. L’un d’eux contiendra les tests lorsque notre composant s’occupe d’un employé, et un autre lorsque ce n’est pas le cas. Notez que nous avons ajouté beforeEach blocs sur chacun d’eux pour une configuration claire pour les tests. Dans chacun d’eux, nous fixons la valeur du isEmployee référez-vous à vrai ou faux selon les besoins. Nous pouvons être assurés que ce changement de valeur n’affectera que ces tests, car nous avons établi une réinitialisation pour cela plus tôt au niveau supérieur. beforeEach bloc.
Nous avons ensuite ajouté un test pour chacun des scénarios.
À première vue, cela peut sembler beaucoup de passe-partout et de structure pour des tests aussi simples, et c’est le cas, mais la réalité est que dans un environnement de production, vous aurez probablement un peu plus de tests et de scénarios complexes. Avoir un bon ordre et une bonne structure pour vos tests est toujours une bonne pratique !
Passons à la dernière partie des tests, vérifiant la capacité de l’utilisateur à soumettre les informations.
UpdateUserForm.spec.js
import UpdateUserForm from './UpdateUserForm.vue'
import { useUser } from './useUser'
import { shallowMount, flushPromises } from 'vue@test-utils'
import { ref } from 'vue'
jest.mock('./useUser')
describe('UpdateUserForm', () => {
const update = jest.fn()
const user = ref({})
const isEmployee = ref(false)
beforeEach(() => {
jest.clearAllMocks()
user.value = { email: 'test@test.com', employeeID: null }
isEmployee.value = false
useUser.mockReturnValue({
update,
user,
isEmployee
})
})
it('passes the userId prop to the useUser composable', () => {
const userId = 123
shallowMount({
props: { userId }
})
expect(useUser).toHaveBeenCalledWith(123)
})
describe('When the user is an employee', () => {
beforeEach(() => {
isEmployee.value = true
})
it('displays the employee ID input field when the user is an employee', () => {
const wrapper = shallowMount({
props: { userId: 123 }
})
expect(wrapper.find('[data-testid="employee-id"]').exists()).toBe(true)
})
// NEW
it('calls the update function when the user submits the form', () => {
const userId = 123
const wrapper = shallowMount({
props: { userId }
})
wrapper.find('[data-testid="email"]').setValue('email@test.com')
wrapper.find('form').trigger('submit')
await flushPromises()
expect(update).toHaveBeenCalledWith(userId, {
email: 'email@test.com',
employeeID: null
})
})
})
describe('When the user is not an employee', () => {
beforeEach(() => {
isEmployee.value = false
})
it('displays the employee ID input field when the user is an employee', () => {
const wrapper = shallowMount({
props: { userId: 123 }
})
expect(wrapper.find('[data-testid="employee-id"]').exists()).toBe(false)
})
})
})
À la ligne 48, nous avons ajouté un nouveau test pour couvrir la soumission du formulaire lorsque l’utilisateur n’est pas un employé.
Nous trouvons le email saisissez et définissez sa valeur sur un nouvel e-mail et procédez à la soumission du formulaire.
Nous devons await flushPromises car nous avons déclaré que le update la fonction va retourner un Promise et est attendu.
Enfin on vérifie que le update la fonction (qui a été renvoyée par le composable) est appelée avec les valeurs correctes.
Une chose importante à mentionner, qui n’est peut-être pas immédiatement évidente, est que user ref est modifié par le composant chaque fois que nous mettons à jour la valeur de l’une des entrées, comme le v-model les liaisons de chacune des entrées pointent en fait vers l’objet utilisateur.
J’ai mentionné plus tôt que cela n’était peut-être pas le cas dans votre composant particulier, et ce n’est pas particulièrement une pratique que j’approuve, car la mutation des valeurs de retour provenant d’une boîte noire telle qu’un composable entraîne fréquemment des problèmes et du code spaghetti. Cependant, je voulais illustrer l’importance du ref gestion que nous avons faite le premier beforeEach bloc.
Si nous avions sauté le paramétrage des valeurs par défaut sur ce bloc, nous serions dans une situation où les tests suivant celui-ci fonctionneraient avec l’email que nous paramétrons sur ce test. Cela serait peut-être facile à identifier dans un composant aussi simple que celui-ci, mais, à mesure que votre application devient de plus en plus complexe, ce type d’erreurs devient extrêmement difficile à cerner car elles produisent des faux positifs dans vos tests.
Conclusion
La gestion des références et la manière dont nous structurons nos tests unitaires doivent faire l’objet d’un examen attentif avec autant d’attention aux détails et au soin que la composition réelle des composants elle-même. À mesure que les applications se développent et deviennent exponentiellement plus complexes, avoir de bonnes pratiques de nettoyage et une gestion de vos valeurs réactives vous évitera bien des maux de tête.
octobre 28, 2024
Comment gérer les références de l’API de composition dans Vue 3 lors des tests unitaires
Réutilisez la logique d’état dans Vue avec des composables écrits avec l’API Composition. En savoir plus.
L’API Composition pour Vue a ouvert une manière très puissante et flexible de coder les composants Vue. Une grande partie de cette flexibilité réside dans la possibilité de créer des composables, qui sont, comme décrit dans la documentation Vue, «une fonction qui exploite l’API de composition de Vue pour encapsuler et réutiliser la logique avec état.»
Un problème que j’ai souvent rencontré est que la manière de tester le utiliser La présence de ces composables au sein d’un composant n’est pas immédiatement évidente, et peut conduire à la création de mauvaises pratiques.
Dans cet article, nous explorerons comment tester l’utilisation d’un composable dans un composant Vue 3 avec Jest et Vue Test Utils. Les concepts, cependant, sont directement traduisibles en Vitest.
Concepts clés
Avant de plonger dans le code, je souhaite établir quelques concepts clés.
Nous tirerons parti de l’utilisation de
jest.mock
pour simuler l’ensemble du composable lors du test, cela signifie que nous ne testerons pas directement le composants internes du composable. Ceci est particulièrement important lors du test de composables qui effectuent des appels d’API en interne ou sont fournis par une bibliothèque tierce.Il n’est pas rare que certaines équipes évitent cela au profit de tests approfondis de l’intégration d’un composant avec toutes les dépendances, mais cette pratique peut s’avérer problématique car elle brise le concept d’encapsulation des tests unitaires. L’essentiel est qu’un test unitaire qui teste deux composants ou fichiers différents entre dans le territoire d’un test d’intégration.
Cela ne veut en aucun cas dire qu’il ne faut pas tester le composable de manière approfondie, mais il faut le tester séparément comme une unité. En tant que consommateur de cette unité, le composant qui l’utilise doit être sûr que les entrées et sorties (E/S) contractuelles du composant correspondent à ce qu’il attend, et toute refactorisation des E/S de l’unité doit être traitée. comme un changement radical dans le contexte plus large de la base de code. (Ouf.)
Cependant, dans certains cas, comme les composables simples qui, par exemple, ne fournissent qu’un sous-ensemble de
computed
valeurs, il peut être acceptable de ne pas se moquer des retours de la fonction. Cependant, en général, je recommande de se moquer du contenu de ces fichiers dans le composant qui les importe, sinon nous créerions des tests d’intégration comme indiqué précédemment.Après avoir exposé tout cela, passons à l’exemple.
Le code
Supposons que nous disposions d’un composable qui obtient des données du point de terminaison de notre API pour un utilisateur. Dans ce composable, quelques appels réseau ont lieu et les données sont analysées dans quelques propriétés calculées différentes qui sont renvoyées par la fonction afin que nous puissions vérifier les autorisations et l’état des utilisateurs. De plus, nous aurons un
update
fonction qui modifierait la configuration de l’utilisateur.Les composants internes de ce composable ne sont pas importants. La seule chose que nous devons savoir en tant que consommateur est le contrat : quelles propriétés ce composable attend-il et que retournera-t-il lorsque je l’appellerai ?
useUser.js
Chaque fois que nous appelons le
useUser
composable, on sait qu’il faudra passer unuserId
et nous recevrons en retour les trois propriétés déclarées ci-dessus.Créons un composant de démonstration simplifié qui utilisera notre
useUser
composable.UpdateUserForm.vue
Il se passe quelques choses ici, alors examinons-les en profondeur.
isEmploye
est vrai – nous devrons tester cela.submit
méthode.useUser
composable et appelez-le avec leuserId
reçu par l’accessoireuserId
… nous voudrons tester cela. Nous en extrayons les trois propriétés consignées en tirant parti de la destruction.update
fonction avec l’ID de l’utilisateur comme premier paramètre et un deuxième paramètre nous mettons à jour les propriétés de l’utilisateuremail
etemployeeID
avec les valeurs mises à jour.user
objet qui nous est rendu par le composable. Dans certains cas, cela peut ne pas être vrai, mais nous utiliserons cela comme exemple plus tard pour clarifier un point.Maintenant que nous avons notre composant, nous pouvons commencer à écrire nos tests.
Test du composant UpdateUserForm
La première chose que nous voudrons tester est que notre
useUser
composable est appelé avec la valeur correcte. Nous ne voulons pas être dans une situation où il ne reçoit pas correctement l’identifiant utilisateur en tant que propriété. Pour cela, nous devrons déjà configurer notre maquette, parcourons donc le code ensemble.UpdateUserForm.spec.js
Il se passe beaucoup de choses ici, alors allons-y étape par étape.
jest.mock
pour se moquer du tout/useUser
déposer. Nous contrôlerons le retour duuseUser
fonction à la ligne 16.beforeEach
appelle, nousclearAllMocks
pour plaisanter parce que nous utiliseronsjest.fn
pour garder une trace deupdate
fonction. Nous utilisons également des plaisanteriesmockReturnValue
pour se moquer du retour duuseUser
composable. Cela signifie que nous devons fournir ici les retours du composable.update
est déclaré ci-dessus comme unjest.fn
nous le déclarons ci-dessus comme unconst
afin que nous puissions y accéder facilement dans tous nos tests sans avoir à rétablir lemockReturnValue
dans chaque test que nous vérifions. Cela peut sembler excessif, mais le fait de se moquer à nouveau de la valeur de retour à chaque test peut rapidement gonfler le document.ref
valeurs pour notreuser
valeur et unref
pour leisEmployee
calculé, plus tard à l’intérieur dubeforeEach
bloc, nous réinitialisons les valeurs de ces deux valeurs.Quelques questions peuvent se poser à ce stade, la première étant pourquoi utilisons-nous un
ref
simuler une valeur calculée ?La réponse est par simplicité. Vous pouvez absolument définir le
isEmployee
constant à un calculé comme tel.Cependant, cela rendra la tâche un peu difficile lors de l’écriture de tests, car vous pourrez facilement modifier la valeur de retour de cette propriété à la volée pour vérifier les différents états de votre composant sans avoir à simuler à nouveau le composable. Cela aura plus de sens lorsque nous écrirons des tests pour cela.
La deuxième question qui peut se poser est la suivante : pourquoi déclarons-nous les références en dehors du champ d’application de tous nos tests, puis les réinitialisons-nous dans le haut de la liste ?
beforeEach
bloc?Au fur et à mesure que vous écrivez de plus en plus de tests utilisant ces valeurs, vous serez confronté à deux défis. Soit vous devez faire le choix de re-simuler les valeurs de retour du composable à chaque test ou de décrire un bloc pour l’adapter à ce que vous essayez de tester, ce qui peut être écrasant et sujet aux erreurs, ou de les conserver dans une portée où elles se trouvent. accessible à tous vos tests.
Pour la propreté, je recommande de faire tout cela au plus haut niveau
describe
bloc de votre test comme indiqué ici.La deuxième partie est que nous choisissons de réinitialiser les valeurs de ces références.
beforeEach
test parce que ces références le feront retenir la valeur que le dernier test leur a laissée car leur portée est en dehors des tests eux-mêmes. Nous utiliserons le niveau supérieurbeforeEach
comme point de configuration pour l’ensemble de notre suite de tests, il est donc logique d’y définir les valeurs par défaut. C’est pourquoi leuser
ref est initialement créé avec un objet vide comme valeur, car cette valeur est bientôt remplacée.Passons à autre chose et écrivons un test qui couvre le
v-if
de notre identifiant d’employéinput
.UpdateUserForm.spec.js
Examinons encore une fois en profondeur.
describe
des blocs qui décrivent avec précision l’état de l’application que nous voulons tester. L’un d’eux contiendra les tests lorsque notre composant s’occupe d’un employé, et un autre lorsque ce n’est pas le cas. Notez que nous avons ajoutébeforeEach
blocs sur chacun d’eux pour une configuration claire pour les tests. Dans chacun d’eux, nous fixons la valeur duisEmployee
référez-vous à vrai ou faux selon les besoins. Nous pouvons être assurés que ce changement de valeur n’affectera que ces tests, car nous avons établi une réinitialisation pour cela plus tôt au niveau supérieur.beforeEach
bloc.À première vue, cela peut sembler beaucoup de passe-partout et de structure pour des tests aussi simples, et c’est le cas, mais la réalité est que dans un environnement de production, vous aurez probablement un peu plus de tests et de scénarios complexes. Avoir un bon ordre et une bonne structure pour vos tests est toujours une bonne pratique !
Passons à la dernière partie des tests, vérifiant la capacité de l’utilisateur à soumettre les informations.
UpdateUserForm.spec.js
À la ligne 48, nous avons ajouté un nouveau test pour couvrir la soumission du formulaire lorsque l’utilisateur n’est pas un employé.
email
saisissez et définissez sa valeur sur un nouvel e-mail et procédez à la soumission du formulaire.await flushPromises
car nous avons déclaré que leupdate
la fonction va retourner unPromise
et est attendu.update
la fonction (qui a été renvoyée par le composable) est appelée avec les valeurs correctes.Une chose importante à mentionner, qui n’est peut-être pas immédiatement évidente, est que
user
ref est modifié par le composant chaque fois que nous mettons à jour la valeur de l’une des entrées, comme lev-model
les liaisons de chacune des entrées pointent en fait vers l’objet utilisateur.J’ai mentionné plus tôt que cela n’était peut-être pas le cas dans votre composant particulier, et ce n’est pas particulièrement une pratique que j’approuve, car la mutation des valeurs de retour provenant d’une boîte noire telle qu’un composable entraîne fréquemment des problèmes et du code spaghetti. Cependant, je voulais illustrer l’importance du
ref
gestion que nous avons faite le premierbeforeEach
bloc.Si nous avions sauté le paramétrage des valeurs par défaut sur ce bloc, nous serions dans une situation où les tests suivant celui-ci fonctionneraient avec l’email que nous paramétrons sur ce test. Cela serait peut-être facile à identifier dans un composant aussi simple que celui-ci, mais, à mesure que votre application devient de plus en plus complexe, ce type d’erreurs devient extrêmement difficile à cerner car elles produisent des faux positifs dans vos tests.
Conclusion
La gestion des références et la manière dont nous structurons nos tests unitaires doivent faire l’objet d’un examen attentif avec autant d’attention aux détails et au soin que la composition réelle des composants elle-même. À mesure que les applications se développent et deviennent exponentiellement plus complexes, avoir de bonnes pratiques de nettoyage et une gestion de vos valeurs réactives vous évitera bien des maux de tête.
Source link
Partager :
Articles similaires