Testing & Deployment Implementation Skill
Unit Testing Basics
TestBed Setup
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
describe('UserListComponent', () => { let component: UserListComponent; let fixture: ComponentFixture<UserListComponent>;
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
// 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
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
class MockUserService { getUsers() { return of([ { id: 1, name: 'John' }, { id: 2, name: 'Jane' } ]); } }
@Component({ selector: 'app-test', template: '<div>{{ (users$ | async)?.length }}</div>' }) class TestComponent { users$ = this.userService.getUsers(); constructor(private userService: UserService) {} }
describe('TestComponent with Mock', () => { let component: TestComponent; let fixture: ComponentFixture<TestComponent>;
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
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
// 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
// angular.json { "projects": { "app": { "architect": { "build": { "options": { "aot": true, "outputHashing": "all", "sourceMap": false, "optimization": true, "buildOptimizer": true, "namedChunks": false } } } } } }
Bundle Analysis
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
// 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
Build for production
ng build --configuration production
Output directory
dist/app/
Serve locally
npx http-server dist/app/
Deployment Targets
Firebase:
npm install -g firebase-tools firebase login firebase init hosting firebase deploy
Netlify:
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:
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:
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
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
// 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)
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
-
Arrange-Act-Assert: Clear test structure
-
One Assertion per Test: Keep tests focused
-
Test Behavior: Not implementation details
-
Use Page Objects: For E2E tests
-
Mock External Dependencies: Services, HTTP
-
Test Error Cases: Invalid input, failures
-
Aim for 80% Coverage: Don't obsess over 100%
Coverage Report
Generate coverage report
ng test --code-coverage
View report
open coverage/index.html
Resources
-
Jasmine Documentation
-
Angular Testing Guide
-
Cypress Documentation
-
Testing Best Practices