Pengujian komponen dalam praktik

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

Jadi, menguji setiap komponen, terlepas dari seberapa kompleksnya, adalah cara yang baik untuk mulai memikirkan pengujian aplikasi baru atau yang sudah ada.

Halaman ini membahas 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 angka meningkat. Pada kenyataannya, sangat sedikit kode yang seperti itu, dan pengujian kode yang tidak memiliki interaksi dapat memiliki nilai terbatas.

Komponen yang sedang diuji

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

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

Komponen React 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. Berikut 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 itu. Lebih jauh lagi, kasus ini mungkin tampak sulit diuji pada pandangan pertama. Bagian mendatang dari kursus ini akan membahas penulisan kode yang dapat diuji secara mendetail.

Berikut adalah hal-hal yang kami uji dalam contoh ini:

  • Pastikan beberapa DOM yang benar 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 dunia nyata dari jaringan, yang mungkin tidak stabil atau lambat saat diuji.
  • Class ini mengimpor class lain, UserRow, yang mungkin tidak ingin kita uji secara implisit.
  • Ini menggunakan Context yang tidak secara khusus merupakan 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. Agar jelas, contoh ini tidak terlalu berguna. Namun, sebaiknya siapkan boilerplate dalam file peer yang disebut UserList.test.tsx (ingat, runner pengujian seperti Vitest, secara default, 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 menyatakan bahwa saat komponen dirender, komponen tersebut berisi teks "Pengguna". Metode ini berfungsi meskipun komponen memiliki efek samping dari pengiriman fetch ke jaringan. fetch masih berlangsung di akhir pengujian, tanpa endpoint yang ditetapkan. Kami tidak dapat mengonfirmasi bahwa setiap informasi pengguna ditampilkan saat pengujian berakhir, setidaknya tanpa menunggu waktu tunggu.

Simulasi fetch()

Tiruan adalah tindakan mengganti fungsi atau class sebenarnya dengan sesuatu yang berada di bawah kontrol Anda untuk pengujian. Ini adalah praktik umum di hampir semua jenis pengujian, kecuali untuk pengujian unit yang paling sederhana. Hal ini dibahas lebih lanjut di 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 diketahui. fetch bersifat global, yang berarti kita tidak harus import atau require-nya ke dalam kode.

Contohnya, Anda dapat membuat tiruan global dengan memanggil vi.stubGlobal menggunakan objek khusus yang ditampilkan oleh vi.fn()—ini akan mem-build tiruan yang dapat diubah nanti. Metode ini dibahas secara lebih mendetail di bagian selanjutnya dari kursus ini, tetapi Anda dapat melihatnya dalam praktiknya pada 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 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 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 "membatalkan" pekerjaan kita, mock fetch dapat memengaruhi pengujian lain, dan setiap permintaan akan direspons dengan tumpukan JSON yang aneh.

Impor tiruan

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

Diagram alir tentang cara
  nama pengguna bergerak melalui komponen kami.
UserListTest tidak memiliki visibilitas UserRow.

Namun, UserRow itu sendiri mungkin merupakan komponen yang kompleks—komponen ini mungkin mengambil data pengguna lebih lanjut, atau memiliki efek samping yang tidak relevan dengan pengujian kita. Menghapus variabilitas tersebut akan membuat pengujian Anda lebih bermanfaat, terutama saat komponen yang ingin Anda uji menjadi lebih kompleks dan lebih terkait dengan dependensinya.

Untungnya, Anda dapat menggunakan Vitest untuk membuat tiruan impor tertentu, meskipun pengujian Anda tidak menggunakannya secara langsung, sehingga kode apa pun yang menggunakannya akan diberikan dengan versi sederhana atau yang diketahui:

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

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

Seperti mengejek fetch global, ini adalah alat yang canggih, tetapi dapat menjadi tidak berkelanjutan jika kode Anda memiliki banyak dependensi. Sekali lagi, perbaikan terbaiknya adalah menulis kode yang dapat diuji.

Klik dan berikan konteks

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

Harness pengujian kami belum menyediakan UserContext. Dengan menambahkan tindakan klik ke pengujian React tanpa tindakan tersebut, hal ini dapat menyebabkan error pada pengujian. Paling baik, jika instance default disediakan di tempat lain, hal ini dapat 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();

Sebaliknya, saat merender komponen, Anda dapat menyediakan Context Anda sendiri. Contoh ini menggunakan instance vi.fn(), Fungsi Tiruan Vitest, yang dapat digunakan untuk memeriksa apakah panggilan telah dilakukan dan argumen apa yang digunakannya.

Dalam kasus kami, ini berinteraksi dengan fetch yang di-mock 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 sederhana tetapi efektif yang memungkinkan Anda menghapus dependensi yang tidak relevan dari komponen inti yang Anda coba uji.

Ringkasan

Contoh ini menunjukkan cara mem-build pengujian komponen untuk menguji dan melindungi komponen React yang sulit diuji. Pengujian ini berfokus pada memastikan bahwa komponen berinteraksi dengan benar dengan dependensinya: fetch global, subkomponen yang diimpor, dan Context.

Memeriksa pemahaman Anda

Pendekatan apa yang digunakan untuk menguji komponen React?

Injeksi dependensi menggunakan Konteks
Meniru dependensi kompleks dengan dependensi sederhana untuk pengujian
Stubbing global
Memeriksa apakah angka bertambah