Pengujian komponen dalam praktik

Pengujian komponen adalah tempat yang baik untuk mulai mendemonstrasikan kode pengujian praktis. Pengujian komponen lebih substansial dibandingkan pengujian unit sederhana, tidak terlalu rumit dibandingkan pengujian menyeluruh, dan menunjukkan interaksi dengan DOM. Secara lebih filosofis, penggunaan React memudahkan developer web untuk menganggap situs atau aplikasi web terdiri dari komponen.

Jadi, menguji komponen individual, terlepas dari seberapa rumitnya, adalah cara yang baik untuk mulai berpikir untuk menguji aplikasi baru atau yang sudah ada.

Halaman ini memandu pengujian komponen kecil dengan dependensi eksternal yang kompleks. Sangat mudah untuk menguji komponen yang tidak berinteraksi dengan kode lain, seperti dengan mengklik tombol dan mengonfirmasi bahwa jumlahnya bertambah. Pada kenyataannya, kode seperti itu sangat sedikit, dan kode pengujian yang tidak memiliki interaksi dapat memiliki nilai terbatas.

(Ini tidak dimaksudkan sebagai tutorial lengkap, dan bagian selanjutnya, Praktik pengujian otomatis, akan memandu pengujian situs sungguhan dengan kode contoh yang dapat Anda gunakan sebagai tutorial. Namun, halaman ini masih akan membahas beberapa contoh pengujian komponen praktis.)

Komponen yang sedang diuji

Kita akan menggunakan Vitest dan lingkungan JSDOM-nya untuk menguji komponen React. Hal ini memungkinkan kita menjalankan pengujian dengan cepat menggunakan Node pada command line sambil mengemulasi browser.

Daftar nama dengan
    tombol Pilih di samping setiap nama.
Komponen React kecil yang menampilkan daftar pengguna dari jaringan.

Komponen React yang bernama UserList ini mengambil daftar pengguna dari jaringan dan memungkinkan Anda memilih salah satunya. Daftar pengguna diperoleh menggunakan fetch di dalam useEffect, dan pengendali pemilihan diteruskan oleh Context. Ini adalah kodenya:

import React, { useEffect, useState, useContext } from 'react';
import { UserContext } from './UserContext.tsx';
import { UserRow } from './UserRow.tsx';

export function UserList({ count = 4 }: { count?: number }) {
  const [users, setUsers] = useState<any[]>([]);
  useEffect(() => {
    fetch('https://jsonplaceholder.typicode.com/users?_limit=' + count)
      .then((response) => response.json())
      .then((json) => setUsers(json));
  }, [count]);

  const c = useContext(UserContext);
  return (
    <div>
      <h2>Users</h2>
      <ul>
        {users.map((u) => (
          <li key={u.id}>
            <button onClick={() => c.userChosen(u.id)}>Choose</button>{' '}
            <UserRow u={u} />
          </li>
        ))}
      </ul>
    </div>
  );
}

Contoh ini tidak menunjukkan praktik terbaik React (misalnya, menggunakan fetch di dalam useEffect), tetapi codebase Anda kemungkinan berisi banyak kasus seperti ini. Lebih penting lagi, kasus ini bisa tampak sulit untuk diuji secara singkat. Bagian selanjutnya dari kursus ini akan membahas penulisan kode yang dapat diuji secara mendetail.

Berikut hal-hal yang kami uji dalam contoh ini:

  • Periksa apakah beberapa DOM yang benar telah dibuat sebagai respons terhadap data dari jaringan.
  • Pastikan bahwa mengklik pengguna akan memicu callback.

Setiap komponen berbeda. Apa yang membuat pengujian ini menarik?

  • Class ini menggunakan fetch global untuk meminta data sungguhan dari jaringan, yang mungkin tidak stabil atau lambat dalam pengujian.
  • Kode ini mengimpor class lain, UserRow, yang mungkin tidak ingin kita uji secara implisit.
  • Class ini menggunakan Context yang secara khusus bukan bagian dari kode yang sedang diuji, dan biasanya disediakan oleh komponen induk.

Tulis pengujian cepat untuk memulai

Kita dapat dengan cepat menguji sesuatu yang sangat mendasar tentang komponen ini. Untuk lebih jelasnya, contoh ini tidak terlalu berguna! Namun, sebaiknya siapkan boilerplate dalam file pembanding yang disebut UserList.test.tsx (ingat, runner pengujian seperti Vitest, secara default, akan menjalankan file yang diakhiri dengan .test.js atau yang serupa, termasuk .tsx):

import { vi, test, assert, afterAll } from 'vitest';
import { render } from '@testing-library/react';
import { UserList } from './UserList.tsx';
import React, { ContextType } from 'react';

test('render', async () => {
  const c = render(<UserList />);

  const headingNode = await c.findAllByText(/Users);
  assert.isNotNull(headingNode);
});

Pengujian ini menegaskan bahwa saat komponen dirender, komponen tersebut akan berisi teks "Users". Ini berfungsi meskipun komponen memiliki efek samping dalam mengirim fetch ke jaringan. fetch masih berlangsung di akhir pengujian, tanpa endpoint yang ditetapkan. Kami tidak dapat mengonfirmasi apakah informasi pengguna ditampilkan saat pengujian berakhir, setidaknya tanpa menunggu waktu tunggu.

Simulasi fetch()

Tiruan adalah tindakan mengganti fungsi atau class sebenarnya dengan sesuatu yang ada di bawah kendali Anda untuk suatu pengujian. Hal ini umum dilakukan di hampir semua jenis pengujian, kecuali untuk pengujian unit yang paling sederhana. Hal ini akan dibahas lebih lanjut dalam Pernyataan dan primitif lainnya.

Anda dapat meniru fetch() untuk pengujian agar selesai dengan cepat dan menampilkan data yang Anda harapkan, dan bukan data "dunia nyata" atau data yang tidak dikenal. fetch adalah global, yang berarti kita tidak harus melakukan import atau require ke dalam kode.

Dalam vitest, Anda dapat membuat tiruan global dengan memanggil vi.stubGlobal dengan objek khusus yang ditampilkan oleh vi.fn()—ini akan membuat tiruan yang dapat kita ubah nanti. Metode ini akan dibahas secara lebih mendetail di bagian selanjutnya dalam kursus ini, tetapi Anda dapat melihatnya dalam praktiknya di kode berikut:

test('render', async () => {
  const fetchMock = vi.fn();
  fetchMock.mockReturnValue(
    Promise.resolve({
      json: () => Promise.resolve([{ name: 'Sam', id: 'sam' }]),
    }),
  );
  vi.stubGlobal('fetch', fetchMock);

  const c = render(<UserList />);

  const headingNode = await c.queryByText(/Users);
  assert.isNotNull(headingNode);

  await waitFor(async () => {
    const samNode = await c.queryByText(/Sam);
    assert.isNotNull(samNode);
  });
});

afterAll(() => {
  vi.unstubAllGlobals();
});

Kode ini menambahkan tiruan, menjelaskan versi "palsu" dari pengambilan jaringan Response, lalu menunggunya muncul. Jika teks tidak muncul—Anda dapat memeriksanya dengan mengubah kueri di queryByText menjadi nama baru—pengujian akan gagal.

Contoh ini telah menggunakan helper tiruan bawaan Vitest, tetapi framework pengujian lainnya memiliki pendekatan yang serupa dengan tiruan. Vitest bersifat unik karena Anda harus memanggil vi.unstubAllGlobals() setelah semua pengujian, atau menetapkan opsi global yang setara. Tanpa "mengurungkan" pekerjaan, tiruan fetch dapat memengaruhi pengujian lain, dan setiap permintaan akan direspons dengan tumpukan JSON yang ganjil.

Impor tiruan

Anda mungkin telah melihat bahwa komponen UserList kita mengimpor komponen yang disebut UserRow. Meskipun kita belum menyertakan kodenya, Anda dapat melihat bahwa kode tersebut merender nama pengguna: pemeriksaan pengujian sebelumnya untuk "Sam", dan yang tidak dirender di dalam UserList secara langsung, sehingga harus berasal dari UserRow.

Diagram alir yang menunjukkan cara nama pengguna berpindah-pindah di komponen kami.
UserListTest tidak memiliki visibilitas UserRow.

Namun, UserRow mungkin merupakan komponen yang kompleks—produk ini dapat mengambil data pengguna lebih lanjut, atau memiliki efek samping yang tidak relevan dengan pengujian kami. Menghapus variasi tersebut akan membuat pengujian Anda lebih bermanfaat, terutama karena komponen yang ingin Anda uji akan lebih kompleks dan lebih terkait dengan dependensinya.

Untungnya, Anda dapat menggunakan Vitest untuk meniru impor tertentu, meskipun pengujian Anda tidak menggunakannya secara langsung, sehingga setiap kode yang menggunakannya akan disediakan versi sederhana atau yang diketahui:

vi.mock('./UserRow.tsx', () => {
  return {
    UserRow(arg) {
      return <>{arg.u.name}</>;
    },
  }
});

test('render', async () => {
  // ...
});

Seperti meniru global fetch, ini adalah alat yang canggih, tetapi mungkin tidak berkelanjutan jika kode Anda memiliki banyak dependensi. Sekali lagi, perbaikan terbaik untuk itu adalah menulis kode yang dapat diuji.

Klik dan berikan konteks

React, dan library lainnya seperti Lit, memiliki konsep yang disebut Context. Kode contoh ini menyertakan UserContext yang memanggil metode jika pengguna dipilih. Hal ini sering dilihat sebagai alternatif untuk "pengeboran properti", dengan callback yang diteruskan ke UserList secara langsung.

Harness pengujian yang kami tulis belum memberikan UserContext. Dengan menambahkan tindakan klik ke pengujian React tanpa tindakan ini, hal ini akan, paling buruk, akan menyebabkan error pada pengujian, atau hal terbaiknya, jika instance default disediakan di tempat lain, menyebabkan beberapa perilaku di luar kendali kita (serupa dengan UserRow yang tidak diketahui di atas):

  const c = render(<UserList />);
  const chooseButton = await c.getByText(/Choose);
  chooseButton.click();

Sebagai gantinya, saat merender komponen, Anda dapat memberikan Context Anda sendiri. Contoh ini menggunakan instance vi.fn(), Vitest Mock Function, yang dapat digunakan setelah fakta untuk memeriksa bahwa panggilan dilakukan, dan argumen yang digunakan tersebut. Dalam kasus ini, kode ini berinteraksi dengan fetch tiruan dalam contoh sebelumnya, dan pengujian dapat mengonfirmasi bahwa ID yang diteruskan adalah "sam":

  const userChosenFn = vi.fn();
  const ucForTest: ContextType<typeof UserContext> = { userChosen: userChosenFn as any };
  const c = render(
    <UserContext.Provider value={ucForTest}>
      <UserList />
    </UserContext.Provider>,
  );

  const chooseButton = await c.getByText(/Choose);
  chooseButton.click();
  assert.deepStrictEqual(userChosenFn.mock.calls, [['sam']]);

Ini adalah pola yang sederhana tetapi efektif, yang dapat Anda gunakan untuk menghapus dependensi yang tidak relevan dari komponen inti yang Anda coba uji.

Ringkasan

Ini telah menjadi contoh cepat dan sederhana yang menunjukkan cara mem-build pengujian komponen untuk menguji dan mengamankan komponen React yang sulit diuji, yang berfokus pada memastikan bahwa komponen berinteraksi dengan dependensinya dengan benar (global fetch, subkomponen yang diimpor, dan Context).

Menguji pemahaman Anda

Pendekatan apa yang digunakan untuk menguji komponen React?

Meniru dependensi kompleks dengan dependensi sederhana untuk pengujian
Injeksi dependensi menggunakan Context
Global Stubbing
Memeriksa apakah angka bertambah