C'est une question de conception qui m'est venue lors des tests unitaires. Plongeons dans l'exemple:
Imaginez ceci:
describe('testing bar', () => { it('foo should throw', () => { foo.mockImplementantion(() => { throw new CustomError('error')}); bar() .then((result) => console.log(result)) .catch((err) => { exepect(err).toBeInstanceOf(CustomError)}) // this is what we are testing }) })
Que voyons-nous ici? La seule fonction qui n'a pas de bloc try-catch est bar. Mais si foo échoue, il devrait être attrapé par la capture principale.
En unittesting ceci comme
async function foo()Â { try { return apiCall() } catch (e) { throw new CustomError(e); } } async function bar() { return foo() } async function main() { try { await bar() }catch(e) { console.error(e) } } main()
Le résultat que nous voyons est qu'un Le rejet de promesse non géré est enregistré dans la console.
Donc, ma question est ... même si je sais que le main ()
détectera l'erreur, devrait J'utilise le bloc try-catch dans toutes les fonctions asynchrones?
3 Réponses :
try..catch
peut être nécessaire si une fonction est capable de récupérer d'une erreur, de faire un effet secondaire comme la journalisation ou de renvoyer une erreur plus significative.
Si CustomError
est plus préférable qu'une erreur que apiCall
peut lancer puis try..catch
nécessaire, sinon ce n'est pas le cas. Le problème avec foo
est qu'il ne gère que les erreurs synchrones. Afin de gérer les promesses rejetées, il devrait être return await apiCall ()
, c'est un piège connu de async
.
Les rejets non interceptés sont indésirables, ils entraînent actuellement UnhandledPromiseRejectionWarning
et devraient planter Node dans les versions futures. Il est préférable de gérer une erreur de manière significative au plus haut niveau, donc main
doit attraper l'erreur. Cela peut être délégué au gestionnaire d'événements process
uncaughtRejection
, mais il peut être avantageux pour lui de conserver un niveau supplémentaire de gestion des erreurs qui ne devrait jamais être atteint.
Le résultat que nous voyons est qu'un rejet de promesse non gérée est enregistré dans la console.
Cela ne devrait pas arriver. Un rejet doit être traité par le test. Un point d'échec possible est expliqué ci-dessus, foo
peut renvoyer l'erreur d'origine de apiCall
au lieu de CustomError
au cas où il n'aurait pas été correctement simulé, ceci échouera à l'attente et entraînera un rejet non géré dans catch ()
. Un autre point d'échec est que le test a une promesse déchaînée car il n'a pas été retourné, le test réussit toujours.
Le test asynchrone qui utilise des promesses doit toujours renvoyer une promesse. Cela peut être amélioré en utilisant async..await
. foo
est async
, il devrait toujours renvoyer une promesse:
it('foo should throw', async () => { foo.mockImplementantion(() => { return Promise.reject(new CustomError('error')) }); await expect(bar()).rejects.toThrow(CustomError); })
Maintenant, même si foo code> mock échoue (
foo
mock n'affectera pas bar
s'ils sont définis dans le même module que celui indiqué) et bar
rejette avec quelque chose qui n'est pas CustomError
, cela sera affirmé.
J'ai trois commentaires à ce sujet 1. try..catch peut être nécessaire si une fonction est capable de récupérer d'une erreur, de faire un effet secondaire comme la journalisation ou de relancer une erreur plus significative. C'est vrai, cela n'a pas d'importance dans ce cas 2. Je suis presque sûr que vous ne pouvez pas faire return await .... vous devriez simplement utiliser return 3. J'ai changé du code dans le test et je passe
@JulianMendez return await
est certainement possible, et même nécessaire dans les blocs try
a>
@JulianMendez Alors après tout, c'était juste le cas de test en train d'être cassé? Pouvez-vous nous montrer comment vous l'avez résolu?
Comme @Bergi me l'a dit, je publierai quelques solutions ici
J'emballe la fonction dans un bloc try catch
1.
describe('testing bar', () => { it('foo should throw', () => { foo.mockImplementantion(() => { throw new CustomError('error')}); return bar() .then((result) => { throw result }) .catch((err) => { exepect(err).toBeInstanceOf(CustomError)}) // this is what we are testing }) })
describe('testing bar', () => { it('foo should throw', (done) => { foo.mockImplementantion(() => { throw new CustomError('error')}); bar() .then((result) => { throw result }) // this is because we are expecting an error, so if the promise resolves it's actually a bad sign. .catch((err) => { exepect(err).toBeInstanceOf(CustomError)}) // this is what we are testing done(); }) })
async function bar() { try{ return foo() } catch (e)Â { throw e } }
1. try..catch
dans try {return foo ()} catch (e) {throw e}
est un no-op et concernant la question, oui, c'est quelque chose que vous voulez certainement éviter, contrairement à l'original foo
. Et il ne détecte toujours pas les erreurs asynchrones en raison du fonctionnement de async
. 2. entraînera un délai d'expiration du test lorsque done est inaccessible, c'est la raison pour laquelle done
ne doit pas être utilisé avec des promesses, ils fournissent un flux de contrôle supérieur dans les tests. 3. .then ((result) => {throw result})
provoquera un faux positif au cas où foo
renvoie par erreur CustomError au lieu de lancer, je viens de voir une question qui l'a fait ce.
Re le faux positif lorsque bar ()
renvoie une CustomError
: vous devez utiliser < code> then (result => {throw new Error ("Expected rejection, got" + result);}, err => {exepect (err) .toBeInstanceOf (CustomError)}) au lieu du .catch ()
qui gérerait le résultat renvoyé.
Non. Vous n'avez pas besoin d'utiliser try / catch dans chaque async / await. Vous n'avez besoin que de le faire au plus haut niveau. Dans ce cas, la fonction principale
que vous utilisez déjà.
La météo que vous devriez est une question d'opinion. Les concepteurs du langage go
sont suffisamment convaincus à ce sujet qui est devenu la norme dans go
pour toujours gérer les erreurs à chaque appel de fonction. Mais ce n'est pas la norme en javascript ou dans la plupart des autres langages.
Votre rejet de promesse non gérée est déclenché par votre fonction it ()
car vous ne lui dites pas d'attendre la fin de la promesse.
Je suppose que vous utilisez quelque chose comme mocha
pour le test unitaire (d'autres frameworks peuvent fonctionner différemment). Dans mocha, il existe deux façons de gérer les tests asynchrones:
Appelez le rappel done
- la fonction it ()
sera toujours appelée avec un rappel done
. C'est à vous de savoir si vous voulez l'utiliser ou comme dans votre code posté pour ne pas l'utiliser:
describe('testing bar', () => { it('foo should throw', (done) => { foo.mockImplementantion(() => { throw new CustomError('error')}); return bar() // <----------- THIS WOULD ALSO FIX IT .then((result) => { console.log(result); }) .catch((err) => { exepect(err).toBeInstanceOf(CustomError); }) }) })
Renvoyez une promesse. Si vous renvoyez une promesse à la fonction it ()
, mocha sera conscient que votre code est asynchrone et attendra la fin:
describe('testing bar', () => { it('foo should throw', (done) => { foo.mockImplementantion(() => { throw new CustomError('error')}); bar() .then((result) => { console.log(result); done(); // ------------- THIS IS YOUR ACTUAL BUG }) .catch((err) => { exepect(err).toBeInstanceOf(CustomError); done(); // ------------- THIS IS YOUR ACTUAL BUG }) }) })
En bref, il n'y a rien de mal avec votre code. Mais vous avez un bogue dans votre test unitaire .
Notez que cette réponse ne fait que reformuler ce que @Bergi essaie de vous dire dans les commentaires. Votre réaction à son commentaire me dit que vous n'avez pas compris ce qu'il essayait de vous dire (non, vous n'avez pas besoin de modifier votre code - modifier votre test unitaire) alors j'ai élaboré.
Oh oui, j'ai compris ce que @Bergi m'a dit en deux. Il y a beaucoup de solutions à mon problème et je pense la même chose que vous: je n'ai pas besoin de bloc try / catch dans chaque async / await que j'utilise. Je publierai une autre solution au problème
C'est Jest mais cela fonctionne de la même manière que Mocha à cet égard. 1 pourrait être ignoré car il montre ce qui ne va pas avec done
dans les promesses. La plupart du temps, un développeur n'a pas assez de discipline pour garantir un flux de contrôle correct. Dans ce cas, done
après une assertion entraînera un délai d'expiration du test car il n'est jamais appelé lorsqu'une assertion échoue. Cela devrait être .then (() => done (), done.fail)
comme dernière chaîne pour couvrir toutes les bases ... sauf que cela ne devrait pas parce que cela est déjà fait quand un la promesse est retournée comme indiqué en 2. TL; DR: n'utilisez pas done
avec les promesses.
Étant donné qu'un processus node.js peut / se terminera à partir d'un rejet de promesse non géré, il est conseillé de toujours utiliser try / catch avec await et d'ajouter toujours un gestionnaire .catch à la fin des chaînes Promise.
ce que vous me dites, c'est que je devrais envelopper la fonction
bar ()
dans un bloc try catch, non?La fonction asynchrone
bar
renvoie une promesse non? La fonction asyncfoo
peut lever une exception dans son bloc catch qui provoque un rejet de promesse non gérée dansbar
, donc ....... .Votre scénario de test ne
retourne
pas la promesse ou n'utiliseasync
/wait
et il n'a pas de rappelterminé
, il est donc probable queexpect
lève une exception concernant l'appel après la fin du test. Quel est le message d'erreur du rejet non géré?@Bergi (nœud: 44872) UnhandledPromiseRejectionWarning: CustomError