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

איך עובדים עם mocks ועם כאלו שמחזירים תוצאה אסינכרונית

במאמרים הקודמים דיברנו על בדיקות יחידה בריאקט. אחד הדברים שהכי מתסכלים אותי במאמרים האלו הן שאני נאלץ לפשט את הדוגמאות ומדובר ביותר מדי דוגמאות של 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 ומדמים תגובה אסינכרונית? זה קל.

  1. ה-mock צריך להחזיר promise.
  2. קיום ההבטחה צריך להיות מייד.

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

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.

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

⚠️אם אהבת את המדריכים על ריאקט – יש ספר מקיף ושלם על ריאקט שכתבתי בשם ללמוד ריאקט בעברית, במסגרת פרויקט עם חברות מובילות ומפתחים אחרים. בספר יש פירוט מקיף יותר על ריאקט ותרגילים רבים ללימוד עצמי. 

פוסטים נוספים שכדאי לקרוא

תמונה מצוירת של רובוט שמנקה HTML
יסודות בתכנות

סניטציה – למה זה חשוב

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

גלילה לראש העמוד