☕ NEW! 完成新手任務即可參加抽獎!LINE 星巴克禮券等你拿,名額有限!        🎉 推廣活動:邀請好友註冊 DevLearn,累積推薦抽 LINE 星巴克禮券! 活動詳情 →        🔥 活動期間 2026/4/1 - 5/31 |已有 0 人參加       
angular 進階

🧪 Angular 測試與部署

📌 Jasmine + Karma 單元測試

Angular CLI 內建 Jasmine(測試框架)和 Karma(測試執行器)。

// calculator.service.ts
@Injectable({ providedIn: 'root' })
export class CalculatorService {
  add(a: number, b: number): number { return a + b; }
  divide(a: number, b: number): number {
    if (b === 0) throw new Error('不能除以零');
    return a / b;
  }
}
// calculator.service.spec.ts — 測試檔案(.spec.ts)
import { TestBed } from '@angular/core/testing';
import { CalculatorService } from './calculator.service';

describe('CalculatorService', () => {
  let service: CalculatorService;

  beforeEach(() => {
    TestBed.configureTestingModule({});  // 設定測試模組
    service = TestBed.inject(CalculatorService);  // 取得服務實例
  });

  it('應該被建立', () => {
    expect(service).toBeTruthy();
  });

  it('1 + 2 應該等於 3', () => {
    expect(service.add(1, 2)).toBe(3);
  });

  it('10 / 2 應該等於 5', () => {
    expect(service.divide(10, 2)).toBe(5);
  });

  it('除以零應該拋出錯誤', () => {
    expect(() => service.divide(10, 0)).toThrowError('不能除以零');
  });
});
# 執行所有測試
ng test

# 執行測試並產生覆蓋率報告
ng test --code-coverage

📌 TestBed 元件測試

// greeting.component.ts
@Component({
  selector: 'app-greeting',
  template: `
    <h1>{{ greeting }}</h1>
    <button (click)="changeName('Angular')">打招呼</button>
  `
})
export class GreetingComponent {
  @Input() name = 'World';
  greeting = '';

  ngOnInit() { this.greeting = `Hello, ${this.name}!`; }

  changeName(newName: string) {
    this.name = newName;
    this.greeting = `Hello, ${this.name}!`;
  }
}
// greeting.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { GreetingComponent } from './greeting.component';

describe('GreetingComponent', () => {
  let component: GreetingComponent;
  let fixture: ComponentFixture<GreetingComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [GreetingComponent]  // Standalone component
    }).compileComponents();

    fixture = TestBed.createComponent(GreetingComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();  // 觸發初始化(ngOnInit)
  });

  it('應該顯示預設招呼語', () => {
    const h1 = fixture.nativeElement.querySelector('h1');
    expect(h1.textContent).toContain('Hello, World!');
  });

  it('應該根據 @Input 顯示不同名字', () => {
    component.name = 'Angular';
    component.ngOnInit();
    fixture.detectChanges();  // 觸發變更偵測
    const h1 = fixture.nativeElement.querySelector('h1');
    expect(h1.textContent).toContain('Hello, Angular!');
  });

  it('按下按鈕後應該改變招呼語', () => {
    const button = fixture.nativeElement.querySelector('button');
    button.click();
    fixture.detectChanges();
    const h1 = fixture.nativeElement.querySelector('h1');
    expect(h1.textContent).toContain('Hello, Angular!');
  });
});

📌 HttpClientTestingModule Mock API

// data.service.spec.ts
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { DataService } from './data.service';

describe('DataService', () => {
  let service: DataService;
  let httpMock: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],  // 使用測試用的 HttpClient
      providers: [DataService]
    });
    service = TestBed.inject(DataService);
    httpMock = TestBed.inject(HttpTestingController);
  });

  afterEach(() => {
    httpMock.verify();  // 確認沒有未處理的請求
  });

  it('GET /products 應回傳產品列表', () => {
    const mockProducts = [
      { id: 1, name: '筆電', price: 30000 },
      { id: 2, name: '手機', price: 15000 }
    ];

    service.getProducts().subscribe(products => {
      expect(products.length).toBe(2);
      expect(products[0].name).toBe('筆電');
    });

    // 攔截 HTTP 請求,回傳假資料
    const req = httpMock.expectOne('https://api.example.com/products');
    expect(req.request.method).toBe('GET');
    req.flush(mockProducts);  // 回傳假資料
  });

  it('應處理 HTTP 錯誤', () => {
    service.getProducts().subscribe({
      next: () => fail('應該要失敗'),
      error: (err) => {
        expect(err.status).toBe(500);
      }
    });

    const req = httpMock.expectOne('https://api.example.com/products');
    req.flush('Server Error', { status: 500, statusText: 'Internal Server Error' });
  });
});

📌 E2E 測試

// 使用 Cypress(目前推薦)
// cypress/e2e/app.cy.ts

describe('首頁', () => {
  beforeEach(() => {
    cy.visit('/');
  });

  it('應該顯示歡迎訊息', () => {
    cy.get('h1').should('contain', '歡迎使用');
  });

  it('導航到產品頁', () => {
    cy.get('a[routerLink="/products"]').click();
    cy.url().should('include', '/products');
    cy.get('.product-card').should('have.length.at.least', 1);
  });

  it('搜尋功能', () => {
    cy.get('input[placeholder="搜尋..."]').type('Angular');
    cy.get('.search-results').should('be.visible');
    cy.get('.search-result-item').should('have.length.at.least', 1);
  });
});

📌 Angular CLI 打包優化

# 生產環境打包(自動開啟 AOT 編譯、Tree Shaking、程式碼壓縮)
ng build --configuration=production

# 分析打包大小
ng build --stats-json
npx webpack-bundle-analyzer dist/my-app/stats.json

angular.json 打包設定

{
  "configurations": {
    "production": {
      "budgets": [
        {
          "type": "initial",
          "maximumWarning": "500kb",
          "maximumError": "1mb"
        }
      ],
      "outputHashing": "all",
      "optimization": true,
      "sourceMap": false
    }
  }
}

📌 部署到雲端

# 部署到 Firebase Hosting
npm install -g firebase-tools
firebase init hosting
ng build --configuration=production
firebase deploy

# 部署到 Azure Static Web Apps
# 在 GitHub Actions 中自動部署
ng build --configuration=production --output-path=dist

# 部署到 Nginx
# 將 dist/ 複製到 Nginx 的靜態資源目錄
# 重要:SPA 需要設定 URL 重寫
# Nginx 設定(SPA URL 重寫)
server {
    listen 80;
    root /usr/share/nginx/html;
    index index.html;

    location / {
        try_files $uri $uri/ /index.html;  # 所有路由都導向 index.html
    }
}

📌 小結

  • .spec.ts 檔案是測試檔案,Angular CLI 內建 Jasmine + Karma
  • TestBed 是 Angular 的測試工具,用來建立元件和服務的測試環境
  • HttpClientTestingModule 可以 Mock HTTP 請求,不需要真正的後端
  • E2E 測試推薦使用 Cypress
  • ng build --configuration=production 自動執行 AOT、Tree Shaking、壓縮
  • SPA 部署需要設定 URL 重寫(所有路由指向 index.html)

💡 大家的想法 · 0

載入中...
💬 即時聊天室 🟢 0 人在線
😀 😎 🤓 💻 🎮 🎸 🔥
➕ 新問題
📋 我的工單
💬 LINE 社群
🔒
需要註冊才能使用此功能
註冊帳號即可解鎖測驗、遊戲、簽到、筆記下載等所有功能,完全免費!
免費註冊