Ελληνικά

Κατακτήστε τη React Testing Library (RTL) με αυτόν τον πλήρη οδηγό. Μάθετε πώς να γράφετε αποτελεσματικά, συντηρήσιμα και ανθρωποκεντρικά tests για τις εφαρμογές σας React, εστιάζοντας σε βέλτιστες πρακτικές και παραδείγματα από τον πραγματικό κόσμο.

React Testing Library: Ένας Ολοκληρωμένος Οδηγός

Στο σημερινό, ταχέως εξελισσόμενο τοπίο της ανάπτυξης web, η διασφάλιση της ποιότητας και της αξιοπιστίας των εφαρμογών σας React είναι υψίστης σημασίας. Η React Testing Library (RTL) έχει αναδειχθεί ως μια δημοφιλής και αποτελεσματική λύση για τη συγγραφή tests που εστιάζουν στην προοπτική του χρήστη. Αυτός ο οδηγός παρέχει μια πλήρη επισκόπηση της RTL, καλύπτοντας τα πάντα, από τις θεμελιώδεις έννοιες έως τις προηγμένες τεχνικές, δίνοντάς σας τη δυνατότητα να δημιουργείτε στιβαρές και συντηρήσιμες εφαρμογές React.

Γιατί να επιλέξετε τη React Testing Library;

Οι παραδοσιακές προσεγγίσεις testing συχνά βασίζονται σε λεπτομέρειες υλοποίησης, καθιστώντας τα tests εύθραυστα και επιρρεπή στο να αποτυγχάνουν με μικρές αλλαγές στον κώδικα. Η RTL, από την άλλη πλευρά, σας ενθαρρύνει να ελέγχετε τα components σας όπως θα αλληλεπιδρούσε ένας χρήστης με αυτά, εστιάζοντας σε αυτό που ο χρήστης βλέπει και βιώνει. Αυτή η προσέγγιση προσφέρει πολλά βασικά πλεονεκτήματα:

Ρύθμιση του Περιβάλλοντος Testing

Προτού μπορέσετε να αρχίσετε να χρησιμοποιείτε την RTL, πρέπει να ρυθμίσετε το περιβάλλον testing σας. Αυτό συνήθως περιλαμβάνει την εγκατάσταση των απαραίτητων εξαρτήσεων και τη διαμόρφωση του testing framework σας.

Προαπαιτούμενα

Εγκατάσταση

Εγκαταστήστε τα ακόλουθα πακέτα χρησιμοποιώντας npm ή yarn:

npm install --save-dev @testing-library/react @testing-library/jest-dom jest babel-jest @babel/preset-env @babel/preset-react

Ή, χρησιμοποιώντας yarn:

yarn add --dev @testing-library/react @testing-library/jest-dom jest babel-jest @babel/preset-env @babel/preset-react

Επεξήγηση των Πακέτων:

Διαμόρφωση

Δημιουργήστε ένα αρχείο `babel.config.js` στη ρίζα του project σας με το ακόλουθο περιεχόμενο:

module.exports = {
  presets: ['@babel/preset-env', '@babel/preset-react'],
};

Ενημερώστε το αρχείο `package.json` σας για να συμπεριλάβετε ένα test script:

{
  "scripts": {
    "test": "jest"
  }
}

Δημιουργήστε ένα αρχείο `jest.config.js` στη ρίζα του project σας για να διαμορφώσετε το Jest. Μια ελάχιστη διαμόρφωση θα μπορούσε να μοιάζει κάπως έτσι:

module.exports = {
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['/src/setupTests.js'],
};

Δημιουργήστε ένα αρχείο `src/setupTests.js` με το ακόλουθο περιεχόμενο. Αυτό διασφαλίζει ότι οι Jest DOM matchers είναι διαθέσιμοι σε όλα τα tests σας:

import '@testing-library/jest-dom/extend-expect';

Γράφοντας το Πρώτο σας Test

Ας ξεκινήσουμε με ένα απλό παράδειγμα. Υποθέστε ότι έχετε ένα React component που εμφανίζει ένα μήνυμα χαιρετισμού:

// src/components/Greeting.js
import React from 'react';

function Greeting({ name }) {
  return <h1>Hello, {name}!</h1>;
}

export default Greeting;

Τώρα, ας γράψουμε ένα test για αυτό το component:

// src/components/Greeting.test.js
import React from 'react';
import { render, screen } from '@testing-library/react';
import Greeting from './Greeting';

test('εμφανίζει ένα μήνυμα χαιρετισμού', () => {
  render(<Greeting name="World" />);
  const greetingElement = screen.getByText(/Hello, World!/i);
  expect(greetingElement).toBeInTheDocument();
});

Επεξήγηση:

Για να εκτελέσετε το test, πληκτρολογήστε την ακόλουθη εντολή στο τερματικό σας:

npm test

Αν όλα έχουν ρυθμιστεί σωστά, το test θα πρέπει να περάσει.

Συνήθεις Αναζητήσεις (Queries) της RTL

Η RTL παρέχει διάφορες μεθόδους αναζήτησης (query methods) για την εύρεση στοιχείων στο DOM. Αυτές οι αναζητήσεις είναι σχεδιασμένες για να μιμούνται τον τρόπο με τον οποίο οι χρήστες αλληλεπιδρούν με την εφαρμογή σας.

`getByRole`

Αυτή η αναζήτηση βρίσκει ένα στοιχείο από τον ARIA ρόλο του. Είναι καλή πρακτική να χρησιμοποιείτε το `getByRole` όποτε είναι δυνατόν, καθώς προωθεί την προσβασιμότητα και διασφαλίζει ότι τα tests σας είναι ανθεκτικά σε αλλαγές στην υποκείμενη δομή του DOM.

<button role="button">Click me</button>
const buttonElement = screen.getByRole('button');
expect(buttonElement).toBeInTheDocument();

`getByLabelText`

Αυτή η αναζήτηση βρίσκει ένα στοιχείο από το κείμενο της σχετικής ετικέτας του. Είναι χρήσιμο για τον έλεγχο στοιχείων φόρμας.

<label htmlFor="name">Name:</label>
<input type="text" id="name" />
const nameInputElement = screen.getByLabelText('Name:');
expect(nameInputElement).toBeInTheDocument();

`getByPlaceholderText`

Αυτή η αναζήτηση βρίσκει ένα στοιχείο από το κείμενο του placeholder του.

<input type="text" placeholder="Enter your email" />
const emailInputElement = screen.getByPlaceholderText('Enter your email');
expect(emailInputElement).toBeInTheDocument();

`getByAltText`

Αυτή η αναζήτηση βρίσκει ένα στοιχείο εικόνας από το alt text του. Είναι σημαντικό να παρέχετε ουσιαστικό alt text για όλες τις εικόνες για να διασφαλίσετε την προσβασιμότητα.

<img src="logo.png" alt="Company Logo" />
const logoImageElement = screen.getByAltText('Company Logo');
expect(logoImageElement).toBeInTheDocument();

`getByTitle`

Αυτή η αναζήτηση βρίσκει ένα στοιχείο από το title attribute του.

<span title="Close">X</span>
const closeElement = screen.getByTitle('Close');
expect(closeElement).toBeInTheDocument();

`getByDisplayValue`

Αυτή η αναζήτηση βρίσκει ένα στοιχείο από την τιμή που εμφανίζει. Αυτό είναι χρήσιμο για τον έλεγχο πεδίων εισαγωγής φόρμας με προ-συμπληρωμένες τιμές.

<input type="text" value="Initial Value" />
const inputElement = screen.getByDisplayValue('Initial Value');
expect(inputElement).toBeInTheDocument();

`getAllBy*` Queries

Εκτός από τις `getBy*` queries, η RTL παρέχει επίσης τις `getAllBy*` queries, οι οποίες επιστρέφουν έναν πίνακα με τα αντίστοιχα στοιχεία. Αυτές είναι χρήσιμες όταν θέλετε να επιβεβαιώσετε ότι πολλά στοιχεία με τα ίδια χαρακτηριστικά υπάρχουν στο DOM.

<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
const listItems = screen.getAllByRole('listitem');
expect(listItems).toHaveLength(3);

`queryBy*` Queries

Οι `queryBy*` queries είναι παρόμοιες με τις `getBy*` queries, αλλά επιστρέφουν `null` αν δεν βρεθεί κανένα αντίστοιχο στοιχείο, αντί να προκαλέσουν σφάλμα. Αυτό είναι χρήσιμο όταν θέλετε να επιβεβαιώσετε ότι ένα στοιχείο *δεν* υπάρχει στο DOM.

const missingElement = screen.queryByText('Non-existent text');
expect(missingElement).toBeNull();

`findBy*` Queries

Οι `findBy*` queries είναι ασύγχρονες εκδόσεις των `getBy*` queries. Επιστρέφουν μια Promise που επιλύεται όταν βρεθεί το αντίστοιχο στοιχείο. Αυτές είναι χρήσιμες για τον έλεγχο ασύγχρονων λειτουργιών, όπως η λήψη δεδομένων από ένα API.

// Προσομοίωση μιας ασύγχρονης λήψης δεδομένων
const fetchData = () => new Promise(resolve => {
  setTimeout(() => resolve('Data Loaded!'), 1000);
});

function MyComponent() {
  const [data, setData] = React.useState(null);

  React.useEffect(() => {
    fetchData().then(setData);
  }, []);

  return <div>{data}</div>;
}
test('φορτώνει δεδομένα ασύγχρονα', async () => {
  render(<MyComponent />);
  const dataElement = await screen.findByText('Data Loaded!');
  expect(dataElement).toBeInTheDocument();
});

Προσομοίωση Αλληλεπιδράσεων Χρήστη

Η RTL παρέχει τα API `fireEvent` και `userEvent` για την προσομοίωση αλληλεπιδράσεων του χρήστη, όπως το κλικ σε κουμπιά, η πληκτρολόγηση σε πεδία εισαγωγής και η υποβολή φορμών.

`fireEvent`

Το `fireEvent` σας επιτρέπει να ενεργοποιείτε προγραμματιστικά DOM events. Είναι ένα API χαμηλότερου επιπέδου που σας δίνει λεπτομερή έλεγχο πάνω στα events που ενεργοποιούνται.

<button onClick={() => alert('Button clicked!')}>Click me</button>
import { fireEvent } from '@testing-library/react';

test('προσομοιώνει ένα κλικ σε κουμπί', () => {
  const alertMock = jest.spyOn(window, 'alert').mockImplementation(() => {});
  render(<button onClick={() => alert('Button clicked!')}>Click me</button>);
  const buttonElement = screen.getByRole('button');
  fireEvent.click(buttonElement);
  expect(alertMock).toHaveBeenCalledTimes(1);
  alertMock.mockRestore();
});

`userEvent`

Το `userEvent` είναι ένα API υψηλότερου επιπέδου που προσομοιώνει τις αλληλεπιδράσεις του χρήστη πιο ρεαλιστικά. Διαχειρίζεται λεπτομέρειες όπως η διαχείριση της εστίασης (focus) και η σειρά των events, καθιστώντας τα tests σας πιο στιβαρά και λιγότερο εύθραυστα.

<input type="text" onChange={e => console.log(e.target.value)} />
import userEvent from '@testing-library/user-event';

test('προσομοιώνει την πληκτρολόγηση σε ένα πεδίο εισαγωγής', () => {
  const inputElement = screen.getByRole('textbox');
  userEvent.type(inputElement, 'Hello, world!');
  expect(inputElement).toHaveValue('Hello, world!');
});

Έλεγχος Ασύγχρονου Κώδικα

Πολλές εφαρμογές React περιλαμβάνουν ασύγχρονες λειτουργίες, όπως η λήψη δεδομένων από ένα API. Η RTL παρέχει διάφορα εργαλεία για τον έλεγχο του ασύγχρονου κώδικα.

`waitFor`

Το `waitFor` σας επιτρέπει να περιμένετε μέχρι μια συνθήκη να γίνει αληθής προτού κάνετε ένα assertion. Είναι χρήσιμο για τον έλεγχο ασύγχρονων λειτουργιών που χρειάζονται κάποιο χρόνο για να ολοκληρωθούν.

function MyComponent() {
  const [data, setData] = React.useState(null);

  React.useEffect(() => {
    setTimeout(() => {
      setData('Data loaded!');
    }, 1000);
  }, []);

  return <div>{data}</div>;
}
import { waitFor } from '@testing-library/react';

test('περιμένει για τη φόρτωση των δεδομένων', async () => {
  render(<MyComponent />);
  await waitFor(() => screen.getByText('Data loaded!'));
  const dataElement = screen.getByText('Data loaded!');
  expect(dataElement).toBeInTheDocument();
});

`findBy*` Queries

Όπως αναφέρθηκε προηγουμένως, οι `findBy*` queries είναι ασύγχρονες και επιστρέφουν μια Promise που επιλύεται όταν βρεθεί το αντίστοιχο στοιχείο. Αυτές είναι χρήσιμες για τον έλεγχο ασύγχρονων λειτουργιών που οδηγούν σε αλλαγές στο DOM.

Έλεγχος των Hooks

Τα React Hooks είναι επαναχρησιμοποιήσιμες συναρτήσεις που ενσωματώνουν λογική με κατάσταση (stateful logic). Η RTL παρέχει το βοηθητικό πρόγραμμα `renderHook` από το `@testing-library/react-hooks` (το οποίο έχει καταργηθεί υπέρ του `@testing-library/react` απευθείας από την v17) για τον έλεγχο προσαρμοσμένων Hooks μεμονωμένα.

// src/hooks/useCounter.js
import { useState } from 'react';

function useCounter(initialValue = 0) {
  const [count, setCount] = useState(initialValue);

  const increment = () => {
    setCount(prevCount => prevCount + 1);
  };

  const decrement = () => {
    setCount(prevCount => prevCount - 1);
  };

  return { count, increment, decrement };
}

export default useCounter;
// src/hooks/useCounter.test.js
import { renderHook, act } from '@testing-library/react';
import useCounter from './useCounter';

test('αυξάνει τον μετρητή', () => {
  const { result } = renderHook(() => useCounter());

  act(() => {
    result.current.increment();
  });

  expect(result.current.count).toBe(1);
});

Επεξήγηση:

Προηγμένες Τεχνικές Testing

Αφού κατακτήσετε τα βασικά της RTL, μπορείτε να εξερευνήσετε πιο προηγμένες τεχνικές testing για να βελτιώσετε την ποιότητα και τη συντηρησιμότητα των tests σας.

Mocking Modules

Μερικές φορές, μπορεί να χρειαστεί να κάνετε mock εξωτερικά modules ή εξαρτήσεις για να απομονώσετε τα components σας και να ελέγξετε τη συμπεριφορά τους κατά τη διάρκεια του testing. Το Jest παρέχει ένα ισχυρό API για mocking για αυτόν τον σκοπό.

// src/api/dataService.js
export const fetchData = async () => {
  const response = await fetch('/api/data');
  const data = await response.json();
  return data;
};
// src/components/MyComponent.js
import React, { useState, useEffect } from 'react';
import { fetchData } from '../api/dataService';

function MyComponent() {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetchData().then(setData);
  }, []);

  return <div>{data}</div>;
}
// src/components/MyComponent.test.js
import { render, screen, waitFor } from '@testing-library/react';
import MyComponent from './MyComponent';
import * as dataService from '../api/dataService';

jest.mock('../api/dataService');

test('ανακτά δεδομένα από το API', async () => {
  dataService.fetchData.mockResolvedValue({ message: 'Mocked data!' });

  render(<MyComponent />);

  await waitFor(() => screen.getByText('Mocked data!'));

  expect(screen.getByText('Mocked data!')).toBeInTheDocument();
  expect(dataService.fetchData).toHaveBeenCalledTimes(1);
});

Επεξήγηση:

Context Providers

Αν το component σας βασίζεται σε έναν Context Provider, θα χρειαστεί να περιβάλλετε το component σας με τον provider κατά τη διάρκεια του testing. Αυτό διασφαλίζει ότι το component έχει πρόσβαση στις τιμές του context.

// src/contexts/ThemeContext.js
import React, { createContext, useState } from 'react';

export const ThemeContext = createContext();

export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

  const toggleTheme = () => {
    setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}
// src/components/MyComponent.js
import React, { useContext } from 'react';
import { ThemeContext } from '../contexts/ThemeContext';

function MyComponent() {
  const { theme, toggleTheme } = useContext(ThemeContext);

  return (
    <div style={{ backgroundColor: theme === 'light' ? '#fff' : '#000', color: theme === 'light' ? '#000' : '#fff' }}>
      <p>Current theme: {theme}</p>
      <button onClick={toggleTheme}>Toggle Theme</button>
    </div>
  );
}
// src/components/MyComponent.test.js
import { render, screen, fireEvent } from '@testing-library/react';
import MyComponent from './MyComponent';
import { ThemeProvider } from '../contexts/ThemeContext';

test('αλλάζει το θέμα', () => {
  render(
    <ThemeProvider>
      <MyComponent />
    </ThemeProvider>
  );

  const themeParagraph = screen.getByText(/Current theme: light/i);
  const toggleButton = screen.getByRole('button', { name: /Toggle Theme/i });

  expect(themeParagraph).toBeInTheDocument();

  fireEvent.click(toggleButton);

  expect(screen.getByText(/Current theme: dark/i)).toBeInTheDocument();
});

Επεξήγηση:

Testing με Router

Όταν ελέγχετε components που χρησιμοποιούν React Router, θα χρειαστεί να παρέχετε ένα mock Router context. Μπορείτε να το πετύχετε αυτό χρησιμοποιώντας το component `MemoryRouter` από το `react-router-dom`.

// src/components/MyComponent.js
import React from 'react';
import { Link } from 'react-router-dom';

function MyComponent() {
  return (
    <div>
      <Link to="/about">Go to About Page</Link>
    </div>
  );
}
// src/components/MyComponent.test.js
import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import MyComponent from './MyComponent';

test('αποδίδει έναν σύνδεσμο προς τη σελίδα about', () => {
  render(
    <MemoryRouter>
      <MyComponent />
    </MemoryRouter>
  );

  const linkElement = screen.getByRole('link', { name: /Go to About Page/i });
  expect(linkElement).toBeInTheDocument();
  expect(linkElement).toHaveAttribute('href', '/about');
});

Επεξήγηση:

Βέλτιστες Πρακτικές για τη Συγγραφή Αποτελεσματικών Tests

Ακολουθούν ορισμένες βέλτιστες πρακτικές που πρέπει να ακολουθείτε κατά τη συγγραφή tests με την RTL:

Συμπέρασμα

Η React Testing Library είναι ένα ισχυρό εργαλείο για τη συγγραφή αποτελεσματικών, συντηρήσιμων και ανθρωποκεντρικών tests για τις εφαρμογές σας React. Ακολουθώντας τις αρχές και τις τεχνικές που περιγράφονται σε αυτόν τον οδηγό, μπορείτε να δημιουργήσετε στιβαρές και αξιόπιστες εφαρμογές που ανταποκρίνονται στις ανάγκες των χρηστών σας. Θυμηθείτε να εστιάζετε στον έλεγχο από την προοπτική του χρήστη, να αποφεύγετε τον έλεγχο λεπτομερειών υλοποίησης και να γράφετε σαφή και συνοπτικά tests. Υιοθετώντας την RTL και τις βέλτιστες πρακτικές, μπορείτε να βελτιώσετε σημαντικά την ποιότητα και τη συντηρησιμότητα των React projects σας, ανεξάρτητα από την τοποθεσία σας ή τις συγκεκριμένες απαιτήσεις του παγκόσμιου κοινού σας.