--- name: testing-deployment-implementation description: Write unit tests for components and services, implement E2E tests with Cypress, set up test mocks, optimize production builds, configure CI/CD pipelines, and deploy to production platforms. --- # Testing & Deployment Implementation Skill ## Unit Testing Basics ### TestBed Setup ```typescript import { TestBed } from '@angular/core/testing'; import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; describe('UserService', () => { let service: UserService; let httpMock: HttpTestingController; beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], providers: [UserService] }); service = TestBed.inject(UserService); httpMock = TestBed.inject(HttpTestingController); }); afterEach(() => { httpMock.verify(); }); }); ``` ### Component Testing ```typescript describe('UserListComponent', () => { let component: UserListComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [UserListComponent], imports: [CommonModule, HttpClientTestingModule], providers: [UserService] }).compileComponents(); fixture = TestBed.createComponent(UserListComponent); component = fixture.componentInstance; }); it('should display users', () => { const mockUsers: User[] = [ { id: 1, name: 'John' }, { id: 2, name: 'Jane' } ]; component.users = mockUsers; fixture.detectChanges(); const compiled = fixture.nativeElement as HTMLElement; const userElements = compiled.querySelectorAll('.user-item'); expect(userElements.length).toBe(2); }); it('should call service on init', () => { const userService = TestBed.inject(UserService); spyOn(userService, 'getUsers').and.returnValue(of([])); component.ngOnInit(); expect(userService.getUsers).toHaveBeenCalled(); }); }); ``` ### Testing Async Operations ```typescript // Using fakeAsync and tick it('should load users after delay', fakeAsync(() => { const userService = TestBed.inject(UserService); spyOn(userService, 'getUsers').and.returnValue( of([{ id: 1, name: 'John' }]).pipe(delay(1000)) ); component.ngOnInit(); expect(component.users.length).toBe(0); tick(1000); expect(component.users.length).toBe(1); })); // Using waitForAsync it('should handle async operations', waitForAsync(() => { const userService = TestBed.inject(UserService); spyOn(userService, 'getUsers').and.returnValue( of([{ id: 1, name: 'John' }]) ); component.ngOnInit(); fixture.whenStable().then(() => { expect(component.users.length).toBe(1); }); })); ``` ## Mocking Services ### HTTP Mocking ```typescript it('should fetch users from API', () => { const mockUsers: User[] = [{ id: 1, name: 'John' }]; service.getUsers().subscribe(users => { expect(users.length).toBe(1); expect(users[0].name).toBe('John'); }); const req = httpMock.expectOne('/api/users'); expect(req.request.method).toBe('GET'); req.flush(mockUsers); }); // POST with error handling it('should handle errors', () => { service.createUser({ name: 'Jane' }).subscribe( () => fail('should not succeed'), (error) => expect(error.status).toBe(400) ); const req = httpMock.expectOne('/api/users'); req.flush('Invalid user', { status: 400, statusText: 'Bad Request' }); }); ``` ### Service Mocking ```typescript class MockUserService { getUsers() { return of([ { id: 1, name: 'John' }, { id: 2, name: 'Jane' } ]); } } @Component({ selector: 'app-test', template: '
{{ (users$ | async)?.length }}
' }) class TestComponent { users$ = this.userService.getUsers(); constructor(private userService: UserService) {} } describe('TestComponent with Mock', () => { let component: TestComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [TestComponent], providers: [ { provide: UserService, useClass: MockUserService } ] }).compileComponents(); fixture = TestBed.createComponent(TestComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should render users', () => { const div = fixture.nativeElement.querySelector('div'); expect(div.textContent).toContain('2'); }); }); ``` ## E2E Testing with Cypress ### Basic E2E Test ```typescript describe('User List Page', () => { beforeEach(() => { cy.visit('/users'); }); it('should display user list', () => { cy.get('[data-testid="user-item"]') .should('have.length', 10); }); it('should filter users by name', () => { cy.get('[data-testid="search-input"]') .type('John'); cy.get('[data-testid="user-item"]') .should('have.length', 1) .should('contain', 'John'); }); it('should navigate to user detail', () => { cy.get('[data-testid="user-item"]').first().click(); cy.location('pathname').should('include', '/users/'); cy.get('[data-testid="user-detail"]').should('be.visible'); }); }); ``` ### Page Object Model ```typescript // user.po.ts export class UserPage { navigateTo(path: string = '/users') { cy.visit(path); return this; } getUsers() { return cy.get('[data-testid="user-item"]'); } getUserByName(name: string) { return cy.get('[data-testid="user-item"]').contains(name); } clickUser(index: number) { this.getUsers().eq(index).click(); return this; } searchUser(query: string) { cy.get('[data-testid="search-input"]').type(query); return this; } } // Test using PO describe('User Page', () => { const page = new UserPage(); beforeEach(() => { page.navigateTo(); }); it('should find user by name', () => { page.searchUser('John'); page.getUsers().should('have.length', 1); }); }); ``` ## Build Optimization ### AOT Compilation ```typescript // angular.json { "projects": { "app": { "architect": { "build": { "options": { "aot": true, "outputHashing": "all", "sourceMap": false, "optimization": true, "buildOptimizer": true, "namedChunks": false } } } } } } ``` ### Bundle Analysis ```bash # Install webpack-bundle-analyzer npm install --save-dev webpack-bundle-analyzer # Run analysis ng build --stats-json webpack-bundle-analyzer dist/app/stats.json ``` ### Code Splitting ```typescript // app-routing.module.ts const routes: Routes = [ { path: '', component: HomeComponent }, { path: 'admin', loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule) }, { path: 'users', loadChildren: () => import('./users/users.module').then(m => m.UsersModule) } ]; ``` ## Deployment ### Production Build ```bash # Build for production ng build --configuration production # Output directory dist/app/ # Serve locally npx http-server dist/app/ ``` ### Deployment Targets **Firebase:** ```bash npm install -g firebase-tools firebase login firebase init hosting firebase deploy ``` **Netlify:** ```bash npm run build # Drag and drop dist/ folder to Netlify # Or use CLI: npm install -g netlify-cli netlify deploy --prod --dir=dist/app ``` **GitHub Pages:** ```bash ng build --output-path docs --base-href /repo-name/ git add docs/ git commit -m "Deploy to GitHub Pages" git push # Enable in repository settings ``` **Docker:** ```dockerfile # Build stage FROM node:18 as build WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build # Serve stage FROM nginx:alpine COPY --from=build /app/dist/app /usr/share/nginx/html COPY nginx.conf /etc/nginx/nginx.conf EXPOSE 80 CMD ["nginx", "-g", "daemon off;"] ``` ## CI/CD Pipelines ### GitHub Actions ```yaml name: CI/CD on: push: branches: [main] pull_request: branches: [main] jobs: build-and-test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Setup Node.js uses: actions/setup-node@v3 with: node-version: '18' - name: Install dependencies run: npm ci - name: Lint run: npm run lint - name: Build run: npm run build - name: Test run: npm run test -- --watch=false --code-coverage - name: E2E Test run: npm run e2e - name: Upload coverage uses: codecov/codecov-action@v3 with: files: ./coverage/lcov.info - name: Deploy if: github.ref == 'refs/heads/main' run: npm run deploy ``` ## Performance Monitoring ### Core Web Vitals ```typescript // Using web-vitals library import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals'; getCLS(console.log); getFID(console.log); getFCP(console.log); getLCP(console.log); getTTFB(console.log); ``` ### Error Tracking (Sentry) ```typescript import * as Sentry from "@sentry/angular"; Sentry.init({ dsn: "https://examplePublicKey@o0.ingest.sentry.io/0", integrations: [ new Sentry.BrowserTracing(), new Sentry.Replay(), ], tracesSampleRate: 1.0, replaysSessionSampleRate: 0.1, replaysOnErrorSampleRate: 1.0, }); @NgModule({ providers: [ { provide: ErrorHandler, useValue: Sentry.createErrorHandler(), }, ], }) export class AppModule {} ``` ## Testing Best Practices 1. **Arrange-Act-Assert**: Clear test structure 2. **One Assertion per Test**: Keep tests focused 3. **Test Behavior**: Not implementation details 4. **Use Page Objects**: For E2E tests 5. **Mock External Dependencies**: Services, HTTP 6. **Test Error Cases**: Invalid input, failures 7. **Aim for 80% Coverage**: Don't obsess over 100% ## Coverage Report ```bash # Generate coverage report ng test --code-coverage # View report open coverage/index.html ``` ## Resources - [Jasmine Documentation](https://jasmine.github.io/) - [Angular Testing Guide](https://angular.io/guide/testing) - [Cypress Documentation](https://docs.cypress.io/) - [Testing Best Practices](https://angular.io/guide/testing-best-practices)