برنامه نویسی

کتابخانه تست مؤلفه خود را بسازید: راهنمای توسعه دهنده

من به عنوان یک نویسنده پرفروش ، شما را دعوت می کنم تا کتابهای من را در آمازون کشف کنید. فراموش نکنید که مرا در متوسط ​​دنبال کنید و پشتیبانی خود را نشان دهید. ممنون حمایت شما به معنای جهان است!

JavaScript در توسعه وب متحول شده است و معماری های مبتنی بر مؤلفه به استاندارد تبدیل شده اند. آزمایش این مؤلفه ها به طور کامل به ابزارهای تخصصی نیاز دارد. من سالها در ساخت کتابخانه های آزمایش صرف کرده ام و می خواهم تکنیک هایی را برای ایجاد راه حل تست اجزای خود به اشتراک بگذارم.

ارائه DOM

پایه و اساس هر کتابخانه تست مؤلفه ارائه DOM است. هنگامی که من برای اولین بار با این چالش مقابله کردم ، متوجه شدم که نیاز به هر دو روش آزمایش جدا شده و یکپارچه.

ایجاد یک رندر DOM سبک با ایجاد یک محیط منزوی شروع می شود. اجرای DOM مجازی عملکرد بهتری نسبت به دستکاری DOM واقعی ارائه می دهد.

function createRenderer() {
  const virtualDOM = document.createElement('div');

  return {
    render(component) {
      ReactDOM.render(component, virtualDOM);
      return virtualDOM;
    },

    shallowRender(component) {
      // Prevent children from rendering deeply
      const shallowContext = createShallowContext();
      ReactDOM.render(
        <ShallowRenderer context={shallowContext}>
          {component}
        </ShallowRenderer>,
        virtualDOM
      );
      return virtualDOM;
    },

    cleanup() {
      ReactDOM.unmountComponentAtNode(virtualDOM);
    }
  };
}

// Usage
const renderer = createRenderer();
const element = renderer.render(<Button>Click Me</Button>);
expect(element.textContent).toBe('Click Me');
renderer.cleanup();
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

برای ارائه کم عمق ، فهمیدم که رهگیری روند آشتی React بهترین کار را انجام می دهد. این امر به ما امکان می دهد مؤلفه های کودک را با صاحبان مکان جایگزین کنیم و آزمایشات خود را بر روی رفتار خاص این مؤلفه بدون آزمایش فرزندان خود متمرکز کنیم.

function ShallowRenderer({ children, context }) {
  return React.cloneElement(children, {}, null);
}

function createShallowContext() {
  const componentStubs = new Map();

  return {
    registerComponent(Component, stub) {
      componentStubs.set(Component, stub);
    },

    getStub(Component) {
      return componentStubs.get(Component) || null;
    }
  };
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

شبیه سازی رویداد

شبیه سازی دقیق رویداد به دلیل تفاوت های مرورگر چالش برانگیز بود. من یاد گرفتم که اشیاء رویداد عادی ایجاد کنم که در محیط ها کار می کنند.

function createEventSimulator() {
  return {
    click(element, options = {}) {
      const clickEvent = new MouseEvent('click', {
        bubbles: true,
        cancelable: true,
        view: window,
        ...options
      });
      element.dispatchEvent(clickEvent);
    },

    change(element, value) {
      // Set the value
      element.value = value;

      // Trigger change event
      const event = new Event('change', {
        bubbles: true,
        cancelable: true
      });
      element.dispatchEvent(event);
    },

    keyPress(element, key, options = {}) {
      const keyEvent = new KeyboardEvent('keypress', {
        key,
        bubbles: true,
        cancelable: true,
        ...options
      });
      element.dispatchEvent(keyEvent);
    },

    drag(element, { from, to }) {
      // Simulate dragstart
      const dragStartEvent = new MouseEvent('dragstart', {
        bubbles: true,
        clientX: from.x,
        clientY: from.y
      });
      element.dispatchEvent(dragStartEvent);

      // Simulate dragover on target
      const dragOverEvent = new MouseEvent('dragover', {
        bubbles: true,
        clientX: to.x,
        clientY: to.y
      });
      element.dispatchEvent(dragOverEvent);

      // Simulate drop on target
      const dropEvent = new MouseEvent('drop', {
        bubbles: true,
        clientX: to.x,
        clientY: to.y
      });
      element.dispatchEvent(dropEvent);
    }
  };
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

رویدادهای لمسی برای شبیه سازی صحیح تعامل چند لمسی نیاز به هندلینگ ویژه دارند:

function simulateTouch(element, touchType, touchPoints) {
  const touchEvent = new TouchEvent(touchType, {
    bubbles: true,
    cancelable: true,
    touches: createTouchList(touchPoints),
    targetTouches: createTouchList(touchPoints),
    changedTouches: createTouchList(touchPoints)
  });

  element.dispatchEvent(touchEvent);
}

function createTouchList(points) {
  return points.map((point, id) => new Touch({
    identifier: id,
    target: element,
    clientX: point.x,
    clientY: point.y
  }));
}

// Usage example
simulateTouch(element, 'touchstart', [
  { x: 100, y: 100 },
  { x: 200, y: 150 }
]);
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

تأیید دولت

آزمایش مؤثر مؤثر نیاز به اعتبار سنجی خروجی DOM و وضعیت داخلی دارد. من ادعاهای تخصصی برای انواع مؤلفه های مختلف ایجاد کرده ام.

const componentAssertions = {
  hasClass(element, className) {
    return element.classList.contains(className);
  },

  hasAttribute(element, name, value) {
    if (value === undefined) {
      return element.hasAttribute(name);
    }
    return element.getAttribute(name) === value;
  },

  hasStyle(element, property, value) {
    const style = window.getComputedStyle(element);
    return style[property] === value;
  },

  isVisible(element) {
    const style = window.getComputedStyle(element);
    return style.display !== 'none' && 
           style.visibility !== 'hidden' && 
           style.opacity !== '0';
  },

  containsText(element, text) {
    return element.textContent.includes(text);
  },

  matchesAccessibility(element, criteria) {
    // Check ARIA attributes
    if (criteria.role && element.getAttribute('role') !== criteria.role) {
      return false;
    }

    // Check for required ARIA attributes based on role
    if (criteria.required) {
      for (const attr of criteria.required) {
        if (!element.hasAttribute(`aria-${attr}`)) {
          return false;
        }
      }
    }

    return true;
  }
};
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

برای اجزای فرم ، من ادعاهای تخصصی ایجاد کردم:

const formAssertions = {
  hasValue(input, expected) {
    return input.value === expected;
  },

  isChecked(checkbox) {
    return checkbox.checked === true;
  },

  isDisabled(element) {
    return element.disabled === true;
  },

  hasError(formElement, errorMessage) {
    // Find associated error element
    const id = formElement.id;
    const errorElement = document.querySelector(`[aria-describedby="${id}"]`);
    return errorElement && errorElement.textContent.includes(errorMessage);
  },

  isValid(formElement) {
    return formElement.validity.valid;
  }
};
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

آزمایش ناهمزمان

رسیدگی به رفتار مؤلفه Async همیشه یک چالش بوده است. من برنامه هایی ایجاد کرده ام که زمان بندی و انتقال دولت را مدیریت می کند.

async function waitFor(predicate, options = {}) {
  const { timeout = 1000, interval = 50 } = options;
  const startTime = Date.now();

  return new Promise((resolve, reject) => {
    const check = () => {
      try {
        const result = predicate();
        if (result) {
          resolve(result);
          return;
        }
      } catch (error) {
        // Predicate threw an error, keep waiting
      }

      const elapsed = Date.now() - startTime;
      if (elapsed >= timeout) {
        reject(new Error(`Timed out after ${timeout}ms waiting for predicate to be true`));
        return;
      }

      setTimeout(check, interval);
    };

    check();
  });
}

async function waitForElement(selector, container = document) {
  return waitFor(() => container.querySelector(selector));
}

async function waitForRender(component) {
  // Force a microtask to allow React to render
  await Promise.resolve();

  // For React 18+ with concurrent features
  if (typeof window.ReactDOM?.flushSync === 'function') {
    ReactDOM.flushSync();
  }

  return component;
}

async function waitForAnimation(element) {
  if (!element.getAnimations) {
    // Fallback for browsers without Animation API
    return waitFor(() => 
      !element.classList.contains('animating') && 
      !element.style.animation
    );
  }

  return waitFor(() => element.getAnimations().every(a => a.playState === 'finished'));
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

به روزرسانی های حالت React نیاز به تکنیک های تخصصی دارد:

function act(callback) {
  // Use React's test utilities if available
  if (typeof React.act === 'function') {
    return React.act(callback);
  }

  // Fallback implementation
  callback();
  // Flush microtasks
  return Promise.resolve();
}

async function waitForUpdate(component, propName, expectedValue) {
  return waitFor(() => {
    const currentProps = component.props;
    return currentProps[propName] === expectedValue;
  });
}

// Example usage
await act(async () => {
  fireEvent.click(button);
  await waitForUpdate(component, 'isLoading', false);
});
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

تولید عکس فوری

آزمایش عکس فوری به تشخیص تغییرات ناخواسته کمک می کند. من ابزارهایی ایجاد کرده ام که نمایش های سریال قابل استفاده از قطعات را ایجاد می کنند.

function createSnapshotter() {
  return {
    captureDOM(element) {
      return {
        tagName: element.tagName.toLowerCase(),
        attributes: getAttributes(element),
        children: Array.from(element.childNodes).map(node => {
          if (node.nodeType === Node.TEXT_NODE) {
            return { type: 'text', content: node.textContent };
          }
          return this.captureDOM(node);
        }),
        textContent: element.textContent
      };
    },

    captureComponent(component) {
      const renderer = createRenderer();
      const element = renderer.render(component);
      const snapshot = this.captureDOM(element);
      renderer.cleanup();
      return snapshot;
    },

    compare(snapshot1, snapshot2) {
      const differences = [];

      compareNodes(snapshot1, snapshot2, '', differences);

      return {
        hasDifferences: differences.length > 0,
        differences
      };
    }
  };
}

function getAttributes(element) {
  const attributes = {};
  Array.from(element.attributes).forEach(attr => {
    attributes[attr.name] = attr.value;
  });
  return attributes;
}

function compareNodes(node1, node2, path, differences) {
  if (!node1 || !node2) {
    differences.push({
      path,
      message: !node1 ? 'Node added' : 'Node removed'
    });
    return;
  }

  if (node1.type !== node2.type) {
    differences.push({
      path,
      message: `Node type changed from ${node1.type} to ${node2.type}`
    });
    return;
  }

  if (node1.type === 'text' && node1.content !== node2.content) {
    differences.push({
      path,
      message: `Text changed from "${node1.content}" to "${node2.content}"`
    });
    return;
  }

  if (node1.tagName !== node2.tagName) {
    differences.push({
      path,
      message: `Tag changed from ${node1.tagName} to ${node2.tagName}`
    });
  }

  // Compare attributes
  const allAttributes = new Set([
    ...Object.keys(node1.attributes || {}),
    ...Object.keys(node2.attributes || {})
  ]);

  for (const attr of allAttributes) {
    const val1 = node1.attributes?.[attr];
    const val2 = node2.attributes?.[attr];

    if (val1 !== val2) {
      differences.push({
        path: `${path}.attributes[${attr}]`,
        message: `Attribute "${attr}" changed from "${val1}" to "${val2}"`
      });
    }
  }

  // Compare children
  const children1 = node1.children || [];
  const children2 = node2.children || [];

  const maxLength = Math.max(children1.length, children2.length);
  for (let i = 0; i < maxLength; i++) {
    compareNodes(
      children1[i], 
      children2[i], 
      `${path}.children[${i}]`, 
      differences
    );
  }
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

زمینه مؤثر

مؤلفه ها به ندرت در انزوا وجود دارند. آنها برای مضامین ، ایالت ، مسیریابی و موارد دیگر به ارائه دهندگان زمینه بستگی دارند. من رویکردهایی را برای مسخره کردن این زمینه ها ایجاد کرده ام.

function createContextMock(Context, defaultValue) {
  return {
    withValue(value, children) {
      return (
        <Context.Provider value={value}>
          {children}
        </Context.Provider>
      );
    },

    peek() {
      // Get current context value
      let currentValue = defaultValue;
      const Probe = () => {
        currentValue = React.useContext(Context);
        return null;
      };

      const renderer = createRenderer();
      renderer.render(<Probe />);
      renderer.cleanup();

      return currentValue;
    }
  };
}

// Mock theme context
const ThemeContext = React.createContext({ mode: 'light' });
const themeMock = createContextMock(ThemeContext, { mode: 'light' });

// Test with dark theme
const { container } = render(
  themeMock.withValue({ mode: 'dark' }, <Button>Submit</Button>)
);

expect(container.querySelector('.button')).toHaveClass('dark-theme');
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

برای زمینه پیچیده تر مانند فروشگاه ها یا روترها:

function createStoreMock(initialState = {}) {
  let state = { ...initialState };
  const listeners = new Set();

  return {
    getState() {
      return { ...state };
    },

    setState(newState) {
      state = { ...state, ...newState };
      listeners.forEach(listener => listener(state));
    },

    subscribe(listener) {
      listeners.add(listener);
      return () => listeners.delete(listener);
    },

    withStore(component) {
      return (
        <StoreContext.Provider value={this}>
          {component}
        </StoreContext.Provider>
      );
    }
  };
}

function createRouterMock(initialRoute = '/') {
  let currentRoute = initialRoute;
  const listeners = new Set();

  return {
    getCurrentRoute() {
      return currentRoute;
    },

    navigate(route) {
      currentRoute = route;
      listeners.forEach(listener => listener(currentRoute));
    },

    subscribe(listener) {
      listeners.add(listener);
      return () => listeners.delete(listener);
    },

    withRouter(component) {
      return (
        <RouterContext.Provider 
          value={{ 
            route: currentRoute, 
            navigate: this.navigate.bind(this) 
          }}
        >
          {component}
        </RouterContext.Provider>
      );
    }
  };
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

با هم قرار دادن همه اینها ، من یک ابزار آزمایش جامع ایجاد کرده ام:

function createTestBed() {
  const container = document.createElement('div');
  document.body.appendChild(container);

  const events = createEventSimulator();
  const assertions = { ...componentAssertions, ...formAssertions };
  const snapshotter = createSnapshotter();

  return {
    render(component) {
      ReactDOM.render(component, container);

      return {
        container,

        // Selection methods
        getByTestId: (id) => container.querySelector(`[data-testid="${id}"]`),
        getByText: (text) => [...container.querySelectorAll('*')]
          .find(el => el.textContent.trim() === text),
        queryBySelector: (selector) => container.querySelector(selector),

        // Event methods
        fireEvent: events,

        // Assertion methods
        assert: assertions,

        // Async methods
        waitFor,
        waitForElement: (selector) => waitForElement(selector, container),

        // Snapshot methods
        snapshot: () => snapshotter.captureDOM(container),

        // Cleanup
        unmount: () => {
          ReactDOM.unmountComponentAtNode(container);
        }
      };
    },

    cleanup() {
      document.body.removeChild(container);
    },

    // Context mocking
    createContextMock,
    createStoreMock,
    createRouterMock
  };
}

// Example usage
const testBed = createTestBed();

const { getByTestId, fireEvent, assert, waitFor, unmount } = testBed.render(
  <Counter initialCount={0} />
);

const countDisplay = getByTestId('count-display');
const incrementButton = getByTestId('increment-button');

expect(assert.containsText(countDisplay, '0')).toBe(true);

fireEvent.click(incrementButton);

waitFor(() => assert.containsText(countDisplay, '1'))
  .then(() => {
    console.log('Test passed!');
    unmount();
    testBed.cleanup();
  });
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

ساختن یک کتابخانه آزمایش مؤلفه درک من از رفتار جاوا اسکریپت و مرورگر را عمیق تر کرده است. تکنیک هایی که من به اشتراک گذاشته ام ، آزمایش جامع مؤلفه را فراهم می کند ، از ارائه DOM ساده گرفته تا رفتار پیچیده ناهمزمان و تمسخر متن.

این الگوهای امروز پایه و اساس بسیاری از کتابخانه های تست محبوب امروز را تشکیل می دهند. با درک نحوه کار آنها در زیر کاپوت ، شما برای سفارشی کردن راه حل های آزمایش برای نیازهای خاص خود ، انعطاف پذیری کسب می کنید.

به یاد داشته باشید که آزمایش مربوط به اعتماد به نفس است ، نه پوشش. بهترین کتابخانه تست کتابی است که به شما در نوشتن تست های معنی دار با حداقل اصطکاک کمک می کند. تکنیک هایی که من به اشتراک گذاشته ام به شما امکان می دهد چنین کتابخانه ای را متناسب با فلسفه تست خاص و معماری مؤلفه خود بسازید.


101 کتاب

101 کتاب یک شرکت انتشارات AI محور است که توسط نویسنده تأسیس شده است آراو جوشیبشر با استفاده از فناوری پیشرفته هوش مصنوعی ، ما هزینه های انتشار خود را فوق العاده کم نگه می داریم – برخی از کتاب ها به اندازه کم قیمت هستند 4 دلار– ایجاد دانش با کیفیت در دسترس همه.

کتاب ما را بررسی کنید کد تمیز Golang در آمازون موجود است.

برای به روزرسانی ها و اخبار هیجان انگیز با ما در ارتباط باشید. هنگام خرید کتاب ، جستجو کنید آراو جوشی برای یافتن بیشتر عناوین ما. برای لذت بردن از لینک ارائه شده استفاده کنید تخفیف های خاص!

خلاقیت های ما

حتما خلاقیت های ما را بررسی کنید:

سرمایه گذار مرکزی | سرمایه گذار اسپانیایی مرکزی | سرمایه گذار آلمانی مرکزی | زندگی هوشمند | دوره ها و پژواک | اسرار گیج کننده | هندوتوا | نخبه | مدارس JS


ما در متوسط ​​هستیم

بینش های فنی Koala | Epochs & Echoes World | سرمایه گذار رسانه مرکزی | رمز و رازهای گیج کننده متوسط | علوم و دوره های متوسط | هندوتوا مدرن

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا