Bài 33.4: Mocking trong Next.js-TypeScript tests

Chào mừng trở lại với loạt bài viết về Lập trình Web Front-end! Trong hành trình xây dựng các ứng dụng web mạnh mẽ với Next.js và TypeScript, việc đảm bảo chất lượng code thông qua các bài kiểm thử (tests) là vô cùng quan trọng. Tuy nhiên, không phải lúc nào chúng ta cũng có thể chạy thử nghiệm trong một môi trường hoàn hảo, không bị ảnh hưởng bởi các yếu tố bên ngoài như API, cơ sở dữ liệu, hoặc các thư viện phức tạp. Đây chính là lúc kỹ thuật Mocking phát huy sức mạnh của mình.

Mocking là gì và Tại sao chúng ta cần nó trong Tests?

Mocking (hay giả lập) là một kỹ thuật trong kiểm thử phần mềm, nơi chúng ta thay thế các đối tượng hoặc module phụ thuộc của đơn vị code đang được test bằng các đối tượng "giả" hoặc "thay thế". Những đối tượng giả này có hành vi được định nghĩa sẵn và có thể kiểm soát được.

Mục đích chính của mocking là để cô lập đơn vị code cần test khỏi các phụ thuộc bên ngoài. Thay vì gọi một API thật (tốn thời gian, không ổn định, yêu cầu server phải chạy), chúng ta sẽ thay thế lời gọi API đó bằng một "mock function" trả về dữ liệu giả định mà chúng ta mong muốn.

Lợi ích chính của mocking bao gồm:

  • Tăng tốc độ chạy test: Test không phải chờ đợi các thao tác chậm như gọi mạng, truy vấn database.
  • Giúp test độc lập: Mỗi test case chỉ phụ thuộc vào logic của đơn vị code đang test và mock object, không bị ảnh hưởng bởi trạng thái của các hệ thống bên ngoài hoặc các test case khác.
  • Kiểm soát dữ liệu đầu vào/đầu ra: Bạn có thể dễ dàng giả lập các kịch bản khác nhau, bao gồm cả các trường hợp thành công, thất bại, hoặc dữ liệu rỗng/lỗi.
  • Dễ dàng test các kịch bản lỗi (edge cases): Bạn có thể mock để giả lập các tình huống khó xảy ra trong thực tế nhưng cần được kiểm tra (ví dụ: server trả về lỗi 500, dữ liệu trả về null).

Trong môi trường Next.js với TypeScript, các đơn vị code của bạn (components, hooks, API handlers, utility functions) thường có sự phụ thuộc vào các module hoặc dịch vụ khác. Mocking trở thành một công cụ thiết yếu để viết các unit test và integration test hiệu quả.

Các Tình huống phổ biến cần Mocking trong Next.js/TypeScript

Trong một ứng dụng Next.js sử dụng TypeScript, bạn sẽ thường gặp các tình huống cần mocking như:

  1. Fetching Data: Components hoặc pages sử dụng các hàm để gọi API (client-side hooks như useEffect, hoặc server-side rendering functions như getServerSideProps, getStaticProps). Bạn cần mock các hàm gọi API này.
  2. Sử dụng Browser APIs: Code tương tác với localStorage, sessionStorage, navigator, window, v.v. Các API này không tồn tại trong môi trường Node.js mặc định khi chạy test bằng Jest.
  3. Tương tác với Thư viện bên thứ ba: Các thư viện tích hợp thanh toán, analytics, logging, v.v., thường thực hiện các side effect (gọi mạng, tương tác với môi trường).
  4. Testing React Hooks: Các custom hooks phức tạp có thể phụ thuộc vào Context API hoặc các hàm khác cần được mock.
  5. Testing Server-side Logic: API Routes hoặc getServerSideProps/getStaticProps phụ thuộc vào các dịch vụ backend hoặc database.

Cách Mocking với Jest trong Next.js/TypeScript

Jest là một framework test rất phổ biến trong hệ sinh thái React/Next.js và cung cấp các công cụ mạnh mẽ để thực hiện mocking. Dưới đây là một số kỹ thuật mocking thường dùng với Jest:

1. jest.fn() - Mocking một hàm cơ bản

Đây là cách đơn giản nhất để tạo một hàm giả. Một mock function được tạo bởi jest.fn() cho phép bạn theo dõi:

  • Số lần hàm được gọi.
  • Các đối số mà hàm được gọi cùng.
  • Giá trị trả về (return value) của hàm.
  • Các instance được tạo ra nếu nó là một hàm constructor.
// Ví dụ cơ bản với jest.fn()
describe('Basic Mock Function', () => {
  it('should be callable and track calls', () => {
    const mockFunc = jest.fn();

    mockFunc('first call', 123);
    mockFunc('second call');

    // Kiểm tra xem hàm có được gọi hay không
    expect(mockFunc).toHaveBeenCalled();

    // Kiểm tra số lần gọi
    expect(mockFunc).toHaveBeenCalledTimes(2);

    // Kiểm tra đối số của lần gọi gần nhất
    expect(mockFunc).toHaveBeenCalledWith('second call');

    // Kiểm tra đối số của một lần gọi cụ thể
    expect(mockFunc).toHaveBeenNthCalledWith(1, 'first call', 123);
  });
});

Giải thích: Chúng ta tạo một mockFunc. Sau khi gọi nó vài lần, chúng ta sử dụng các matcher của Jest (toHaveBeenCalled, toHaveBeenCalledTimes, toHaveBeenCalledWith, toHaveBeenNthCalledWith) để kiểm tra xem hàm mock đã hoạt động như mong đợi hay chưa.

2. Cấu hình hành vi của Mock Function

Mock functions trở nên mạnh mẽ hơn khi bạn cấu hình chúng để trả về các giá trị cụ thể hoặc thực hiện logic giả:

  • mockReturnValue(value): Trả về một giá trị cụ thể mỗi khi được gọi.
  • mockReturnValueOnce(value): Trả về giá trị cụ thể chỉ cho lần gọi tiếp theo. Bạn có thể gọi nó nhiều lần để cấu hình nhiều giá trị trả về liên tiếp.
  • mockImplementation(fn): Cung cấp một implementation (logic) giả cho hàm mock.
  • mockImplementationOnce(fn): Cung cấp implementation giả chỉ cho lần gọi tiếp theo.
  • mockResolvedValue(value): Trả về một Promise được giải quyết (resolved) với giá trị cụ thể. Rất hữu ích khi mock các hàm async.
  • mockRejectedValue(error): Trả về một Promise bị từ chối (rejected) với lỗi cụ thể.
// Ví dụ cấu hình mock function
describe('Configuring Mock Function', () => {
  it('should return specified values', () => {
    const mockFunc = jest.fn();

    // Cấu hình giá trị trả về
    mockFunc.mockReturnValue('default value');
    mockFunc.mockReturnValueOnce('first value');
    mockFunc.mockReturnValueOnce('second value');

    expect(mockFunc()).toBe('first value');
    expect(mockFunc()).toBe('second value');
    expect(mockFunc()).toBe('default value'); // Sau khi hết các giá trị Once, nó quay lại giá trị default
  });

  it('should use specified implementation', () => {
    const mockFunc = jest.fn();

    // Cấu hình implementation giả
    mockFunc.mockImplementation((a: number, b: number) => a + b);

    expect(mockFunc(2, 3)).toBe(5);
  });

  it('should handle async return values', async () => {
    const mockAsyncFunc = jest.fn();

    // Cấu hình trả về Promise đã resolved
    mockAsyncFunc.mockResolvedValue({ data: 'fake data' });

    const result = await mockAsyncFunc();
    expect(result).toEqual({ data: 'fake data' });

    // Cấu hình trả về Promise bị rejected
    mockAsyncFunc.mockRejectedValue(new Error('API failed'));

    await expect(mockAsyncFunc()).rejects.toThrow('API failed');
  });
});

Giải thích: Các phương thức cấu hình cho phép bạn điều khiển chính xác những gì hàm mock sẽ làm khi được gọi, mô phỏng các kịch bản thành công hoặc thất bại trong thế giới thực. mockResolvedValuemockRejectedValuekhông thể thiếu khi làm việc với các hàm bất đồng bộ như gọi API.

3. jest.mock() - Mocking toàn bộ Module

Khi đơn vị code của bạn phụ thuộc vào toàn bộ một module (ví dụ: một file chứa các hàm gọi API, một thư viện UI phức tạp), bạn có thể mock toàn bộ module đó. Jest sẽ thay thế module gốc bằng phiên bản mock của nó ở mọi nơi nó được import trong file test.

Có hai cách chính để sử dụng jest.mock():

  • Mock tự động: jest.mock('./path/to/module'); - Jest sẽ tự động mock module, biến tất cả các export của module thành các mock function cơ bản (jest.fn()).
  • Mock thủ công với factory function: jest.mock('./path/to/module', () => ({ ... })); - Bạn cung cấp một hàm factory trả về một đối tượng. Đối tượng này sẽ trở thành module mock. Bạn có thể định nghĩa các mock function hoặc giá trị cụ thể cho từng export. Cách này cho phép bạn kiểm soát hoàn toàn nội dung của module mock.

Cách thứ hai với factory function là thường dùng hơn trong các ứng dụng thực tế vì nó cho phép bạn cấu hình hành vi cụ thể cho từng phần của module mock.

// Giả sử bạn có một file apiService.ts như sau:
// src/apiService.ts
export const getUsers = async () => {
  console.log('Calling real API: /api/users');
  // Logic gọi API thực tế ở đây
  const res = await fetch('/api/users');
  if (!res.ok) throw new Error('Failed to fetch users');
  return res.json();
};

export const createUser = async (user: any) => {
  console.log('Calling real API: /api/users POST');
  // Logic gọi API thực tế
  const res = await fetch('/api/users', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(user),
  });
  if (!res.ok) throw new Error('Failed to create user');
  return res.json();
};

// Bây giờ, test một component sử dụng apiService:
// src/components/UserList.tsx
import { useEffect, useState } from 'react';
import { getUsers } from '../apiService'; // Component phụ thuộc vào getUsers

function UserList() {
  const [users, setUsers] = useState<any[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const loadUsers = async () => {
      try {
        setLoading(true);
        const data = await getUsers(); // Lời gọi hàm cần mock
        setUsers(data);
      } catch (err: any) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };
    loadUsers();
  }, []); // Chú ý dependency array rỗng, chỉ chạy 1 lần khi mount

  if (loading) return <div>Loading users...</div>;
  if (error) return <div>Error: {error}</div>;
  if (users.length === 0) return <div>No users found.</div>;

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

export default UserList;

// File test cho component UserList:
// src/components/UserList.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom'; // Import matcher cho DOM
import UserList from './UserList';
// Import hàm gốc để có thể cast nó sang kiểu jest.Mock
import { getUsers } from '../apiService';

// 1. Mock toàn bộ module apiService trước khi import component cần test
// Jest sẽ hoist (đưa lên đầu) lời gọi jest.mock()
jest.mock('../apiService');

// 2. Ép kiểu hàm gốc đã import thành kiểu jest.Mock để truy cập các phương thức mock
const mockedGetUsers = getUsers as jest.Mock;

describe('UserList Component', () => {
  // Đảm bảo reset mock sau mỗi test case để chúng độc lập
  beforeEach(() => {
    mockedGetUsers.mockClear();
  });

  it('should display loading state initially', () => {
    // Cấu hình mock để nó *chưa* resolve ngay lập tức
    // Điều này cần thiết nếu bạn muốn test trạng thái loading
    // Tuy nhiên, thường thì ta sẽ cấu hình mockResolvedValue ngay.
    // Cách đơn giản nhất là mock ResolvedValue:
     mockedGetUsers.mockResolvedValue([]); // Provide a default mock value

    render(<UserList />);

    // React Testing Library xử lý bất đồng bộ khá tốt với findBy...
    // Ban đầu component render, trạng thái loading là true
    expect(screen.getByText('Loading users...')).toBeInTheDocument();
  });


  it('should render list of users after fetching', async () => {
    // Dữ liệu giả mà mock function sẽ trả về
    const dummyUsers = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }];

    // Cấu hình mock function getUsers để trả về dữ liệu giả khi được gọi
    mockedGetUsers.mockResolvedValue(dummyUsers);

    // Render component
    render(<UserList />);

    // Kiểm tra xem hàm getUsers đã được gọi chưa
    expect(mockedGetUsers).toHaveBeenCalledTimes(1);

    // Chờ cho đến khi dữ liệu được load và hiển thị (sử dụng findBy để chờ bất đồng bộ)
    await waitFor(() => {
        expect(screen.getByText('Alice')).toBeInTheDocument();
        expect(screen.getByText('Bob')).toBeInTheDocument();
    });

    // Đảm bảo trạng thái loading không còn hiển thị
    expect(screen.queryByText('Loading users...')).not.toBeInTheDocument();
    // Đảm bảo thông báo rỗng không hiển thị
    expect(screen.queryByText('No users found.')).not.toBeInTheDocument();
  });

  it('should display error message if fetching fails', async () => {
    // Cấu hình mock function getUsers để trả về lỗi khi được gọi
    mockedGetUsers.mockRejectedValue(new Error('Network Error'));

    // Render component
    render(<UserList />);

    // Kiểm tra xem hàm getUsers đã được gọi chưa
    expect(mockedGetUsers).toHaveBeenCalledTimes(1);

    // Chờ cho đến khi thông báo lỗi hiển thị
    await waitFor(() => {
        expect(screen.getByText('Error: Network Error')).toBeInTheDocument();
    });

    // Đảm bảo trạng thái loading không còn hiển thị
    expect(screen.queryByText('Loading users...')).not.toBeInTheDocument();
  });

  it('should display "No users found" if fetch returns empty array', async () => {
    // Cấu hình mock function getUsers để trả về mảng rỗng
    mockedGetUsers.mockResolvedValue([]);

    // Render component
    render(<UserList />);

    // Kiểm tra xem hàm getUsers đã được gọi chưa
    expect(mockedGetUsers).toHaveBeenCalledTimes(1);

    // Chờ cho đến khi thông báo "No users found" hiển thị
    await waitFor(() => {
        expect(screen.getByText('No users found.')).toBeInTheDocument();
    });

    // Đảm bảo các trạng thái khác không hiển thị
    expect(screen.queryByText('Loading users...')).not.toBeInTheDocument();
    expect(screen.queryByText('Error:')).not.toBeInTheDocument();
  });
});

Giải thích:

  • Chúng ta sử dụng jest.mock('../apiService'); để bảo Jest rằng chúng ta muốn mock toàn bộ module apiService.
  • Sau đó, chúng ta import hàm getUsers từ module gốc. Vì module đã bị mock, hàm getUsers mà chúng ta import không phải là hàm gốc thực hiện gọi API, mà là một mock function do Jest tạo ra. Chúng ta ép kiểu nó sang jest.Mock để có thể truy cập các phương thức như mockResolvedValue.
  • Trước mỗi test case (beforeEach), chúng ta dùng mockedGetUsers.mockClear() để đảm bảo số lần gọi và các cấu hình mock từ test case trước không ảnh hưởng đến test case hiện tại.
  • Trong mỗi test case, chúng ta sử dụng mockedGetUsers.mockResolvedValue() hoặc mockedGetUsers.mockRejectedValue() để định nghĩa dữ liệu hoặc lỗi mà hàm mock sẽ trả về.
  • Chúng ta render component UserList. Khi UserList gọi getUsers bên trong useEffect, nó sẽ gọi hàm mock mà chúng ta đã cấu hình, thay vì gọi API thật.
  • Cuối cùng, chúng ta sử dụng các hàm truy vấn (query/find/get) của React Testing Library và các matcher của Jest để kiểm tra xem component đã render đúng dựa trên dữ liệu mock hay chưa. waitFor là cần thiết để chờ đợi hiệu ứng bất đồng bộ của useEffect và cập nhật state của component.
4. Mocking Global Objects

Trong các ứng dụng web front-end, code của bạn thường tương tác với các đối tượng global của trình duyệt như window, document, localStorage, fetch, v.v. Khi chạy test bằng Jest (mặc định chạy trong môi trường Node.js), các đối tượng này có thể không tồn tại hoặc có hành vi khác. Bạn cần mock chúng.

Jest cung cấp đối tượng global (tương đương window trong trình duyệt) để bạn có thể thêm hoặc thay thế các thuộc tính global.

// Ví dụ Mocking fetch API
describe('Mocking Global Fetch', () => {
  const mockFetch = jest.fn();
  const originalFetch = global.fetch; // Lưu lại hàm fetch gốc nếu muốn khôi phục sau

  // Thay thế global fetch bằng mock function trước mỗi test
  beforeEach(() => {
    global.fetch = mockFetch as any; // Ép kiểu để TypeScript không báo lỗi
    // Reset cấu hình mock sau mỗi test
    mockFetch.mockClear();
  });

  // Khôi phục hàm fetch gốc sau tất cả các test (tùy chọn)
  afterAll(() => {
     global.fetch = originalFetch;
  });


  it('should call fetch with correct URL', async () => {
    // Cấu hình mock fetch để trả về response giả
    mockFetch.mockResolvedValue({
      ok: true,
      json: () => Promise.resolve({ message: 'success' }),
    });

    // Giả sử bạn có một hàm util gọi fetch
    async function getData(url: string) {
      const res = await fetch(url);
      if (!res.ok) throw new Error('Fetch failed');
      return res.json();
    }

    await getData('/api/some-data');

    expect(mockFetch).toHaveBeenCalledTimes(1);
    expect(mockFetch).toHaveBeenCalledWith('/api/some-data');
  });

  it('should handle fetch errors', async () => {
    // Cấu hình mock fetch để trả về lỗi
    mockFetch.mockResolvedValue({
      ok: false, // isOk = false
      status: 500,
      statusText: 'Internal Server Error',
    });

    async function getData(url: string) {
        const res = await fetch(url);
        if (!res.ok) throw new Error('Fetch failed'); // Hàm này sẽ throw lỗi
        return res.json();
    }

    await expect(getData('/api/error-data')).rejects.toThrow('Fetch failed');
    expect(mockFetch).toHaveBeenCalledTimes(1);
  });
});

// Ví dụ Mocking localStorage
describe('Mocking Global localStorage', () => {
    const localStorageMock = {
        getItem: jest.fn(),
        setItem: jest.fn(),
        clear: jest.fn(),
        removeItem: jest.fn(),
        length: 0, // Thêm các thuộc tính khác nếu cần
        key: jest.fn(),
    };

    const originalLocalStorage = Object.getOwnPropertyDescriptor(window, 'localStorage');

    beforeEach(() => {
        // Thay thế localStorage trên đối tượng window bằng mock object
        Object.defineProperty(window, 'localStorage', {
            value: localStorageMock,
            writable: true, // Cho phép ghi đè nếu cần thiết
            configurable: true, // Cho phép defineProperty lại hoặc xóa
        });
        // Reset mock function trong localStorageMock
        localStorageMock.getItem.mockClear();
        localStorageMock.setItem.mockClear();
        localStorageMock.clear.mockClear();
        localStorageMock.removeItem.mockClear();
        localStorageMock.key.mockClear();
    });

    afterEach(() => {
        // Khôi phục localStorage gốc sau mỗi test (quan trọng!)
        if (originalLocalStorage) {
             Object.defineProperty(window, 'localStorage', originalLocalStorage);
        } else {
             // Nếu localStorage gốc không tồn tại, xóa thuộc tính
             delete (window as any).localStorage;
        }
    });

    it('should call localStorage.setItem when saving data', () => {
        function saveData(key: string, value: string) {
            localStorage.setItem(key, value);
        }

        saveData('userPref', 'darkMode');

        expect(localStorageMock.setItem).toHaveBeenCalledTimes(1);
        expect(localStorageMock.setItem).toHaveBeenCalledWith('userPref', 'darkMode');
    });

    it('should call localStorage.getItem when loading data', () => {
        localStorageMock.getItem.mockReturnValue('lightMode'); // Cấu hình giá trị trả về

        function loadData(key: string): string | null {
            return localStorage.getItem(key);
        }

        const pref = loadData('userPref');

        expect(localStorageMock.getItem).toHaveBeenCalledTimes(1);
        expect(localStorageMock.getItem).toHaveBeenCalledWith('userPref');
        expect(pref).toBe('lightMode');
    });
});

Giải thích:

  • Chúng ta sử dụng global.fetch = mockFetch as any; để thay thế hàm fetch global bằng mock function của chúng ta. Ép kiểu as any đôi khi cần thiết để tránh lỗi TypeScript khi gán vào các thuộc tính global có kiểu phức tạp.
  • Đối với localStorage hoặc các thuộc tính phức tạp khác của window, sử dụng Object.defineProperty là cách an toàn hơn để thay thế, đảm bảo các thuộc tính như configurable được set đúng.
  • beforeEachafterEach (hoặc afterAll) được sử dụng để thiết lập môi trường mock trước mỗi test và dọn dẹp (khôi phục) sau đó. Điều này cực kỳ quan trọng để đảm bảo tính độc lập giữa các test case, đặc biệt khi mock các đối tượng global.
5. Mocking React Hooks

React Hooks là một phần không thể thiếu trong các ứng dụng React/Next.js hiện đại. Khi test các component sử dụng custom hooks có logic phức tạp hoặc phụ thuộc ngoài (như gọi API, sử dụng Context), bạn cần mock hook đó.

Cách phổ biến để mock hook là mock module chứa hook đó bằng jest.mock() và cung cấp implementation giả cho hook.

// Giả sử bạn có một custom hook useAuth.ts:
// src/hooks/useAuth.ts
import { useState, useEffect, createContext, useContext } from 'react';

interface AuthContextType {
  user: { id: number; name: string } | null;
  login: (username: string, password: string) => Promise<void>;
  logout: () => Promise<void>;
}

// Giả định một Context (thực tế có thể dùng Context hoặc không)
const AuthContext = createContext<AuthContextType | undefined>(undefined);

// Giả định hook sử dụng context hoặc gọi API
export const useAuth = (): AuthContextType => {
  const context = useContext(AuthContext);
  if (context === undefined) {
    // Fallback hoặc logic mặc định nếu không dùng Context Provider
    const [user, setUser] = useState<{ id: number; name: string } | null>(null);

    // Logic gọi API login/logout
    const login = async (username: string, password: string) => {
        // Logic gọi API login thật
        console.log('Real login API called');
        return new Promise<void>(resolve => {
            setTimeout(() => {
                setUser({ id: 1, name: username });
                resolve();
            }, 500); // Simulate async operation
        });
    };
     const logout = async () => {
        // Logic gọi API logout thật
         console.log('Real logout API called');
        return new Promise<void>(resolve => {
            setTimeout(() => {
                setUser(null);
                resolve();
            }, 300); // Simulate async operation
        });
    };

    useEffect(() => {
        // Logic kiểm tra session ban đầu
        console.log('Checking initial session');
        // Đây cũng là chỗ cần mock nếu nó gọi API hoặc localStorage
    }, []);

    return { user, login, logout };
  }
  return context; // Sử dụng context nếu có
};


// Component sử dụng hook useAuth:
// src/components/Profile.tsx
import { useAuth } from '../hooks/useAuth'; // Component phụ thuộc vào useAuth

function Profile() {
  const { user, logout } = useAuth(); // Sử dụng hook

  if (!user) {
    return <div>Please log in to view your profile.</div>;
  }

  return (
    <div>
      <h2>Welcome, {user.name}!</h2>
      <button onClick={() => logout()}>Logout</button> {/* Sử dụng hàm từ hook */}
    </div>
  );
}

export default Profile;

// File test cho component Profile:
// src/components/Profile.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import Profile from './Profile';
import { useAuth } from '../hooks/useAuth'; // Import hook gốc

// Mock module chứa hook useAuth
jest.mock('../hooks/useAuth');

// Ép kiểu hook gốc đã import thành jest.Mock
const mockedUseAuth = useAuth as jest.Mock;

describe('Profile Component', () => {
    // Đảm bảo reset mock sau mỗi test case
    beforeEach(() => {
        mockedUseAuth.mockClear();
    });

    it('should display login message when user is not logged in', () => {
        // Cấu hình hook mock để trả về trạng thái chưa login
        mockedUseAuth.mockReturnValue({
            user: null,
            login: jest.fn(), // Mock các hàm khác cũng là best practice
            logout: jest.fn(),
        });

        render(<Profile />);

        expect(screen.getByText('Please log in to view your profile.')).toBeInTheDocument();
        expect(screen.queryByText(/Welcome,/)).not.toBeInTheDocument(); // Đảm bảo thông báo welcome không xuất hiện
    });

    it('should display welcome message and logout button when user is logged in', () => {
        const dummyUser = { id: 1, name: 'Test User' };

        // Cấu hình hook mock để trả về trạng thái đã login
        mockedUseAuth.mockReturnValue({
            user: dummyUser,
            login: jest.fn(),
            logout: jest.fn(), // Tạo mock function cho logout
        });

        render(<Profile />);

        expect(screen.getByText(`Welcome, ${dummyUser.name}!`)).toBeInTheDocument();
        const logoutButton = screen.getByRole('button', { name: 'Logout' });
        expect(logoutButton).toBeInTheDocument();

        // Kiểm tra xem hàm logout có được gọi khi click button không
        fireEvent.click(logoutButton);
        expect(mockedUseAuth().logout).toHaveBeenCalledTimes(1);
    });
});

Giải thích:

  • Chúng ta mock module ../hooks/useAuth bằng jest.mock().
  • Sau đó, chúng ta import useAuth gốc (thực tế là bản mock) và ép kiểu nó sang jest.Mock.
  • Trong mỗi test case, chúng ta sử dụng mockedUseAuth.mockReturnValue() để định nghĩa giá trị mà hook useAuth sẽ trả về khi component Profile gọi nó. Chúng ta cấu hình các thuộc tính user, login, logout với giá trị mock phù hợp với từng kịch bản test (login/logout).
  • Khi Profile component render, nó gọi useAuth(). Thay vì chạy logic thật trong hook, nó nhận được giá trị mock mà chúng ta đã cấu hình.
  • Chúng ta test component Profile dựa trên giá trị mock này, ví dụ, kiểm tra xem thông báo nào hiển thị khi usernull hay khi user có giá trị.
  • Chúng ta cũng mock hàm logout được trả về từ hook mock bằng jest.fn() và kiểm tra xem nó có được gọi khi người dùng click vào nút "Logout" hay không.

Kỹ thuật mocking hooks giúp bạn kiểm tra logic của component độc lập với logic phức tạp bên trong hook, làm cho test nhanh hơn và đáng tin cậy hơn.

Kỹ thuật mocking là một công cụ không thể thiếu giúp bạn viết các unit test và integration test hiệu quả, đặc biệt trong môi trường phức tạp như Next.js với TypeScript. Bằng cách kiểm soát các phụ thuộc, bạn có thể tập trung vào việc kiểm tra logic cốt lõi của từng đơn vị code, xây dựng một bộ test nhanh chóng, ổn định và đáng tin cậy.

Comments

There are no comments at the moment.