✕ סגור 
צור קשר
תודה על ההתעניינות .

Thank you! Your submission has been received!

Oops! Something went wrong while submitting the form

בדיקות יחידה בעולם האמיתי עם ריאקט, Enzyme ו-Jest – מוקינג לסרביסים

רן בר זיק
|
קלה
|
Aug 13, 2019
alt="facebook"alt="linkedin"להרשמה לניוזלטר

אחד הנושאים שדנתי בהם לא מעט הוא בדיקות יחידה בריאקט. אחד הדברים שהכי מתסכלים אותי במאמרים האלו, הוא זה שאני נאלץ לפשט את הדוגמאות ומדובר ביותר מדי דוגמאות של hello world, ואז ניגשים לכתיבת טסטים וכל הדוגמאות הנחמדות והפשוטות שאני מביא מתנפצות על קרקע המציאות. כי במציאות, הקומפוננטות מורכבות יותר וכתיבת בדיקות אוטומטיות לקומפוננטות יותר מורכבות זה לא קל – לפחות בהתחלה. אבל שווה להשקיע את המאמץ ההתחלתי ולהבין בבדיקות כי אם יש משהו שמבטיח איכות, מהירות פיתוח ואפס תקלות אלו בדיקות אוטומטיות. כן, כן.


אז דיברנו קודם על בדיקות של קומפוננטת hello world וגם של קומפוננטה שמקבלת props. אבל במציאות קומפוננטות עצמאיות יקבלו מידע מסרביסים. כלומר קוד שקורא החוצה לשרת. בואו ואציג לכם קומפוננטה כזו. קומפוננטת בדיחת אבא. כן כן.


קודם כל, היכנסו לקישור הבא כדי לראות איך זה עובד. תראו בעצם דף שבו יש בדיחת אבא שמשתמשת ב-API. שימו לב שהשארתי את ה-sourcemaps פתוחים, אז בהחלט אתם מוזמנים להיכנס לכלי המפתחים ולדבג שם. באפליקציות אמיתיות אגב, זה נחשב כחולשת אבטחה.


אז איך זה עובד? יש לנו service פשוט שמשתמש ב-fetch API כדי לקבל את בדיחת האבא. ה-service החביב הזה נמצא ב-src/services/DadJokeService.service.js ונראה ככה:

class DadJokeService {
 getDadJoke() {
   return fetch(`https://icanhazdadjoke.com`, {
     headers: {
       'Accept': 'application/json'
       },
     })
     .then(res => res.json())
     .then(json => json.joke );
 }
}
const instance = new DadJokeService();
export default instance;

אין פה משהו שמתכנת בסיסי לא מבין. זה ג׳אווהסקריפט טהור, לא קשור לריאקט או לפריימוורק. סתם service שכתוב בונילה - הכי פשוט שיש.


עכשיו, הקומפוננטה שצורכת אותו. איך היא נראית? גם פשוטה למדי. היא נמצאת בנתיב הזה: src/components/DadJoke/DadJoke.jsx ובנויה ככה:


import React from 'react';
import DadJokeService from '../../services/DadJokeService.service';
class DadJoke extends React.Component {
 constructor() {
   super();
   this.state = { joke: 'Loading joke...' };
 }
 componentDidMount() {
   DadJokeService.getDadJoke().then(result => this.setState({ joke: result }));
 }
 
 render() {
     return <p>{this.state.joke}</p>
 }
}
export default DadJoke;

אז מה לא פשוט כאן?


הכי פשוט בעולם. בהתחלה הקומפוננטה מריצה Loading ומיד אחרי שהיא מקבלת תשובה מה-API היא מציגה את מה שיש שם. הכי פשוט שיש.


מה לא פשוט? לבדוק את זה. עד עכשיו כל המאמרים וגם כל הדוגמאות היו על קומפוננטות מאוד סטטיות. זו קומפוננטה דינמית. איך אני בודק אותה?


אולי כדאי לדבר קודם על איך לא לבדוק אותה. לא מרנדרים אותה איך שהיא. למה? כי אם נעשה את זה, נצטרך לחכות בכל בדיקה ובדיקה לתוצאות ה-API. וזה רעיון רע מאוד בבדיקות יחידה שאמורות להתרכז בפונקציונליות הבסיסית של הקומפוננטה. אנחנו לא עושים כאן בדיקות End to End או אינטגרציה. אנחנו עושים בדיקה אך ורק לקוד של הקומפוננטה. אני רוצה לבדוק שאכן הקומפוננטה קוראת ל-service. אני רוצה לבדוק שהקומפוננטה מציגה את המידע שה-service מחזיר. הדבר האחרון שאני רוצה לבדוק זה את ה-service. יש לו את הבדיקות שלו. אני בטח ובטח לא רוצה לבדוק את ה-API בבדיקת היחידה הזו. בדיקות יחידה הן… ובכן, אך ורק ליחידת הקוד הספציפית הזו.


אז איך עושים את זה? באמצעות mock. שזה ׳לדמות׳ את ה-service ואת התגובה שלו. יצירת mock היא ממש ממש פשוטה ב-jest. עושים import למודול שלנו, במקרה הזה ה-service ואז משתמשים ב-jest.fn();


נשמע מסובך? בואו ונדגים:


import DadJokeService from '../../services/DadJokeService.service';
   DadJokeService.getDadJoke = jest.fn();
   DadJokeService.getDadJoke.mockReturnValue('This is mock dadjoke');

וזה גם יהיה בכל הדוגמאות שיש באתרים השונים ובדוקומנטציה של jest. שזה נחמד אבל במציאות זה לא עובד ככה. למה? כי בגלל שמדובר ב-service וב-fetch, אני צריך לעשות קריאה אסינכרונית ולא סינכרונית. אז איך עושים mock ומדמים תגובה אסינכרונית? זה קל.


   ה-mock צריך להחזיר promise.


   קיום ההבטחה צריך להיות מייד.


אופן הביצוע


איך עושים את שניהם? עם הקוד הבא:


import React from 'react';
import { shallow } from 'enzyme';
import DadJoke from './DadJoke';
import DadJokeService from '../../services/DadJokeService.service';
let element;
describe('DadJoke Component is working normally', () => {
 beforeEach(() => {
   // mock return promise
   const dadJokeResult = Promise.resolve('This is mock dadjoke');
   DadJokeService.getDadJoke = jest.fn();
   DadJokeService.getDadJoke.mockReturnValue(dadJokeResult);
 });
 it('renders without crashing', async () => {
   element = shallow(<DadJoke />);
   expect(element.text()).toContain('Loading');
 });
 it('implement service', async () => {
   element = shallow(<DadJoke />);
   await flushAllPromises();
   expect(element.text()).toContain('This is mock dadjoke');
 });
 // Resolving all
 const flushAllPromises = () => new Promise((resolve) => setImmediate(resolve()));
});

ואפשר לראות כמה זה פשוט. פשוט יוצרים Promise וגורמים ל-mock להחזיר אותו ובסוף בסוף, כשאני רוצה שכל ה-Promises יופעלו, אני יוצר פונקציית עזר קטנה בשם flushAllPromises, שמשתמשת ב-node כדי לעשות flush. שימו לב שהבדיקה חייבת להיות אסינכרונית, ואת זה אני עושה באמצעות שימוש ב-async ב-it.


אני יודע שזה נראה מסובך, אבל ככה זה עובד בעולם האמיתי כשיוצאים מעולם ה-hello world. יוצא לנו לעבוד עם promises, ויוצא לנו לעבוד לפעמים עם שירותים ודברים אחרים שצריך לעשות להם mocking.


אבל האמת היא, שכשהתייעצתי עם ערן שפירא, ראש הצוות שלי שנתן את דוגמאות הקוד במאמר, הוא טען ובצדק שבעולם האמיתי לא יהיו לנו את הדילמה הזו כי אנחנו משתמשים ברידקס, וזה נכון. במאמר הבא אני אבצע הסבר קצר על דיבוג בדיקות, ואז נמשיך הלאה לרידקס ולפיצ׳רים נוספים שמזכירים אותו.

מאת: רן בר זיק, מתכנת מומחה ועיתונאי טכנולוגי

רוצים להתעדכן בתכנים נוספים בנושאי ענן וטכנולוגיות מתקדמות? הירשמו עכשיו לניוזלטר שלנו ותמיד תישארו בעניינים > להרשמה

אחד הנושאים שדנתי בהם לא מעט הוא בדיקות יחידה בריאקט. אחד הדברים שהכי מתסכלים אותי במאמרים האלו, הוא זה שאני נאלץ לפשט את הדוגמאות ומדובר ביותר מדי דוגמאות של hello world, ואז ניגשים לכתיבת טסטים וכל הדוגמאות הנחמדות והפשוטות שאני מביא מתנפצות על קרקע המציאות. כי במציאות, הקומפוננטות מורכבות יותר וכתיבת בדיקות אוטומטיות לקומפוננטות יותר מורכבות זה לא קל – לפחות בהתחלה. אבל שווה להשקיע את המאמץ ההתחלתי ולהבין בבדיקות כי אם יש משהו שמבטיח איכות, מהירות פיתוח ואפס תקלות אלו בדיקות אוטומטיות. כן, כן.


אז דיברנו קודם על בדיקות של קומפוננטת hello world וגם של קומפוננטה שמקבלת props. אבל במציאות קומפוננטות עצמאיות יקבלו מידע מסרביסים. כלומר קוד שקורא החוצה לשרת. בואו ואציג לכם קומפוננטה כזו. קומפוננטת בדיחת אבא. כן כן.


קודם כל, היכנסו לקישור הבא כדי לראות איך זה עובד. תראו בעצם דף שבו יש בדיחת אבא שמשתמשת ב-API. שימו לב שהשארתי את ה-sourcemaps פתוחים, אז בהחלט אתם מוזמנים להיכנס לכלי המפתחים ולדבג שם. באפליקציות אמיתיות אגב, זה נחשב כחולשת אבטחה.


אז איך זה עובד? יש לנו service פשוט שמשתמש ב-fetch API כדי לקבל את בדיחת האבא. ה-service החביב הזה נמצא ב-src/services/DadJokeService.service.js ונראה ככה:

class DadJokeService {
 getDadJoke() {
   return fetch(`https://icanhazdadjoke.com`, {
     headers: {
       'Accept': 'application/json'
       },
     })
     .then(res => res.json())
     .then(json => json.joke );
 }
}
const instance = new DadJokeService();
export default instance;

אין פה משהו שמתכנת בסיסי לא מבין. זה ג׳אווהסקריפט טהור, לא קשור לריאקט או לפריימוורק. סתם service שכתוב בונילה - הכי פשוט שיש.


עכשיו, הקומפוננטה שצורכת אותו. איך היא נראית? גם פשוטה למדי. היא נמצאת בנתיב הזה: src/components/DadJoke/DadJoke.jsx ובנויה ככה:


import React from 'react';
import DadJokeService from '../../services/DadJokeService.service';
class DadJoke extends React.Component {
 constructor() {
   super();
   this.state = { joke: 'Loading joke...' };
 }
 componentDidMount() {
   DadJokeService.getDadJoke().then(result => this.setState({ joke: result }));
 }
 
 render() {
     return <p>{this.state.joke}</p>
 }
}
export default DadJoke;

אז מה לא פשוט כאן?


הכי פשוט בעולם. בהתחלה הקומפוננטה מריצה Loading ומיד אחרי שהיא מקבלת תשובה מה-API היא מציגה את מה שיש שם. הכי פשוט שיש.


מה לא פשוט? לבדוק את זה. עד עכשיו כל המאמרים וגם כל הדוגמאות היו על קומפוננטות מאוד סטטיות. זו קומפוננטה דינמית. איך אני בודק אותה?


אולי כדאי לדבר קודם על איך לא לבדוק אותה. לא מרנדרים אותה איך שהיא. למה? כי אם נעשה את זה, נצטרך לחכות בכל בדיקה ובדיקה לתוצאות ה-API. וזה רעיון רע מאוד בבדיקות יחידה שאמורות להתרכז בפונקציונליות הבסיסית של הקומפוננטה. אנחנו לא עושים כאן בדיקות End to End או אינטגרציה. אנחנו עושים בדיקה אך ורק לקוד של הקומפוננטה. אני רוצה לבדוק שאכן הקומפוננטה קוראת ל-service. אני רוצה לבדוק שהקומפוננטה מציגה את המידע שה-service מחזיר. הדבר האחרון שאני רוצה לבדוק זה את ה-service. יש לו את הבדיקות שלו. אני בטח ובטח לא רוצה לבדוק את ה-API בבדיקת היחידה הזו. בדיקות יחידה הן… ובכן, אך ורק ליחידת הקוד הספציפית הזו.


אז איך עושים את זה? באמצעות mock. שזה ׳לדמות׳ את ה-service ואת התגובה שלו. יצירת mock היא ממש ממש פשוטה ב-jest. עושים import למודול שלנו, במקרה הזה ה-service ואז משתמשים ב-jest.fn();


נשמע מסובך? בואו ונדגים:


import DadJokeService from '../../services/DadJokeService.service';
   DadJokeService.getDadJoke = jest.fn();
   DadJokeService.getDadJoke.mockReturnValue('This is mock dadjoke');

וזה גם יהיה בכל הדוגמאות שיש באתרים השונים ובדוקומנטציה של jest. שזה נחמד אבל במציאות זה לא עובד ככה. למה? כי בגלל שמדובר ב-service וב-fetch, אני צריך לעשות קריאה אסינכרונית ולא סינכרונית. אז איך עושים mock ומדמים תגובה אסינכרונית? זה קל.


   ה-mock צריך להחזיר promise.


   קיום ההבטחה צריך להיות מייד.


אופן הביצוע


איך עושים את שניהם? עם הקוד הבא:


import React from 'react';
import { shallow } from 'enzyme';
import DadJoke from './DadJoke';
import DadJokeService from '../../services/DadJokeService.service';
let element;
describe('DadJoke Component is working normally', () => {
 beforeEach(() => {
   // mock return promise
   const dadJokeResult = Promise.resolve('This is mock dadjoke');
   DadJokeService.getDadJoke = jest.fn();
   DadJokeService.getDadJoke.mockReturnValue(dadJokeResult);
 });
 it('renders without crashing', async () => {
   element = shallow(<DadJoke />);
   expect(element.text()).toContain('Loading');
 });
 it('implement service', async () => {
   element = shallow(<DadJoke />);
   await flushAllPromises();
   expect(element.text()).toContain('This is mock dadjoke');
 });
 // Resolving all
 const flushAllPromises = () => new Promise((resolve) => setImmediate(resolve()));
});

ואפשר לראות כמה זה פשוט. פשוט יוצרים Promise וגורמים ל-mock להחזיר אותו ובסוף בסוף, כשאני רוצה שכל ה-Promises יופעלו, אני יוצר פונקציית עזר קטנה בשם flushAllPromises, שמשתמשת ב-node כדי לעשות flush. שימו לב שהבדיקה חייבת להיות אסינכרונית, ואת זה אני עושה באמצעות שימוש ב-async ב-it.


אני יודע שזה נראה מסובך, אבל ככה זה עובד בעולם האמיתי כשיוצאים מעולם ה-hello world. יוצא לנו לעבוד עם promises, ויוצא לנו לעבוד לפעמים עם שירותים ודברים אחרים שצריך לעשות להם mocking.


אבל האמת היא, שכשהתייעצתי עם ערן שפירא, ראש הצוות שלי שנתן את דוגמאות הקוד במאמר, הוא טען ובצדק שבעולם האמיתי לא יהיו לנו את הדילמה הזו כי אנחנו משתמשים ברידקס, וזה נכון. במאמר הבא אני אבצע הסבר קצר על דיבוג בדיקות, ואז נמשיך הלאה לרידקס ולפיצ׳רים נוספים שמזכירים אותו.

מאת: רן בר זיק, מתכנת מומחה ועיתונאי טכנולוגי

רוצים להתעדכן בתכנים נוספים בנושאי ענן וטכנולוגיות מתקדמות? הירשמו עכשיו לניוזלטר שלנו ותמיד תישארו בעניינים > להרשמה

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.
בואו נעבוד ביחד
support@israelclouds.com
צרו קשר