0

Áp dụng chu trình RED-GREEN-REFACTOR: Viết unit test hỏng trước, viết vừa đủ code để pass test, refactor lại code rồi mới commit

Kỷ luật Test-Driven Development (TDD) không phải là việc viết test sau khi code xong để đạt KPI độ phủ mã (code coverage). Nó là một triết lý thiết kế hệ thống. Chu trình RED-GREEN-REFACTOR buộc bạn phải định nghĩa hành vi của một hàm, một module từ góc nhìn của người sử dụng (consumer) trước khi chìm đắm vào các chi tiết triển khai.

Nguyên tắc tối thượng của TDD cực kỳ khắc nghiệt: Tuyệt đối không viết bất kỳ dòng code logic nào nếu chưa có một unit test bị hỏng (failing test) yêu cầu nó. Nếu phát hiện một đoạn code được viết trước khi có test, bạn phải xóa nó đi, viết test trước, rồi mới code lại.

Dưới đây là cách vận hành chu trình này trong thực tế, lấy ví dụ với bài toán xử lý logic trừ tiền thẻ vé (Smart Card) tại cổng kiểm soát (Passenger Gate - PG) của hệ thống AFC.

1. RED: Định Nghĩa Sự Thất Bại (Viết Test Hỏng)

Thay vì tạo file card.service.ts và lao vào code logic kết nối database, bạn phải mở file card.service.spec.ts trước. Mục tiêu ở giai đoạn này là viết một test phản ánh đúng yêu cầu nghiệp vụ, chạy nó và nhìn thấy báo lỗi màu đỏ (RED).

Việc test bị hỏng chứng minh hai điều: môi trường test của bạn hoạt động đúng (không báo pass ảo) và tính năng này thực sự chưa tồn tại.

Hành động: Viết test cho trường hợp thẻ không đủ số dư.

TypeScript

// card.service.spec.ts
import { CardService } from './card.service';
import { InsufficientBalanceError } from '../errors';

describe('CardService - Deduct Fare', () => {
  it('should throw InsufficientBalanceError if card balance is less than fare', async () => {
    const cardService = new CardService();
    const mockCard = { id: 'CARD_123', balance: 5000 }; 
    const fare = 15000;

    await expect(cardService.deductFare(mockCard, fare))
      .rejects.toThrow(InsufficientBalanceError);
  });
});

Chạy lệnh npm test -> Màn hình đỏ rực báo lỗi: CardService is not defined hoặc deductFare is not a function. Bạn đã hoàn thành bước RED.

2. GREEN: Vượt Qua Bài Test Bằng Mọi Giá (Viết Code Vừa Đủ)

Nguyên tắc của bước GREEN là: Viết lượng code ít nhất, ngớ ngẩn nhất, thô thiển nhất miễn là làm cho test chuyển sang màu xanh (Pass).

Ở bước này, bạn không quan tâm đến Clean Code, không thiết kế Design Pattern, không tối ưu thuật toán. Thậm chí việc "hardcode" (gắn cứng giá trị) cũng được hoan nghênh, cốt để chứng minh rằng dây chuyền logic từ Test gọi vào Code đã thông suốt.

Hành động: Khai báo class và hàm vừa đủ để pass bài test trên.

TypeScript

// card.service.ts
import { InsufficientBalanceError } from '../errors';

export class CardService {
  async deductFare(card: any, fare: number): Promise<void> {
    // Code thô thiển nhất có thể để pass test:
    if (card.balance < fare) {
      throw new InsufficientBalanceError('Số dư không đủ để qua cổng');
    }
  }
}

Chạy lại npm test -> Màn hình xanh lá (GREEN). Cảm giác an toàn đã được thiết lập. Bài test của bạn đã khóa chặt business logic này lại.

3. REFACTOR: Cấu Trúc Lại Mã Nguồn (Làm Sạch Code)

Khi đã có "lưới an toàn" là bài test báo pass màu xanh, bạn có thể thoải mái đập đi xây lại đoạn code thô thiển vừa viết mà không sợ làm gãy logic hệ thống. Đây là lúc áp dụng các nguyên tắc SOLID, trích xuất hàm, khai báo Interface chặt chẽ và kết nối với các tầng khác.

Nếu trong quá trình refactor bạn lỡ tay làm sai, bài test sẽ lập tức báo đỏ, giúp bạn rollback ngay lập tức trong vòng vài giây.

Hành động: Chuyển đổi mã thô thành mã chuẩn mực (Production-ready).

TypeScript

// card.service.ts
import { InsufficientBalanceError } from '../errors';
import { ICardRepository } from '../interfaces/card-repository.interface';
import { Card } from '../entities/card.entity';

export class CardService {
  constructor(private readonly cardRepo: ICardRepository) {}

  async deductFare(cardId: string, fare: number): Promise<Card> {
    const card = await this.cardRepo.findById(cardId);
    
    if (card.balance < fare) {
      throw new InsufficientBalanceError(`Thẻ ${card.id} không đủ số dư.`);
    }

    card.balance -= fare;
    return this.cardRepo.save(card);
  }
}

Lúc này, bạn quay lại sửa file test một chút để truyền mock ICardRepository vào CardService, đảm bảo test vẫn xanh. Giai đoạn REFACTOR hoàn tất. Bạn đã có thể tự tin tạo Commit.

Kỷ Luật Thép: Xóa Đoạn Code Sinh Ra Trước Khi Có Test

Trong quá trình phát triển, bạn sẽ thường bị cám dỗ bởi tư duy: "Tiện tay viết luôn đoạn logic khóa thẻ nếu thẻ bị blacklist, đằng nào cũng dùng tới".

Đây là cái bẫy chết người của việc "Over-engineering" và vi phạm nghiêm trọng YAGNI (You Aren't Gonna Need It). Nếu bạn lỡ tay viết một đoạn logic xử lý thẻ blacklist nhưng nhận ra mình chưa viết test cho nó, TDD yêu cầu bạn phải xóa hoặc comment hoàn toàn đoạn code đó lại.

  1. Xóa đoạn logic thừa.

  2. Quay lại bước RED: Viết it('should throw CardBlacklistedError if card status is BLOCKED').

  3. Chạy test để thấy nó đỏ.

  4. Mới được phép viết lại đoạn logic khóa thẻ để nó xanh.

Điều Khiển AI Theo Luồng RED-GREEN-REFACTOR

Khi dùng Cursor hay Claude để code, AI có xu hướng bỏ qua TDD và phun ra hàng trăm dòng code implement cùng lúc. Để ép AI làm việc theo chuẩn này, bạn cần thiết lập rule (System Prompt) cho chúng:

"Từ giờ chúng ta sẽ code theo chuẩn TDD strict mode. BƯỚC 1: Tôi cung cấp yêu cầu. Bạn chỉ được phép sinh ra file *.spec.ts chứa các Unit Test đang hỏng (RED). Không được viết code logic. Đợi tôi chạy test và phản hồi báo lỗi. BƯỚC 2: Khi tôi gửi báo lỗi, bạn viết code vừa đủ vào file logic để pass test (GREEN). BƯỚC 3: Bạn tự động rà soát lại đoạn code vừa viết và đề xuất Refactor. Xóa bỏ bất kỳ dòng code nào bạn lỡ viết mà không có test cover."

Bằng việc bẻ gãy thói quen "code trước, test sau", bạn đang xây dựng một kiến trúc mà mọi hàm sinh ra đều có mục đích rõ ràng, được chứng minh tính đúng đắn ngay từ trong trứng nước và có thể refactor bất kỳ lúc nào trong tương lai.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí