0

Your README Is Lying to You: Test Your Onboarding Docs Like You Test Code

Tuần này trên Hacker News có một bài lên top khá thú vị: tác giả trả tiền cho người lạ chỉ để họ làm theo README của project. Kết quả đúng như dự đoán: rất nhiều người kẹt ngay từ mấy bước đầu. Đọc xong mình thấy hơi nhột, vì mình cũng từng viết README kiểu "chạy make setup là xong", trong khi thực tế máy mình đã cài sẵn 7 thứ mà mình quên mất. Nếu bạn maintain một open source project, một internal tool hay chỉ là repo cho team mới vào, thì README chính là trải nghiệm đầu tiên của người dùng. Bài này chia sẻ cách mình test README một cách có hệ thống: từ test thủ công với người thật cho đến tự động hoá trong CI.

Tại sao README luôn "mục" theo thời gian

Code có test, có type checker, có linter. README thì không có gì cả. Nên nó hỏng âm thầm theo vài kiểu quen thuộc:

  • Implicit dependency: bạn cài jq, pnpm, Docker từ 2 năm trước nên quên không ghi vào phần Prerequisites.
    • Version drift: README ghi Node 16, còn package.json đã yêu cầu >=20.11 từ lâu.
    • Bước bị bỏ qua: thiếu cp .env.example .env, nên app crash với một lỗi chẳng ai hiểu nổi.
    • Lệnh đã đổi tên: script npm run dev đã đổi thành npm run start:dev sau một lần refactor.

Vấn đề cốt lõi là người viết README là người duy nhất không thể test nó, vì máy của bạn đã ở trạng thái "mọi thứ đều chạy". Muốn test được README, bạn cần một môi trường sạch: một người khác, hoặc một container trắng tinh.

flowchart LR
    A[Dev viết README] --> B[Máy dev đã có sẵn tool]
        B --> C[Test thấy OK]
            C --> D[Người mới clone repo]
                D --> E{Thiếu dependency?}
                    E -->|Có| F[Kẹt, bỏ cuộc]
                        E -->|Không| G[Chạy được]
                        ```
                        
                        ## Bước 1: Test thủ công với người thật và ghi friction log
                        
                        Không cần trả tiền cho người lạ như tác giả bài HN. Mình hay làm thế này: nhờ một bạn trong team **chưa từng đụng vào repo** (hoặc intern mới), đưa link README, rồi ngồi im quan sát. Luật chơi rất đơn giản:
                        
                        1. Người test chỉ được đọc README, không được hỏi mình.
                        2. Người test vừa làm vừa nói ra suy nghĩ (think-aloud).
                        3. Mình chỉ ghi chép, **tuyệt đối không nhắc**.
                        
                        Đoạn khó nhất là ngồi im khi thấy người ta loay hoay. Nhưng chính những lúc đó mới lộ ra lỗ hổng. Mình ghi lại vào một file `friction-log.md` theo format sau:
                        
                        ```markdown
                        | Thời điểm | Bước trong README | Chuyện gì xảy ra | Mức độ |
                        |-----------|-------------------|------------------|--------|
                        | 02:10 | Install | Không biết cần Python 3.12, máy có 3.9 | Blocker |
                        | 05:45 | Config | Không biết lấy API_KEY ở đâu | Blocker |
                        | 09:30 | Run | Port 8080 bị chiếm, không có hướng dẫn đổi port | Minor |
                        ```
                        
                        Một buổi 30 phút thường cho mình 5-10 vấn đề. Mấy dòng Blocker phải fix ngay. Những chỗ Minor thì gom lại thành một mục Troubleshooting ở cuối README.
                        
                        ## Bước 2: Biến README thành thứ chạy được
                        
                        Test với người thật thì tốn công, không làm mỗi tuần được. Nên mình tự động hoá phần có thể tự động: **các lệnh shell trong README**. Ý tưởng là đánh dấu những code block cần test bằng một tag riêng trong info string của fence, ví dụ `bash readme-test`. GitHub vẫn highlight là bash bình thường vì nó chỉ đọc từ đầu tiên.
                        
                        Sau đó viết một script Python nhỏ để extract và chạy chúng:
                        
                        ```python
                        #!/usr/bin/env python3
                        # scripts/readme_runner.py
                        import re
                        import subprocess
                        import sys
                        from pathlib import Path
                        
                        FENCE = '`' * 3
                        BLOCK_RE = re.compile(FENCE + r'bash readme-test\n(.*?)' + FENCE, re.DOTALL)
                        
                        
                        def extract_blocks(path: str) -> list[str]:
                            text = Path(path).read_text(encoding='utf-8')
                                return BLOCK_RE.findall(text)
                                
                                
                                def main() -> None:
                                    readme = sys.argv[1] if len(sys.argv) > 1 else 'README.md'
                                        blocks = extract_blocks(readme)
                                            if not blocks:
                                                    print('Không tìm thấy block readme-test nào')
                                                            sys.exit(1)
                                                            
                                                                for i, block in enumerate(blocks, 1):
                                                                        print(f'--- Block {i} ---\n{block}')
                                                                        
                                                                            # Gộp tất cả block vào MỘT shell để giữ cd, export... giữa các bước
                                                                                script = 'set -euxo pipefail\n' + '\n'.join(blocks)
                                                                                    result = subprocess.run(['bash', '-c', script])
                                                                                        sys.exit(result.returncode)
                                                                                        
                                                                                        
                                                                                        if __name__ == '__main__':
                                                                                            main()
                                                                                            ```
                                                                                            
                                                                                            Điểm quan trọng: **chạy tất cả block trong cùng một shell session**. Người dùng thật cũng gõ lệnh liên tiếp trong một terminal, nên `cd` hay `export` ở bước 2 phải còn hiệu lực ở bước 5. Flag `set -euxo pipefail` giúp script dừng ngay ở lệnh đầu tiên bị fail và in từng lệnh ra để dễ debug.
                                                                                            
                                                                                            Nhưng nếu chạy script này trên máy bạn thì vẫn vô nghĩa, vì máy bạn đã có sẵn mọi thứ. Phải chạy trong container trắng:
                                                                                            
                                                                                            ```bash
                                                                                            # Chỉ cài đúng những gì README ghi ở phần Prerequisites, không hơn
                                                                                            docker run --rm -v \"$PWD\":/src:ro ubuntu:24.04 bash -c '
                                                                                              apt-get update -qq
                                                                                                apt-get install -y -qq python3 git curl > /dev/null
                                                                                                  cp -r /src /work && cd /work
                                                                                                    python3 scripts/readme_runner.py README.md
                                                                                                    '
                                                                                                    ```
                                                                                                    
                                                                                                    Mount repo ở chế độ `:ro` rồi copy sang `/work` để script không ghi bậy vào thư mục thật của bạn. Nếu README nói prerequisites là Python, git và curl, thì container chỉ được có đúng chừng đó. Lần đầu chạy, mình gần như chắc chắn bạn sẽ thấy nó fail ở chỗ bạn không ngờ tới.
                                                                                                    
                                                                                                    ## Bước 3: Đưa vào CI để README không mục lại
                                                                                                    
                                                                                                    Fix một lần chưa đủ, README sẽ hỏng lại ngay sau vài sprint. Cho nó chạy trong GitHub Actions, trigger khi README đổi và chạy định kỳ hàng tuần để bắt các lỗi do upstream thay đổi (package bị yank, install script đổi URL...):
                                                                                                    
                                                                                                    ```yaml
                                                                                                    # .github/workflows/readme-test.yml
                                                                                                    name: readme-test
                                                                                                    on:
                                                                                                      pull_request:
                                                                                                          paths: ['README.md', 'docs/**', 'package.json', 'pyproject.toml']
                                                                                                            schedule:
                                                                                                                - cron: '0 2 * * 1'   # 2h sáng thứ Hai, UTC
                                                                                                                  workflow_dispatch:
                                                                                                                  
                                                                                                                  jobs:
                                                                                                                    readme:
                                                                                                                        runs-on: ubuntu-24.04
                                                                                                                            container: ubuntu:24.04
                                                                                                                                timeout-minutes: 15
                                                                                                                                    steps:
                                                                                                                                          - uses: actions/checkout@v4
                                                                                                                                                - name: Prerequisites đúng như README
                                                                                                                                                        run: apt-get update && apt-get install -y python3 git curl
                                                                                                                                                              - name: Chạy các bước trong README
                                                                                                                                                                      run: python3 scripts/readme_runner.py README.md
                                                                                                                                                                      ```
                                                                                                                                                                      
                                                                                                                                                                      Để ý là mình thêm `package.json` và `pyproject.toml` vào `paths`. Khi ai đó bump version runtime mà quên sửa README, CI sẽ đỏ ngay trong PR đó, đúng lúc người sửa còn nhớ context.
                                                                                                                                                                      
                                                                                                                                                                      ```mermaid
                                                                                                                                                                      flowchart TD
                                                                                                                                                                          A[PR sửa README hoặc dependency] --> C[GitHub Actions]
                                                                                                                                                                              B[Cron hàng tuần] --> C
                                                                                                                                                                                  C --> D[Container ubuntu 24.04 sạch]
                                                                                                                                                                                      D --> E[readme_runner.py]
                                                                                                                                                                                          E --> F{Pass?}
                                                                                                                                                                                              F -->|Có| G[Merge]
                                                                                                                                                                                                  F -->|Không| H[Sửa README trước khi merge]
                                                                                                                                                                                                  ```
                                                                                                                                                                                                  
                                                                                                                                                                                                  Vài lưu ý thực tế khi áp dụng:
                                                                                                                                                                                                  
                                                                                                                                                                                                  - **Lệnh cần secret** (API key, login cloud): đừng tag `readme-test`, hoặc dùng mock/env giả trong CI.
                                                                                                                                                                                                  - **Lệnh chạy server lâu**: tách ra, chạy background rồi `curl --retry 10 --retry-connrefused localhost:8080/health` để verify, sau đó kill.
                                                                                                                                                                                                  - **Hướng dẫn cho macOS/Windows**: CI Linux không cover được, nên vẫn cần test thủ công định kỳ.
                                                                                                                                                                                                  
                                                                                                                                                                                                  ## Kết luận
                                                                                                                                                                                                  
                                                                                                                                                                                                  README là một phần của sản phẩm, không phải phụ lục. Một người kẹt ở bước 2 thì sẽ không bao giờ thấy được tính năng xịn nhất của bạn ở bước 10. Những việc bạn có thể làm ngay trong tuần này:
                                                                                                                                                                                                  
                                                                                                                                                                                                  1. **Làm 1 buổi test 30 phút** với một đồng nghiệp chưa từng đụng repo. Ngồi im, ghi friction log.
                                                                                                                                                                                                  2. **Fix hết các Blocker** trong log, nhất là implicit dependency và version runtime.
                                                                                                                                                                                                  3. **Tag các code block** quan trọng bằng `bash readme-test` và chạy `readme_runner.py` trong `ubuntu:24.04` sạch.
                                                                                                                                                                                                  4. **Thêm workflow CI** trigger theo cả PR lẫn cron hàng tuần.
                                                                                                                                                                                                  5. **Lặp lại test với người thật** mỗi quý hoặc mỗi lần có thay đổi lớn về setup.
                                                                                                                                                                                                  
                                                                                                                                                                                                  Công sức bỏ ra chỉ khoảng nửa ngày, nhưng sẽ tiết kiệm cho bạn hàng chục issue kiểu \"không chạy được, help\" và hàng giờ onboarding cho người mới. Coi README như code: nó cần được test, và nó cần một môi trường sạch để chứng minh là nó chạy được.

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í